select用法
在权限系统中有几个常见的业务,我们需要查询出系统中的用户、角色、权限等数据。在使用纯粹的 JDBC 时,需要写查询语句,并且对结果集进行手动处理,将结果映射到对象的属性中。使用 MyBatis 时,只需要在 XML 中添加一个 select 元素,写一个 SQL,再做一些简单的配置,就可以将查询的结果直接映射到对象中。
先写一个根据用户 id 查询用户信息的简单方法。在 UserMapper 接口中添加一个 selectById 方法,代码如下。
package cn.liaozh.mybatis2.ch2.xml.mapper;
import cn.liaozh.mybatis2.ch2.xml.model.SysUser;
public interface UserMapper {
/**
* 通过id查询用户
* @param id 用户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.liaozh.mybatis2.ch2.xml.mapper.UserMapper">
<resultMap id="baseResultMap" type="cn.liaozh.mybatis2.ch2.xml.model.SysUser">
<id column="id" property="id"/>
<result column="user_name" property="userName"/>
<result column="user_password" property="userPassword"/>
<result column="user_email" property="userEmail"/>
<result column="user_info" property="userInfo"/>
<result column="head_img" property="headImg" jdbcType="BLOB"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
</resultMap>
<select id="selectById" resultMap="baseResultMap">
SELECT *
FROM sys_user
WHERE id = #{id}
</select>
</mapper>
前面创建接口和 XML 时提到过,接口和 XML 是通过将 namespace 的值设置为接口的全限定名称来进行关联的,那么接口中方法和 XML 又是怎么关联的呢?
可以发现,XML 中的 select 标签的 id 属性值和定义的接口方法名是一样的。MyBatis 就是通过这种方式将接口方法和 XML 中定义的 SQL 语句关联到一起的,如果接口方法没有和 XML 中的 id 属性值相对应,启动程序便会报错。映射 XML 和接口的命名需要符合如下规则。
-
当只使用 XML 而不使用接口的时候,namespace 的值可以设置为任意不重复的名称。
-
标签的 id 属性值在任何时候都不能出现英文句号 “.”,并且同一个命名空间下不能出现重复的 id。
-
因为接口方法是可以重载的,所以接口中可以出现多个同名但参数不同的方法,但是 XML 中 id 的值不能重复,因而接口中的所有同名方法会对应着 XML 中的同一个 id 的方法。最常见的用法就是,同名方法中其中一个方法增加一个 RowBound 类型的参数用于实现分页查询。
明白上述两者之间的关系后,通过 UserMapper.xml 先来了解一下 XML 中一些标签和属性的作用。
-
<select>
:映射查询语句使用的标签。 -
id
:命名空间中的唯一标识符,可用来代表这条语句。 -
resultMap
:用于设置返回值的类型和映射关系。 -
select
标签中的select * from sys_user where id=#{id}
是查询语句。 -
#{id}
:MyBatis SQL 中使用预编译参数的一种方式,大括号中的 id 是传入的参数名。
在上面的 select 中,使用 resultMap 设置返回值的类型,这里的 userMap 就是上面 <resultMap> 中的 id 属性值,通过 id 引用需要的 <resultMap>。
resultMap 标签用于配置 Java 对象的属性和查询结果列的对应关系,通过 resultMap 中配置的 column 和 property 可以将查询列的值映射到 type 对象的属性上,因此当我们使用 select * 查询所有列的时候,MyBatis 也可以将结果正确地映射到 SysUser 对象上。
resultMap 是一种很重要的配置结果映射的方法,我们必须熟练掌握 resultMap 的配置方法。resultMap 包含的所有属性如下。
-
id:必填,并且唯一。在 select 标签中,resultMap 指定的值即为此处 id 所设置的值。
-
type:必填,用于配置查询列所映射到的 Java 对象类型。
-
extends:选填,可以配置当前的 resultMap 继承自其他的 resultMap,属性值为继承 resultMap 的 id。
-
autoMapping:选填,可选值为 true 或 false,用于配置是否启用非映射字段(没有在 resultMap 中配置的字段)的自动映射功能,该配置可以覆盖全局的 autoMappingBehavior 配置。
以上是 resultMap 的属性,resultMap 包含的所有标签如下。
-
constructor:配置使用构造方法注入结果,包含以下两个子标签。
-
idArg:id 参数,标记结果作为 id(唯一值),可以帮助提高整体性能。
-
arg:注入到构造方法的一个普通结果。
-
-
id:一个 id 结果,标记结果作为 id(唯一值),可以帮助提高整体性能。
-
result:注入到 Java 对象属性的普通结果。
-
association:一个复杂的类型关联,许多结果将包成这种类型。
-
collection:复杂类型的集合。
-
discriminator:根据结果值来决定使用哪个结果映射。
-
case:基于某些值的结果映射。
本章中会介绍常用标签 constructor、id、result。而 association、collection 和 discriminator 标签会在后面的章节中讲解。
首先来了解一下这些标签属性之间的关系。
-
constructor:通过构造方法注入属性的结果值。构造方法中的 idArg、arg 参数分别对应着 resultMap 中的 id、result 标签,它们的含义相同,只是注入方式不同。
-
resultMap 中的 id 和 result 标签包含的属性相同,不同的地方在于,id 代表的是主键(或唯一值)的字段(可以有多个),它们的属性值是通过 setter 方法注入的。
接着来看一下 id 和 result 标签包含的属性。
-
column:从数据库中得到的列名,或者是列的别名。
-
property:映射到列结果的属性。可以映射简单的如 “username” 这样的属性,也可以映射一些复杂对象中的属性,例如 “address.street.number”,这会通过 “.” 方式的属性嵌套赋值。
-
javaType:一个 Java 类的完全限定名,或一个类型别名(通过 typeAlias 配置或者默认的类型)。如果映射到一个 JavaBean,MyBatis 通常可以自动判断属性的类型。如果映射到 HashMap,则需要明确地指定 javaType 属性。
-
jdbcType:列对应的数据库类型。JDBC 类型仅仅需要对插入、更新、删除操作可能为空的列进行处理。这是 JDBC jdbcType 的需要,而不是 MyBatis 的需要。
-
typeHandler:使用这个属性可以覆盖默认的类型处理器。这个属性值是类的完全限定名或类型别名。
下面来看一下接口方法的返回值要如何定义。
接口中定义的返回值类型必须和 XML 中配置的 resultType 类型一致,否则就会因为类型不一致而抛出异常。返回值类型是由 XML 中的 resultType(或 resultMap 中的 type)决定的,不是由接口中写的返回值类型决定的(本章主要讲 XML 方式,所以先忽略注解的情况)。
UserMapper 接口中的 selectById 方法,通过主键 id 查询,最多会有一条记录,所以这里定义的返回值为 SysUser。在讲解这个方法时,为了方便初学者理解,我们给出了完整的代码。之后再添加方法时,不会列出完整代码,只会像下面这样给出新增的代码。
在 UserMapper 接口中添加 selectAll 方法,代码如下。
/**
* 获取全部用户
* @return 用户列表
*/
List<SysUser> selectAll();
在对应的 UserMapper.xml 中添加如下的 <select> 部分的代码。
<select id="selectAll" resultType="tk.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>
这个接口中对应方法的返回值类型为 List<SysUser>,为什么不是 SysUser 呢?
在定义接口中方法的返回值时,必须注意查询 SQL 可能返回的结果数量。
当返回值最多只有 1 个结果的时候(可以 0 个),可以将接口返回值定义为 SysUser,而不是 List<SysUser>。当然,如果将返回值改为 List<SysUser> 或 SysUser[],也没有问题,只是不建议这么做。当执行的 SQL 返回多个结果时,必须使用 List<SysUser> 或 SysUser[] 作为返回值,如果使用 SysUser,就会抛出 TooManyResultsException 异常。
观察一下 UserMapper.xml 中 selectById 和 selectAll 的区别:selectById 中使用了 resultMap 来设置结果映射,而 selectAll 中则通过 resultType 直接指定了返回结果的类型。可以发现,如果使用 resultType 来设置返回结果的类型,需要在 SQL 中为所有列名和属性名不一致的列设置别名,通过设置别名使最终的查询结果列和 resultType 指定对象的属性名保持一致,进而实现自动映射。
名称映射规则
可以通过在 resultMap 中配置 property 属性和 column 属性的映射,或者在 SQL 中设置别名这两种方式实现将查询列映射到对象属性的目的。
property 属性或别名要和对象中属性的名字相同,但是实际匹配时,MyBatis 会先将两者都转换为大写形式,然后再判断是否相同,即 property="userName" 和 property="username" 都可以匹配到对象的userName 属性上。判断是否相同的时候要使用 USERNAME,因此在设置 property 属性或别名的时候,不需要考虑大小写是否一致,但是为了便于阅读,要尽可能按照统一的规则来设置。
在数据库中,由于大多数数据库设置不区分大小写,因此下画线方式的命名很常见,如 user_name、user_email。在 Java 中,一般都使用驼峰式命名,如 userName、userEmail。因为数据库和 Java 中的这两种命名方式很常见,因此 MyBatis 还提供了一个全局属性 mapUnderscoreToCamelCase,通过配置这个属性为 true 可以自动将以下画线方式命名的数据库列映射到 Java 对象的驼峰式命名属性中。这个属性默认为 false,如果想要使用该功能,需要在 MyBatis 的配置文件(第 1 章中的 mybatis-config.xml 文件)中增加如下配置。
<settings>
<!--开启自动字段下划线转驼峰命名-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
使用上述配置的时候,前面的 selectAll 可以改写如下。
<select id="selectAll" resultType="tk.mybatis.simple.model.SysUser">
select id,
user_name,
user_password,
user_email,
user_info,
head_img,
create_time
from sys_user
</select>
还可以将 SQL 简单写为 select * from sys_user
,但是考虑到性能,通常都会指定查询列,很少使用 *
代替所有列。
了解上面这些要点之后,基本的查询就难不倒我们了。接下来通过测试用例来验证上面的两个查询。在学习过程中,建议大家在有不同想法的时候,多写一些例子来验证自己的问题或猜测。
为了方便大家学习后面的大量测试,此处先根据第 1 章中的测试提取一个基础测试类 BaseMapperTest。
/**
* 基础测试类
*/
public class BaseMapperTest {
private static SqlSessionFactory sqlSessionFactory;
@BeforeClass
public static void init() {
try {
Reader reader = Resources.getResourceAsReader("mybatis.cfg.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
public SqlSession getSqlSession() {
return sqlSessionFactory.openSession();
}
}
将原来的 CountryMapperTest 测试类修改如下。
public class CountryMapperTest extends BaseMapperTest{
@Test
public void testQueryAll() {
SqlSession sqlSession = getSqlSession();
List<Country> countryList = sqlSession.selectList("listAll");
this.printCountryList(countryList);
sqlSession.close();
}
private void printCountryList(List<Country> countryList) {
for (Country country : countryList) {
System.out.printf("%-4d%4s%4s\n",country.getId(),country.getCountryname(),country.getCountrycode());
}
}
}
修改后的测试类继承了 BaseMapperTest,通过调用 getSqlSession() 方法获取一个 SqlSession 对象,另外由于在 UserMapper 中添加了一个 selectAll 方法,因此 CountryMapperTest 中的 selectAll 方法不再唯一,调用时必须带上 namespace(命名空间),因此这里要改为 tk.mybatis.simple.mapper.CountryMapper.selectAll。修改后,运行该测试类保证其可以正常运行。
参考 CountryMapperTest 测试类,可以模仿着编写一个 UserMapperTest 测试类,代码如下。
public class UserMapperTest extends BaseMapperTest{
@Test
public void testSelectById(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
SysUser sysUser = userMapper.selectById(1L);
Assert.assertNotNull(sysUser);
Assert.assertEquals("admin",sysUser.getUserName());
}finally {
sqlSession.close();
}
}
@Test
public void testSelectAll(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<SysUser> userList = userMapper.selectAll();
Assert.assertNotNull(userList);
Assert.assertTrue(userList.size() > 0);
}finally {
sqlSession.close();
}
}
}
右键单击该类,在 Run As 选项中选择 JUnit Test 执行测试,测试通过,输出如下日志。
通过日志输出的结果可以验证之前的代码。
上面两个 SELECT 查询仅仅是简单的单表查询,这里列举的是两种常见的情况,在实际业务中还需要多表关联查询,关联查询结果的类型也会有多种情况,下面来列举一些复杂的用法。
第一种简单的情形:根据用户 id 获取用户拥有的所有角色,返回的结果为角色集合,结果只有角色的信息,不包含额外的其他字段信息。这个方法会涉及 sys_user、sys_role 和 sys_user_role 这 3 个表,并且该方法写在任何一个对应的 Mapper 接口中都可以。将这个方法写到 UserMapper 中,代码如下。
/**
* 根据用户ID获取角色信息
* @param userId 用户ID
* @return 角色列表
*/
List<SysRole> selectRolesByUserId(Long userId);
在对应的 UserMapper.xml 中添加如下代码。
<select id="selectRolesByUserId" resultType="cn.liaozh.mybatis2.ch2.xml.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 作为返回值类型即可。大家可以参照之前的测试示例编写代码对此方法进行测试。
为了说明第二种情形,我们设置一个需求(仅为了说明用法):以第一种情形为基础,假设查询的结果不仅要包含 sys_role 中的信息,还要包含当前用户的部分信息(不考虑嵌套的情况),例如增加查询列 u.user_name as userName。这时 resultType 该如何设置呢?
先介绍两种简单的方法,第一种方法就是在 SysRole 对象中直接添加 userName 属性,这样仍然使用 SysRole 作为返回值,或者也可以创建一个如下所示的对象。
public class SysRoleExtend extends SysRole {
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
将 resultType 设置为扩展属性后的 SysRoleExtend 对象,通过这种方式来接收多余的值。这种方式比较适合在需要少量额外字段时使用,但是如果需要其他表中大量列的值时,这种方式就不适用了,因为我们不能将一个类的属性都照搬到另一个类中。
针对这种情况,在不考虑嵌套 XML 配置(第 6 章会详细讲解)的情况下,可以使用第二种方法,代码如下。
/**
* 角色表
*/
public class SysRole {
// 其它原有字段...
/**
* 用户信息
*/
private SysUser user;
}
直接在 SysRole 中增加 SysUser 对象,字段名为 user,增加这个字段后,修改 XML 中的 selectRolesByUserId 方法。
<select id="selectRolesByUserId" resultType="cn.liaozh.mybatis2.ch2.xml.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>
注意看查询列增加的两行,如下所示。
u.user_name as "user.userName",
u.user_email as "user.userEmail"
这里在设置别名的时候,使用的是 “user.属性名”,user 是 SysRole 中刚刚增加的属性,userName 和 userEmail 是 SysUser 对象中的属性,通过这种方式可以直接将值赋给 user 字段中的属性。
在 UserMapperTest 中执行如下测试代码。
@Test
public void testSelectRolesByUserId(){
SqlSession sqlSession = getSqlSession();
try {
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 调用 selectRolesByUserId 方法查询用户的角色
List<SysRole> roleList = userMapper.selectRolesByUserId(1L);
// 结果不为空
Assert.assertNotNull(roleList);
// 角色数量大于 0 个
Assert.assertTrue(roleList.size() > 0);
}finally {
// 不要忘记关闭 sqlSession
sqlSession.close();
}
}
输出日志如下。
从输出日志中可以很明显看到增加的两列,但是看不到对象中的效果。在 Assert.assertNotNull(roleList);这一行设置断点,可以看到当前实例的状态如图2-2所示。
以上是两种简单方式的介绍,在本书后续章节中还会介绍通过 resultMap 处理这种嵌套对象的方式。
关于 select 方法的基本内容就先介绍这么多,下面来看 insert 用法。
