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 用法。

image 2024 05 21 15 46 33 994
Figure 1. 图2-2 roleList状态