现在大多数时候都是用SpringBoot来整合Mybatis了,很多细节也都被隐藏掉了。而XML是Mybatis最原始的用法,学习这些用法。有助于我们理解Mybatis的底层运行机制
一 权限控制需求
权限管理的需求:
一个用户拥有若干角色,一个角色拥有若干权限,权限就是对某个资源(模块〉的某种操作(增、删、改、查),这样就构成了“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间、角色与权限之间,一般是多对多
的关系
表关系如下:
1.1 环境搭建
在数据库中建立这5张表,并在模块中创建5个对应的实体类
这部分代码省略,可以去参考资料中获取资源
注意点
- 一般我们都是表中用下划线命名法,实体用驼峰命名法
- 再数据库字段和Java类型的对应关系中,需要注意特殊的类型
byte[]
.这个类型一般对应数据库中的BLOB\LONGVARBINARY
以及一些和二级制有关的字段类型。 - 由于Java中的基本类型有默认值,无法表示null(
字段不存在
)。所以实体类字段需要使用引用类型 - 创建代码的过程,在学习
Mybatis Generator
之后可以用代码代替
在这里,我新建了一个simple_all模块,可以将之前的配置文件直接复制到新项目
二 使用XML 方式
2.1 Mybatis3.0的变化
MyBatis 3.0 相比2.0 版本的一个最大变化,就是支持使用接口来调用方法。
以前使用SqlSession 通过命名空间
调用MyBatis 方法时,首先需要用到命名空间和方法id 组成的字符串来调用相应的方法。当参数多于1 个
的时候,需要将所有参数放到一个Map
对象中。通过Map 传递多个参数,使用起来很不方便,而且还无法避免很多重复的代码。
使用接口调用方式就会方便很多,MyBatis 使用Java 的动态代理可以直接通过接口来调用相应的方法,不需要提供接口的实现类
,更不需要在实现类中使用SqlSession以通过命名空间间接调用。另外,当有多个参数的时候,通过参数注解@Param
设置参数的名字省去了手动构造Map 参数的过程
注意点
接口可以配合XML 使用,也可以配合注解来使用。XML 可以单独使用,但是注解必须在接口中使用
。(我的理解就是xml可以用2.0的用法,直接配合SqlSession使用
)
2.2 创建映射文件
先在src/main/resource 下面创建目录(这个目录不是乱写的,后面会说到,主要就是要和包名类似
),然后建立5个空的xml映射文件
结构如下
然后在src/main/resource建立5个接口,注意这个5接口的包名和映射文件的目录结构一致
打开UserMapper.xnl 文件,在文件中输入以下内容。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="zyc.mybatis.simple.mapper.UserMapper">
</mapper>
注意其中namespace属性。
当Mapper 接口和XML 文件关联的时候,命名空间namespace 的值就需要配置成接口的全限定名称
, MyBatis 内部就是通过这个值将接口和XML 关联起来的(重要点!
) 甚至在idea中编程,都会有代码提示
继续创建其他4个mapper映射文件的内容
在mybati s -config.xml
配置文件中的mappers 元素中配置所有的mapper ,原来我们需要像下面这么配
但是这样太麻烦了,我们改成
<mappers>
<package name="zyc.mybatis.simple.mapper"/>
</mappers>
配置包名匹配原理
这种配置方式会先查找zyc.mybatis.simple.mapper 包下所有的接口
,循环对接口进行如下操作。
- 判断接口对应的命名空间是否己经存在,如果
存在就抛出异常
,不存在就继续进行接下来的操作。 - 加载接口对应的xml映射文件,
将接口全限定名转换为路径
,例如,将接口
zyc.mybatis.simple.mapper.UserMapper 转换为zyc/mybatis/simple/mapper/UserMapper. xml,
以.xml 为后缀搜索XML资源,如果找到就解析XML
。 - 处理接口中的注解方法。
因为这里的接口和XML 映射文件完全符合上面操作的第2 点
,因此直接配置包名就能自动扫描包下的接口和XML 映射文件,省去了很多麻烦。准备好这一切后就可以开始学习具体的用法了
三 select用法
3.1 简单查询
在UserMapper接口中添加方法
/**
* 通过 id 查询用户
*
* @param id
* @return
*/
SysUser selectById(Long id);
在UserMapper.xml中添加查询语句
<resultMap id="userMap" type="zyc.mybatis.simple.model.SysUser">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<result property="userPassword" column="user_password"/>
<result property="userEmail" column="user_email"/>
<result property="userInfo" column="user_info"/>
<result property="headImg" column="head_img" jdbcType="BLOB"/>
<result property="createTime" column="create_time" jdbcType="TIMESTAMP"/>
</resultMap>
<select id="selectById" resultMap="userMap">
select * from sys_user where id = #{id}
</select>
3.2 方法映射规则
接口和XML 是通过将namespace 的值设置为接口的全限定名称来进行关联
的,那么接口中方法和XML 又是怎么关联的呢?
可以发现,XML 中的select 标签的id 属性值和定义的接口方法名是一样的。MyBatis就是通过这种方式将接口方法和XML 中定义的SQL 语句关联到一起的,如果接口方法没有和XML 中的id 属性值相对应,启动程序便会报错
规则如下
- 当
只使用XML
而不使用接口的时候,namespace 的值可以设置为任意不重复的名称
。 - 标签的id 属性值在任何时候都
不能出现英文句号“.”
,并且同一个命名空间下不能出现重复的id
- 一般情况下我们不使用重载。测试可以两个方法对应一个标签
(关键这样写没意义)
。
3.3 标签和属性
< select >:
映射查询语句使用的标签。
- id :命名空间中的唯一标识符,可用来代表这条语句。
- resultMap :用于设置返回值的类型和
映射关系
<resultMap>
- id :必填,并且唯一。在select 标签中,resultMap 指定的值即为此处id 所设置的值。
- type :必填,用于配置查询列所映射到的Java 对象类型。
- extends :选填,可以配置当前的resultMap 继承自其他的re sultMap ,属性值为继承resultMap 的id
- autoMapping :选填,可选值为true 或false ,用于配置是
否启用非映射字段(
没有在resultMap 中配置的字段〉的自动映射功能,该配置可以覆盖全局的autoMappingBehavior
配置。
<resultMap>包含的标签
- constructor :配置使用构造方法注入结果,包含以下两个子标签
- idArg: id 参数,标记结果作为id (唯一值),可以帮助提高整体性能。
- arg :注入到构造方法的一个普通结果。
- id:一个id 结果,标记结果作为id (唯一值),可以帮助提高整体性能。
- result :注入到Java 对象属性的普通结果
- association :一个复杂的类型关联,许多结果将包成这种类型(
后面会提
) - collection :复杂类型的集合。(
后面会提
) - discriminator :根据结果值来决定使用哪个结果映射。(
后面会提
) - case :基于某些值的结果映射。
id 和result 标签包含的属性
- column :从数据库中得到的
列名,或者是列的别名
。 - property :
映射到列结果的属性
。可以映射简单的如“username ”这样的属性,也可以映射一些复杂对象中的属性,例如“address.street.number ”,这会通过“.”方式的属性嵌套赋值。 - javaType :一个Java类的完全限定名,或一个类型别名(通过typeAlias 配置或者默认的类型)。如果映射到一个JavaBean, M yBatis 通常可以
自动判断属性的类型
。如果映射到HashMap ,则需要明确地指定javaType 属性。 - jdbcType :
列对应的数据库类型
。JDB C 类型仅仅需要对插入、更新、删除操作可能为空的列进行处理。这是JDBC jdbcType 的需要,而不是MyBatis 的需要 - typeHandler :使用这个属性可以覆盖默认的类型处理器。这个属性值是类的完全限定名或类型别名。
3.4 查询多条记录
接口增加方法
/**
* 查询全部用户
*
* @return
*/
List<SysUser> selectAll();
映射文件增加标签
<select id="selectAll" resultType="zyc.mybatis.simple.model.SysUser">
select id,
user_name userName,
user_password userPassword,
user_email userEmail,
user_info userInfo,
head_img headImg,
create_time createTime
from sys_user
</select>
selectByid 和selectAll 的区别
selectByid 中使用了resultMap
来设置结果映射,而selectAll 中则通过resultType
直接指定了返回结果的类型。可以发现,如果使用resultType 来设置返回结果的类型,需要在SQL 中为所有列名和属性名不一致的列设置别名
,通过设置别名使最终的查询结果列和resultType 指定对象的属性名保持一致,进而实现自动映射。
名称映射规则
在数据库中,由于大多数数据库设置不区分大小写,因此下画线方式的命名很常见,如user name 、user email 。在Java 中,一般都使用驼峰式命名,如userName 、userEmail 。因为数据库和Java 中的这两种命名方式很常见,因此MyBatis 还提供了一个 全局属性mapUnderscoreToCamelCase ,通过配置这个属性为true 可以自动将以下画线方式命名的数据库列映射
到Java 对象的驼峰式命名属性中。这个属性默认为false
3.5 测试之前写的方法
测试基本类,父类。给其他类继承
/**
* 基础测试类
*/
public class BaseMapperTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void init(){
try {
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
} catch (IOException ignore) {
ignore.printStackTrace();
}
}
public SqlSession getSqlSession(){
return sqlSessionFactory.openSession();
}
}
测试类
public class UserMapperTest extends BaseMapperTest {
@Test
public void testSelectById(){
//获取 sqlSession
SqlSession sqlSession = getSqlSession();
try {
//获取 UserMapper 接口
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//调用 selectById 方法,查询 id = 1 的用户
SysUser user = userMapper.selectById(1l);
//user 不为空
Assert.assertNotNull(user);
//userName = admin
Assert.assertEquals("admin", user.getUserName());
} finally {
//不要忘记关闭 sqlSession
sqlSession.close();
}
}
@Test
public void testSelectAll(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
//调用 selectAll 方法查询所有用户
List<SysUser> userList = userMapper.selectAll();
//结果不为空
Assert.assertNotNull(userList);
//用户数量大于 0 个
Assert.assertTrue(userList.size() > 0);
} finally {
//不要忘记关闭 sqlSession
sqlSession.close();
}
}
}
这两个方法在正常情况下都能测试通过
问题验证
修改映射文件所在的目录,验证接口自动寻找规则
报错了,符合预期
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): zyc.mybatis.simple.mapper.UserMapper.selectAll
3.6 多表关联查询(复杂的用法)
查询一个类的数据
UserMapper接口增加
/**
* 根据用户 id 获取角色信息
*
* @param userId
* @return
*/
List<SysRole> selectRolesByUserId(Long userId);
对应映射
<select id="selectRolesByUserId" resultType="zyc.mybatis.simple.model.SysRole">
select
r.id,
r.role_name roleName,
r.enabled,
r.create_by createBy,
r.create_time createTime
from sys_user u
inner join sys_user_role ur on u.id = ur.user_id
inner join sys_role r on ur.role_id = r.id
where u.id = #{userId}
</select>
查询多个类
假设查询的结果不仅要包含sysrole 中
的信息,还要包含当前用户的部分信息
(不考虑嵌套的情况),例如增加查询列u .u sername as userName 。这时resultType 该如何设置呢?
方法一
在SysRole 对象中直接添加userName 属性,这样仍然使用SysRole 作为返回值.
增加一个类,继承SysRole ,我的理解就是一个便于关系映射的类
```public class SysRoleExtend extends SysRole {
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
其他旧省略了
方法二
在SysRole类中,增加一个SysUser的字段(这种应该比较我们正常的开发方式
)
/**
* 用户信息
*/
private SysUser user;
调整之前的映射配置,增加两列
<select id="selectRolesByUserId" resultType="zyc.mybatis.simple.model.SysRole">
select
r.id,
r.role_name roleName,
r.enabled,
r.create_by createBy,
r.create_time createTime,
u.user_name as "user.userName",
u.user_email as "user.userEmail"
from sys_user u
inner join sys_user_role ur on u.id = ur.user_id
inner join sys_role r on ur.role_id = r.id
where u.id = #{userId}
</select>
四 insert用法
4.1 标签包含的属性
- id :命名空间中的唯一标识符,可用来代表这条语句。
- parameterType :即将传入的语句参数的完全限定类名或别名。这个属性是可选的,因为MyBatis 可以推断出传入语句的具体参数,因此
不建议配置该属性
。 - flushCache :默认值为true ,任何时候只要语句被调用,都会清空一级缓存和二级缓存。
- timeout :设置在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。
- statementType :对于STATEMENT 、PREPARED 、C ALLABLE, MyBatis 会分别使用对应的Statement 、PreparedStatement 、CallableStatement ,
默认值为PREPARED
。 - useGeneratedKeys :默认值为false 。如果设置为true, MyBatis 会使用JDBC的
getGeneratedKeys 方法来取出由数据库内部生成的主键。 - keyProperty : MyBatis 通过getGeneratedKeys 获取主键值后将要赋值的属性名。如果希望得到多个数据库自动生成的列,属性值也可以是
以逗号分隔
的属性名称列表。 - keyColumn :仅对INSERT 和UPDATE 有用。通过生成的键值设置表中的列名,这个设置仅在某些数据库(如PostgreSQL )中是必须的,当主键列不是表中的第一列时需要设置。如果希望得到多个生成的列,也可以是逗号分隔的属性名称列表。(
多个时,和keyProperty一一对应
) - databaseId :如果配置了databaseidProvider ,MyBatis会加载所有的不带databaseid 的或匹配当前databaseid 的语句。如果同时存在带databaseid 和不带databaseid 的语句,
后者会被忽略(主要语句在不同的数据库执行时会用到)
。
为了防止类型错误,对于一些特殊的数据类型,建议指定具体的jdbcType 值。例如headimg 指定BLOB 类型,createTime指定TIM STAMP 类型。
Mapper接口增加方法
/**
* 插入一个用户
* @param sysUser
* @return int表示的是影响的行数
*/
int insert(SysUser sysUser);
映射文件增加
<!-- *注意这里可以指定jdbc的类型 -->
<insert id="insert">
insert into sys_user(
user_name, user_password, user_email,
user_info, head_img, create_time)
values(
#{userName}, #{userPassword}, #{userEmail},
#{userInfo}, #{headImg, jdbcType=BLOB}, #{createTime, jdbcType=TIMESTAMP})
</insert>
4.2 使用JDBC 方式返回主键自增的值
只需要修改两个属性即可
接口增加方法
/**
* 插入一个用户,带有id。
* 使用 useGeneratedKeys 方式
* @param sysUser
* @return int表示的是影响的行数
*/
int insert2(SysUser sysUser);
映射文件
<insert id="insert2" useGeneratedKeys="true" keyProperty="id">
//不变...
</insert>
debug可以发现id有值了
4.3 使用selectKey 返回主键的值
上面的方式适用于支持主键自增的数据库,但Oracle一般id都是取之序列。所以不适用。
selectKey 标签两种类型都支持。
/**
* selectkey方式
* @param sysUser
* @return
*/
int insert3(SysUser sysUser);
xml
<insert id="insert3">
insert into sys_user(
user_name, user_password, user_email,
user_info, head_img, create_time)
values(
#{userName}, #{userPassword}, #{userEmail},
#{userInfo}, #{headImg, jdbcType=BLOB}, #{createTime, jdbcType=TIMESTAMP})
<selectKey keyColumn="id" resultType="long" keyProperty="id" order="AFTER">
SELECT LAST_INSERT_ID()
</selectKey>
</insert>
<!-- Oracle 的例子,查询多个列的时候需要 keyColumn -->
<insert id="insertOracle">
<selectKey keyColumn="id" resultType="long" keyProperty="id" order="BEFORE">
SELECT SEQ_USER.nextval from dual
</selectKey>
insert into sys_user(
id, user_name, user_password, user_email,
user_info, head_img, create_time)
values(
#{id}, #{userName}, #{userPassword}, #{userEmail},
#{userInfo}, #{headImg, jdbcType=BLOB}, #{createTime, jdbcType=TIMESTAMP})
</insert>
也成功了
属性说明
- resultType 用于设置
返回值类型
。 - order 属性的设置和使用的数据库有关。
在MySQL 数据库中,order 属性设置的值是AFTER
,因为当前记录的主键值在insert 语句
执行成功后才能获取到。而在Oracle 数据库中,order 的值要设置为BEFORE
,这是因为Oracl e
中需要先从序列获取值,然后将值作为主键插入到数据库中
五 update用法
接口
int updateById(SysUser sysUser);
映射文件
<update id="updateById">
update sys_user
set user_name = #{userName},
user_password = #{userPassword},
user_email = #{userEmail},
user_info = #{userInfo},
head_img = #{headImg, jdbcType=BLOB},
create_time = #{createTime, jdbcType=TIMESTAMP}
where id = #{id}
</update>
六 delete用法
下面两种方式都行
/**
* 通过主键删除
*
* @param id
* @return
*/
int deleteById(Long id);
/**
* 通过主键删除
*
* @param sysUser
* @return
*/
int deleteById(SysUser sysUser);
xml(两个接口对应一个标签
)
<delete id="deleteById">
delete from sys_user where id = #{id}
</delete>
七 多个接口参数的用法
不涉及集合和数组(之后会讲
)
如果我们的需求是多个参数的话,我们可以使用Map或@Param
.map就是以key来对应xml中sql使用的参数值名字,value来存放参数值(不建议,太麻烦,还要自己拼一个Map
),@param是比较推荐的方式。
/**
* 根据用户 id 和 角色的 enabled 状态获取用户的角色
*
* @param userId
* @param enabled
* @return
*/
List<SysRole> selectRolesByUserIdAndRoleEnabled(@Param("userId")Long userId, @Param("enabled")Integer enabled);
<select id="selectRolesByUserIdAndRoleEnabled" resultType="zyc.mybatis.simple.model.SysRole">
select
r.id,
r.role_name roleName,
r.enabled,
r.create_by createBy,
r.create_time createTime
from sys_user u
inner join sys_user_role ur on u.id = ur.user_id
inner join sys_role r on ur.role_id = r.id
where u.id = #{userId} and r.enabled = #{enabled}
</select>
当参数类型是一些JavaBean 的时候,用法略有不同 .我们不能直接使用属性名,而是要用.
.如
#{user.name}
八 动态代理实现原理
前面我们使用MyBatis时,只需要声明接口即可,原理是生成了动态代理类。但这种动态代理方式不想我们在spring中经常碰到的jdk动态代理或者cglib动态代理方式(技术还是jdk动态代理
)
大体逻辑是通过接口名和方法名,确认方法在映射文件中的id。然后使用最原始的SqlSession来查询。
下面是一个简单的实现(实际肯定更加复杂
)
InvocationHandler实现
public class MyMapperProxy<T> implements InvocationHandler {
private Class<T> mapperInterface;
private SqlSession sqlSession;
public MyMapperProxy(Class<T> mapperInterface, SqlSession sqlSession) {
this.mapperInterface = mapperInterface;
this.sqlSession = sqlSession;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//针对不同的 sql 类型,需要调用 sqlSession 不同的方法
//参数也有很多情况,这里只考虑一个参数的情况
List<?> list = sqlSession.selectList(mapperInterface.getCanonicalName() + "." + method.getName());
//返回值也有很多情况
return list;
}
}
测试方法
@Test
public void testMyMapperProxy(){
SqlSession sqlSession = getSqlSession();
MyMapperProxy<UserMapper> userMapperMyMapperProxy = new MyMapperProxy<>(UserMapper.class,sqlSession);
UserMapper userMapper = (UserMapper) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{UserMapper.class},userMapperMyMapperProxy);
List<SysUser> list = userMapper.selectAll();
System.out.println(list.size());
}