MyBatis从入门到精通(第2章):MyBatis XML方式的基本用法【select用法】
本章将通过完成权限管理的常见业务来学习 MyBatis XML方式的基本用法。
这个权限管理控制,采用RBAC(Role-Based Access Control,基于角色的访问控制)方式。
2.1 一个简单的权限控制需求
权限管理的需求:一个用户拥有若干角色,一个角色拥有若干权限,权限就是对某个模块资源的某种操作(增、删、改、查),这便是“用户-角色-权限”的授权模型。
采用RBAC授权模型,用户与角色之间、角色与权限之间,一般是多对多的关系。
2.1.1 创建数据库表
在已经创建好的 testmybatis 数据库中执行如下SQL脚本。(如何通过SQL脚本用Navicat管理数据库,请参考我上一篇博客的 1.3.1 准备数据库)
执行如下脚本创建上图中的5张表:用户表,角色表,权限表,用户角色关联表,角色权限关联表。
本书中此处没有创建表之间的外键关系,对于表之间的关系,会通过业务逻辑来进行限制。
-- --2.1.1数据库创建5张表 CREATE TABLE sys_user( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', user_name VARCHAR(50) COMMENT '用户名', user_password VARCHAR(50) COMMENT '密码', user_email VARCHAR(50) COMMENT '邮箱', user_info TEXT COMMENT '简介', head_img BLOB COMMENT '头像', create_time DATETIME COMMENT '创建时间', PRIMARY KEY (id) ); ALTER TABLE sys_user COMMENT '用户表'; CREATE TABLE sys_role ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '角色ID', role_name VARCHAR(50) COMMENT '角色名', enabled INT COMMENT '有效标志', create_by BIGINT COMMENT '创建人', create_time DATETIME COMMENT '创建时间', PRIMARY KEY (id) ); ALTER TABLE sys_role COMMENT '角色表'; CREATE TABLE sys_privilege ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '权限ID', privilege_name VARCHAR(50) COMMENT '权限名称', privilege_url VARCHAR(200) COMMENT '权限URL', PRIMARY KEY (id) ); ALTER TABLE sys_privilege COMMENT '权限表'; CREATE TABLE sys_user_role ( user_id BIGINT COMMENT '用户ID', role_id BIGINT COMMENT '角色ID' ); ALTER TABLE sys_user_role COMMENT '用户角色关联表'; CREATE TABLE sys_role_privilege ( role_id BIGINT COMMENT '角色ID', privilege_id BIGINT COMMENT '权限ID' ); ALTER TABLE sys_role_privilege COMMENT '角色权限关联表';
为了方便后面的测试,接着在表中用SQL脚本插入一些测试数据。
-- --2.1.1插入测试数据 INSERT INTO `sys_user` VALUES ('1','admin','123456','admin@mybatis.tk','管理员',NULL,'2019-07-12 17:00:58'); INSERT INTO `sys_user` VALUES ('1001','test','123456','test@mybatis.tk','测试用户',NULL,'2019-07-12 17:01:52'); INSERT INTO `sys_role` VALUES ('1','管理员','1','1','2019-11-17 18:54:48'); INSERT INTO `sys_role` VALUES ('2','普通用户','1','1',current_timestamp); INSERT INTO `sys_user_role` VALUES ('1','1'); INSERT INTO `sys_user_role` VALUES ('1','2'); INSERT INTO `sys_user_role` VALUES ('1001','2'); INSERT INTO `sys_privilege` VALUES ('1','用户管理','/users'); INSERT INTO `sys_privilege` VALUES ('2','角色管理','/roles'); INSERT INTO `sys_privilege` VALUES ('3','系统日志','/logs'); INSERT INTO `sys_privilege` VALUES ('4','人员维护','/persons'); INSERT INTO `sys_privilege` VALUES ('5','单位维护','/companies'); INSERT INTO `sys_role_privilege` VALUES ('1','1'); INSERT INTO `sys_role_privilege` VALUES ('1','3'); INSERT INTO `sys_role_privilege` VALUES ('1','2'); INSERT INTO `sys_role_privilege` VALUES ('2','4'); INSERT INTO `sys_role_privilege` VALUES ('2','5');
2.1.2 创建实体类
在包(package) cn.bjut.simple.model 下依次创建这5张数据库表 未来(查询结果)映射的实体类。
用户表
package cn.bjut.simple.model; import java.util.Date; public class SysUser { public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getUserPassword() { return userPassword; } public void setUserPassword(String userPassword) { this.userPassword = userPassword; } public String getUserEmail() { return userEmail; } public void setUserEmail(String userEmail) { this.userEmail = userEmail; } public String getUserInfo() { return userInfo; } public void setUserInfo(String userInfo) { this.userInfo = userInfo; } public byte[] getHeadImg() { return headImg; } public void setHeadImg(byte[] headImg) { this.headImg = headImg; } public Date getCreateTime() { return createTime; } public void setCreateTime(Date createTime) { this.createTime = createTime; } /** * 用户ID */ private Long id; /** * 用户名 */ private String userName; /** * 密码 */ private String userPassword; /** * 邮箱 */ private String userEmail; /** * 简介 */ private String userInfo; /** * 头像 */ private byte[] headImg; /** * 创建时间 */ private Date createTime; }
用户与角色关联表
package cn.bjut.simple.model; /** * 用户角色关联表 */ public class SysUserRole { private Long userId; private Long roleId; public Long getUserId() { return userId; } public Long getRoleId() { return roleId; } public void setUserId(Long userId) { this.userId = userId; } public void setRoleId(Long roleId) { this.roleId = roleId; } }
角色表
package cn.bjut.simple.model; import java.util.Date; /** * 角色表 */ public class SysRole { private Long id; private String roleName; private Integer enabled; private Long createBy; private Date createTime; private SysUser user; public Long getId() { return id; } public String getRoleName() { return roleName; } public Integer getEnabled() { return enabled; } public Long getCreateBy() { return createBy; } public Date getCreateTime() { return createTime; } public SysUser getUser() { return user; } public void setId(Long id) { this.id = id; } public void setRoleName(String roleName) { this.roleName = roleName; } public void setEnabled(Integer enabled) { this.enabled = enabled; } public void setCreateBy(Long createBy) { this.createBy = createBy; } public void setCreateTime(Date createTime) { this.createTime = createTime; } public void setUser(SysUser user) { this.user = user; } }
权限表
package cn.bjut.simple.model; /** * 权限表 */ public class SysPrivilege { private Long id; private String privilegeName; private String privilegeUrl; public Long getId() { return id; } public String getPrivilegeName() { return privilegeName; } public String getPrivilegeUrl() { return privilegeUrl; } public void setId(Long id) { this.id = id; } public void setPrivilegeName(String privilegeName) { this.privilegeName = privilegeName; } public void setPrivilegeUrl(String privilegeUrl) { this.privilegeUrl = privilegeUrl; } }
角色与权限关联表
package cn.bjut.simple.model; /** * 角色与权限关联表 */ public class SysRolePrivilege { private Long roleId; private Long privilege; public Long getRoleId() { return roleId; } public Long getPrivilege() { return privilege; } public void setRoleId(Long roleId) { this.roleId = roleId; } public void setPrivilege(Long privilege) { this.privilege = privilege; } }
可以参考上面创建Java实体类[JavaBean]的方式依次完成 SysUserRole 、SysRole 、SysPrivilege 、SysRolePrivilege 四个实体类的代码。
另外还可以根据本书在 第5章介绍使用MyBatis官方提供的工具 MyBatis Generator 插件 ,根据数据库表的字段信息自动生成这些实体类。
- MyBatis默认遵循(从SQL到JAVA)“下划线转驼峰”的命名方式。如sys_user表对应的实体类名是SysUser,数据库字段user_name对应的实体类的变量名是userName。
- 在实体类中不要使用Java的基本数据类型,基本类型包括 byte、int、short、long、float、doubule、char、boolean。因为Java基本类型会有默认值,例如当某个实体类(对应着一个数据库表)中存在private int age;如果使用age != null进行判断,结果总会为true 会导致很多隐藏的问题。一个特殊的类型“byte[]”不是Java基本数据类型。
2.2 使用接口+XML方式
注意:接口可以配合XML使用,也可以配合注解来使用。SSM整合或SpringBoot开发,一般使用接口+通用Mapper+MyBatis3实现单表CRUD操作。
首先,在 src/main/resources 的 cn.bjut.simple.mapper目录下创建5个表各自对应的XML映射文件,分别为 UserMapper.xml 、RoleMapper 、PrivilegeMapper 、 UserRoleMapper 和 RolePrivilegeMapper.xml 。
然后,在 src/main/java 下面创建包 cn.bjut.simple.mapper 。接着在该包下创建XML文件对应的接口类,分别为 UserMapper.java、RoleMapper、PrivilegeMapper、 UserRoleMapper 和 RolePrivilegeMapper.java 。
为了后续更快速的创建Mapper.xml文件,我们也可以按照如下内容添加文件模版。
<?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=""> </mapper>
此处Mapper映射.xml文件的命名空间 namespace 的值需要配置成接口文件的全限定名称,例如 UserMapper接口对应的 cn.bjut.simple.mapper.UserMapper ,MyBatis内部就是通过这个值将接口和XML映射文件关联起来的。
准备好这几个XML映射文件后,还需要在1.3.2节中创建的 mybatis-config.xml配置文件中的 mappers元素中 配置所有的mapper文件路径。
<mappers> <mapper resource="cn/bjut/simple/mapper/CountryMapper.xml"/> <mapper resource="cn/bjut/simple/mapper/UserMapper.xml"/> <mapper resource="cn/bjut/simple/mapper/RoleMapper.xml"/> <mapper resource="cn/bjut/simple/mapper/PrivilegeMapper.xml"/> <mapper resource="cn/bjut/simple/mapper/UserRoleMapper.xml"/> <mapper resource="cn/bjut/simple/mapper/RolePrivilegeMapper.xml"/> </mappers>
使用这种配置方式,最明显的缺点就是,后续如果新增了Mapper.xml映射文件,仍然需要来此处修改mybatis-config文件,不好维护操作麻烦。因此我们修改成如下配置方式,配置一个包名。
<mappers> <package name="cn.bjut.simple.mapper"/> </mappers>
2.3 SELECT用法
查询单条数据
先写一个根据用户id查询用户信息的方法。在 UserMapper 接口中添加一个 selectById方法,代码如下。
/** * 通过id查询用户 * * @param id * @return */ SysUser selectById(Long id);
然后在对应的UserMapper.xml中添加如下的 <resultMap>和<select>部分的代码。
<?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="cn.bjut.simple.mapper.UserMapper"> <resultMap id="userMap" type="cn.bjut.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"> //XML中的select标签的id属性值 = “与之对应接口的方法名” SELECT * FROM sys_user WHERE id = #{id} </select> </mapper>
映射XML和接口的命名需要符合如下规则:
- 标签的id属性值在任何时候都不能出现英文句号“.”,并且同一个命名空间下不能出现重复的id。
- 接口的方法是可以重载的,所以接口中可以出现多个同名但参数不同的方法,但是XML中id的值不能重复,因而接口中的所有同名方法只会对应着XML中的同一个id的select方法。最常见的用法就是,同名方法中其中一个方法增加一个 RowBound 类型的参数用于实现分页查询。
XML 标签和属性的讲解:
- <select>:映射查询语句使用的标签。
- id:命名空间中的唯一标识符,可用来代表这条语句。
- resultMap:用于设置数据库返回值(列)的类型 和Java对象属性的映射关系。
- SELECT * FROM sys_user WHERE id = #{id}是查询SQL语句。
- #{id}:MyBatis SQL中使用预编译参数的一种方式,大括号中的id是传入的占位参数名。
在上面的 select 中,使用 resultMap 设置返回值的类型,这里的 userMap 就是上面 <resultMap> 中的 id 属性值,通过 id 引用需要的 <resultMap>。
resultMap 标签用于配置 Java 对象的属性和查询结果列的对应关系,通过 resultMap 中配置的 column 和 property 可以将查询列的值映射到 type 对象的属性上。
上面查询语句用到的resultMap包含的属性和标签讲解:
- id:必填,并且唯一。在select标签中,resultMap属性的值为此处id所设置的值。
- type:必填,用于配置查询列所映射到的Java实体类对象。
- extends: 选填,可以配置当前的 resultMap 继承自其他的 resultMap ,属性值为继承 resultMap 的 id。
- autoMapping: 选填(true/false),该配置可以覆盖全局的 autoMappingBehavior配置。
以上是 resultMap 的属性, resultMap 包含的所有标签如下。
constructor :配置使用构造方法注入结果,包含以下两个子标签。
- idArg:id 参数,标记结果作为 id(唯一值),可以帮助提高整体性能。
- arg:注入到构造方法的一个普通结果。
· id:一个 id 结果,标记结果作为 id(唯一值)。
· result:注入到 Java 对象属性的普通结果。
- association :一个复杂的类型关联,许多结果将包成这种类型。
- collection :复杂类型的集合。
- discriminator :根据结果值来决定使用哪个结果映射。
- case:基于某些值的结果映射。
接着看一下id和result标签包含的属性。
id: 一个id结果,标记结果作为id主键(或唯一值)的字段(可以有多个),它们的属性值是通过setter方法注入的。
result : 注入到JAVA对象属性的普通结果。
- column: 从数据库中得到的列名,或者列的别名。
- property:映射到列结果的属性。可以映射一些复杂对象中的属性,例如 “address.street.number” ,这会通过“.”方式的属性嵌套赋值。
- javaType: 一个Java类的完全限定名,或通过typeAlias配置的类型别名。如果映射到 HashMap,则需要明确地指定 javaType 属性。
- jdbcType:列对应的数据库类型。JDBC 类型仅仅需要对插入、更新、删除操作可能为空的列进行处理。
接口中定义的返回值类型必须和XML映射Mapper文件中配置的resultType类型一致。否则就会因为类型不一致而抛出异常。
返回值类型是由XML中的resultType(或resultMap中的type)决定的,不是由接口中写的返回值类型决定的。(本章讲XML方式,所以先忽略注解的情况)
查询返回多条数据:
在UserMapper接口中添加 selectAll方法,代码如下。
/** * 查询全部用户 * * @return */ List<SysUser> selectAll();
在对应的 UserMapper.xml 中添加如下的<select>部分的代码。
<select id="selectAll" resultType="cn.bjut.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>
在定义查询接口中方法的返回值时,必须注意查询SQL可能返回的结果数量。若执行的SQL返回多个结果时(>1),必须使用 List<SysUser>作为返回值。
selectById 设置结果映射使用了resultMap标签 中的id值。
selectAll 通过 resultType 直接指定了返回结果的类型。
如果使用 resultType 来设置返回结果的类型,需要在 SQL 中为所有列名和属性名不一致的列设置别名,通过设置别名使最终的查询结果列和 resultType 指定对象的属性名保持一致,进而实现自动映射。
因为数据库和 Java 中的这两种命名方式很常见,因此 MyBatis 还提供了一个全局属性 mapUnderscoreToCamelCase ,通过配置这个属性为 true 可以自动将以下画线方式命名的数据库列映射到 Java 对象的驼峰式命名属性中。
mapUnderscoreToCamelCase<settings> <!--下划线转驼峰--> <setting name="mapUnderscoreToCamelCase" value="true"/> <!--配置日志输出接口--> <setting name="logImpl" value="LOG4J"/> </settings>使用上述配置的时候,前面的 selectAll 可以改写如下。View Code<select id="selectAll" resultType="cn.bjut.simple.model.SysUser"> SELECT id, user_name , user_password , user_email , user_info , head_img , create_time FROM sys_user </select>
===============================================================================================================================
接下来通过测试用例来验证上面的两个查询。为了方便学习后面的大量测试,此处先根据第1章中的测试提取一个(后面测试类的父类)基础测试类 BaseMapperTest
/** * 基础测试类 */ 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(); } }
将原来的 CountryMapperTest 测试类修改如下。
public class CountryMapperTest2 extends BaseMapperTest { @Test public void testSelectAll(){ SqlSession sqlSession = this.getSqlSession(); //调用本类继承父类的成员方法 try { List<Country> countryList = sqlSession.selectList("cn.bjut.simple.mapper.ContryMapper.selectAll"); printCountryList(countryList); //调用本类的成员方法可以省略this关键字 } finally { sqlSession.close(); //不要忘记关闭释放sqlSession } } private void printCountryList(List<Country> countryList){ for(Country country : countryList){ System.out.printf("%-4d%4s%4s\n", country.getId(), country.getCountryname(), country.getCountrycode()); } } }
另外由于在 UserMapper 中添加了一个 selectAll 方法,因此 CountryMapperTest 中的 selectAll 方法不再唯一,调用时必须带上 namespace (命名空间)。
参考 CountryMapperTest 测试类,可以模仿着编写一个 UserMapperTest 测试类,代码如下。
@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); List<SysUser> userList = userMapper.selectAll(); //结果不为空 Assert.assertTrue(userList.size() >0 ); } finally { sqlSession.close(); //不要忘记关闭sqlSession } }
// 获取 UserMapper 接口的动态代理的实现类对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
与上面2个SELECT单表查询不同,在实际业务中还需要多表关联查询。下面举例一些更为复杂的用法。
第一种简单的情形:根据用户id获取用户拥有的所有角色,返回的结果为角色集合,结果只有角色的信息,不包含额外的其他字段信息。
这个方法会涉及 sys_user sys_role sys_user_role 这3个表,并且该方法写在上述任何一个对应的Mapper接口中都可以。
例如,我们把这个方法写到(位于src/main/java/cn.bjut.simple.mapper包中的) UserMapper接口中,代码如下。
/** * 根据用户id获取角色信息 * * @param userID * @return */ List<SysRole> selectRolesByUserId(Long userID);
在对应的 UserMapper.xml中添加如下代码。
<select id="selectRolesByUserId" resultType="cn.bjut.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>
虽然这个多表关联的查询中涉及了3个表,但是返回的结果只有sys_role一个表中的信息,所以直接使用 SysRole 作为返回值类型即可。
我们来编写代码对此方法进行测试。(src/main/test/java ......mapper包中 UserMapperTest增添)
@Test public void testSelectRolesByUserID(){ SqlSession sqlSession = getSqlSession(); try { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); List<SysRole> RoleList = userMapper.selectRolesByUserId(1L); Assert.assertNotNull(RoleList); //结果不为空 Assert.assertTrue(RoleList.size() >0 ); } finally { sqlSession.close(); //不要忘记关闭sqlSession } }
注:如果报错,很有可能是如下图片中的select语句中‘英文逗号 ,的位置和有无造成的。
如果我希望这个查询语句同时返回SysUser表的user_name字段呢,该如何设置resultType?请参考链接文章。(不考虑嵌套的情况)
我们设置一个需求(仅为了说明用法):以第一种情形为基础,假设查询的结果不仅要包含 sys_role 中的信息,还要包含当前用户的部分信息(不考虑嵌套的情况),例如增加查询列 u.user_ name as userName 。这时 resultType 该如何设置呢 ?
方法1:就是在 SysRole 对象中直接添加 userName 属性,这样仍然使用 SysRole 作为返回值。
方法2:新建扩展类(利用继承性),在扩展类中添加userName字段。
public class SysRoleExtend extends SysRole { private String userName; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } }
此时需要将映射文件中SELECT语句中的 resultType修改为:cn.bjut.example.model.SysRoleExtend。
这种方式比较适合需要少量额外字段的场景。如果需要其他表的大量字段,可以使用下面的方式3
方法3(推荐使用):新建扩展类,在扩展类中直接添加SysUser实体类的 成员变量属性字段。
package cn.bjut.example.model; public class SysRoleExtend extends SysRole {
//引用数据类型是另一个 实体类
private SysUser user; public SysUser getSysUser() { return sysUser; } public void setSysUser(SysUser sysUser) { this.sysUser = sysUser; } }
此时需要将resultType修改为:cn.bjut.example.model.SysRoleExtend。
在XML映射Mapper文件中
这里设置别名的时候,使用的是 user.属性名 ,user是SysRole中刚刚增加的属性 ,userName和userEmail是SysUser对象中的属性 ,通过这种方式可以直接将值赋给user字段中的属性。
@Test public void testSelectRolesByUserID2(){ SqlSession sqlSession = getSqlSession(); try { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); List<SysRoleExtend> RoleList = userMapper.selectRolesByUserId2(1L); Assert.assertNotNull(RoleList); //结果不为空 Assert.assertTrue(RoleList.size() >0 ); } finally { sqlSession.close(); //不要忘记关闭sqlSession } }
输出日志如下。
以上是两种简单方式的介绍,在本书后续章节中还会介绍通过 resultMap 处理这种嵌套对象的方式。
============================================================================
参考资料:
权限点用来管理要控制权限的资源
end