MyBatis 中 resultMap 的应用

本文内容次要参考了掘金上一位大佬 @清幽之地 的文章,原文介绍了不同场景下 resultMap 的应用,全面且具体。写本文的目标是为了理论入手学习并做一个记录,原文见 MyBatis 中弱小的 resultMap。

resultMap 简介

resultMap 的次要性能是将查问到的简单数据映射到一个后果集中。MyBatis 官网所述:

resultMap 元素是 MyBatis 中最重要最弱小的元素。它能够让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来,并在一些情景下容许你进行一些 JDBC 不反对的操作。实际上,在为一些比方连贯的简单语句编写映射代码的时候,一份 resultMap 可能代替实现等同性能的数千行代码。ResultMap 的设计思维是,对简略的语句做到零配置,对于简单一点的语句,只须要形容语句之间的关系就行了。

resultMap 实际

首先创立 SpringBoot 我的项目,配置 MyBatis,实现简略 CRUD,具体操作可参考我的另一篇博客SpringBoot 整合 MyBatis。

1. 字段映射

字段映射是 resultMap 最根底、最罕用的性能。本试验中,咱们创立实体类 User:

package com.example.entity;import lombok.Data;import java.util.Date;/** * @Author john * @Date 2021/11/14 */@Datapublic class User {    private long id;    private String userName;    private int age;    private String address;    private Date createTime;    private Date updateTime;}

user 表定义如下:

user 表中的数据如下:

mapper 文件内容如下:

<?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="com.example.mapper.UserMapper">    <sql id="insertFields">        user_name, age, address, gmt_create, gmt_modified    </sql>    <sql id="selectFields">        id, user_name, age, address, gmt_create, gmt_modified    </sql>    <resultMap id="UserMap" type="User">        <result column="gmt_create" jdbcType="DATE" property="createTime"/>        <result column="gmt_modified" jdbcType="DATE" property="updateTime"/>    </resultMap>    <select id="findUserById" parameterType="Long" resultMap="UserMap">        select        <include refid="selectFields"></include>        from user        where id = #{id}    </select>    <insert id="insertUser" parameterType="User" keyProperty="id">        insert into user (<include refid="insertFields"></include>)        values(#{userName}, #{age}, #{address}, UTC_TIMESTAMP(), UTC_TIMESTAMP())    </insert></mapper>

上述代码中,咱们应用 resultMap 定义了 'gmt_create' 与 'createTime' 以及 'gmt_modified' 与 'updateTime' 这两组变量之间的映射关系。

执行 findUserById() 办法能够查问到 User 的相干信息:

如果不配置 resultMap,而间接应用 resultType,那么后果集中将失落 createTime 和 updateTime 这两个属性的值。批改 mapper 文件,将 resultMap 正文掉,并将 select 标签中的 resultMap="UserMap" 替换为 resultType="User":

<!--    <resultMap id="UserMap" type="User">--><!--        <result column="gmt_create" jdbcType="DATE" property="createTime" />--><!--        <result column="gmt_modified" jdbcType="DATE" property="updateTime" />--><!--    </resultMap>-->    <select id="findUserById" parameterType="Long" resultType="User">        select        <include refid="selectFields"></include>        from user        where id = #{id}    </select>

执行 findUserById() 办法查问用户信息:

能够看到,'createTime' 与 'updateTime' 并没有被赋值,因为 MyBatis 会依据查问到的列名找到 POJO 对应的属性名,而后调用该属性的 setter 办法进行赋值,而 User 对象中并没有 'gmt_create' 和 'gmt_modified' 这两个属性,因而不会赋值。然而 'user_name' 为什么就赋值给了 'userName'?因为咱们开启了驼峰式命名映射,'user_name' 会主动映射到 'userName',详见博客SpringBoot 整合 MyBatis。。当然,如果将 User 类中的属性 'gmt_create' 和 'gmt_modified' 批改为 'gmtCreate' 和 'gmtModified',也是能够赋值胜利的。

另外还有一种办法能够赋值胜利,如果咱们将 SQL 语句批改为:

<select id="findUserById" parameterType="Long" resultType="User">    select    id, user_name, age, address, gmt_create as createTime, gmt_modified as updateTime    from user    where id = #{id}</select>

上述代码中,咱们为 'gmt_create' 和 'gmt_modified' 别离设置了别名 'createTime' 和 'updateTime',这样做也能够实现赋值。反过来,如果 SQL 语句为字段取了别名,如将上述 SQL 改为:'select id as uid, ...',则会导致 user 表中的 id 字段和 User 对象的 id 属性不再对应,这种状况也能够应用 resultMap 创立自定义的映射关系,将 'id' 和 'uid' 对应。

2. 构造方法

如果心愿将查问到的数据注入到构造方法中,那么能够应用 constructor 元素。

比方咱们为 User 类新增一个构造方法:

public User(String userName, Integer age) {    this.userName = userName;    this.age = age;}

mapper 文件中 resultMap 配置为:

<resultMap id="UserMap" type="User">    <constructor>        <arg column="user_name" name="userName" javaType="String"/>        <arg column="age" name="age" javaType="int"/>    </constructor>    <result column="gmt_create" jdbcType="DATE" property="createTime"/>    <result column="gmt_modified" jdbcType="DATE" property="updateTime"/></resultMap>

其中,column 是数据表的字段名或别名;name 是构造方法中的参数名;javaType 是参数的类型。

配置结束后调用 findUserById() 办法可胜利查问出 User。

constructor 元素在什么状况下比拟有用呢?首先,MyBatis 的赋值规定是先创立一个 User 对象,而后调用 setter 办法为属性赋值,而在 Java 中,有参构造方法会笼罩默认的无参构造方法。那么问题来了,如果咱们不设置 constructor,那么 MyBatis 在数据库中查到了 6 项数据(id、user_name...),而后创立 User 对象,因为咱们并没有显示地申明无参构造方法,所以 MyBatis 会调用有参构造方法,并依据有参结构的类型,从左往右进行匹配,行将查问到的 id 赋值给参数 userName,将 user_name 赋值给 age,因为类型不匹配,所以程序会报错。应用 constructor 后能够防止这个问题,当然也能够增加无参构造方法来解决。

待解决:上述有参构造方法中,如果参数 age 的类型设置为 int,程序会报错,设置为 Integer 后,失常运行,这是啥起因呢?麻烦理解的大佬帮忙解答。

3. 关联

通常一个 User 会负责一种角色,如管理员、版主等。在 User 类中可应用一个成员变量来示意:

@Datapublic class User {    //省略用户属性...        //角色信息    private Role role;}

Role 类定义为:

package com.example.entity;import lombok.Data;import java.util.Date;/** * @Author john * @Date 2021/11/19 */@Datapublic class Role {    private long id;    private String roleName;    private Date createTime;    private Date updateTime;}

role 表定义为:

role 表中的数据为:

用户-角色关联表定义如下:

用户-角色关联表中的数据如下:

查问 User 时,如果心愿查问出用户的角色信息,那么就不能应用 resultType="User",因为 User 类中只有一个 Role 对象,并没有 Role 对象里的属性(id 和 roleName)。这里咱们应用 association 来关联它们。

将 mapper 文件批改为:

<?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="com.example.mapper.UserMapper">    <resultMap id="UserMap" type="User">        <id column="id" jdbcType="INTEGER" property="id"/>        <result column="user_name" jdbcType="VARCHAR" property="userName"/>        <result column="age" jdbcType="INTEGER" property="age"/>        <result column="address" jdbcType="VARCHAR" property="address"/>        <result column="gmt_create" jdbcType="DATE" property="createTime"/>        <result column="gmt_modified" jdbcType="DATE" property="updateTime"/>        <association property="role" javaType="Role">            <id column="role_id" property="id"/>            <result column="role_name" property="roleName"/>        </association>    </resultMap>    <select id="findUserById" parameterType="Long" resultMap="UserMap">        select        u.id,        u.user_name,        u.age,        u.address,        u.gmt_create,        u.gmt_modified,        r.id as 'role_id',        r.role_name        from user as u        left join user_roles as ur on ur.user_id = u.id        left join role as r on ur.role_id = r.id        where u.id = #{id}    </select></mapper>

因为须要多表查问,所以要认真制订好每个字段的别名。还有一点须要阐明,resultMap 中的 id 元素(id column="id" jdbcType="INTEGER" property="id")是用来指定主键的,但也能够应用 result 来代替。

执行 findUserById() 办法查问 User:

胜利查问出了角色信息。

待解决:联结查问中,须要在 resultMap 中增加字段的映射关系,如 id、user_name 等(之前不增加也是能够赋值胜利的)。即便字段名和属性名完全一致/合乎驼峰命名规定,不增加则不会赋值,原理是什么?麻烦理解的大佬帮忙解答。

4. 汇合

4.1 汇合的嵌套后果映射

当一个 User 有多个角色时,如 'John' 的既负责 '管理员',又负责 '版主',这时咱们须要将 User 类中的角色属性的类型改成 List。

@Datapublic class User {    //省略用户属性...        //角色信息    private List<Role> roles;}

此时一个用户对应多个角色,所以就不是简略的 association,因为 association 解决的是有一个类型的关联。当有多个类型的关联,须要用到 collection 属性。

将 mapper 文件批改为:

<resultMap id="UserMap" type="User">    <id column="id" jdbcType="INTEGER" property="id"/>    <result column="user_name" jdbcType="VARCHAR" property="userName"/>    <result column="age" jdbcType="INTEGER" property="age"/>    <result column="address" jdbcType="VARCHAR" property="address"/>    <result column="gmt_create" jdbcType="DATE" property="createTime"/>    <result column="gmt_modified" jdbcType="DATE" property="updateTime"/>    <collection property="roles" ofType="Role">        <id column="role_id" property="id"/>        <result column="role_name" property="roleName"/>    </collection></resultMap>

用户-角色关联表中的数据批改为:

执行 findUserById() 办法查问 User:

数据太长所以分两次截图,能够看到查问出了 'John' 的两个身份信息。

4.2 汇合的嵌套 Select 查问

很多业务零碎都会有一个菜单表,比方博客零碎的 menu 表(局部)内容如下:

idnameparent_id
1文章0
1001所有文章1
1002写文章1
1003分类目录1
2用户0
2001个人资料2

上述菜单分为两级,第一级有 '文章' 和 '用户','文章' 下有 '所有文章'、'写文章'、'分类目录' 这三个二级菜单;'用户' 菜单下有一个二级菜单 '个人资料'。前端页面中,当咱们将鼠标挪动到 '文章' 上时,会显示该菜单下的所有二级菜单:

上面咱们演示如何查问出所有的菜单信息,并按层级展现。

定义 Menu 实体类:

package com.example.entity;import lombok.Data;import java.util.Date;import java.util.List;/** * @Author john * @Date 2021/11/20 */@Datapublic class Menu {    private long id;    private String name;    private long parentId;    private List<Menu> childMenus;    private Date createTime;    private Date updateTime;}

Menu 对象有一个属性 childMenus,该属性示意本菜单下的所有子菜单。

menu 表定义如下:

menu 表中的数据为:

MenuMapper 定义如下:

package com.example.mapper;import com.example.entity.Menu;import java.util.List;/** * @Author john * @Date 2021/11/20 */public interface MenuMapper {    List<Menu> getMenus();}

Mapper 接口中定义了一个办法,用来查问某个级别的菜单以及这些菜单的所有子菜单。

mapper 文件定义如下:

<?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="com.example.mapper.MenuMapper">    <resultMap id="menuMap" type="Menu">        <result column="gmt_create" property="createTime"/>        <result column="gmt_modified" property="updateTime"/>        <collection column="{parent_id=id}" property="childMenus" ofType="Menu" select="getMenus"/>    </resultMap>    <select id="getMenus" resultMap="menuMap">        SELECT        m.id,        m.name,        m.parent_id        FROM        menu m        where 1=1        <choose>            <when test="parent_id!=null">                and m.parent_id = #{parent_id}            </when>            <otherwise>                and m.parent_id = '0'            </otherwise>        </choose>    </select></mapper>

SQL 的含意是当 parent_id 为空时,查问所有的一级菜单,否则依据 parent_id 查问。通过 collection,咱们能够将所有的菜单信息查问进去,并按层级展现。getMenus() 办法执行后果如下:

[Menu(id=1, name=文章, parentId=0, childMenus=[Menu(id=1001, name=所有文章, parentId=1, childMenus=[], createTime=null, updateTime=null), Menu(id=1002, name=写文章, parentId=1, childMenus=[], createTime=null, updateTime=null), Menu(id=1003, name=分类目录, parentId=1, childMenus=[], createTime=null, updateTime=null)], createTime=null, updateTime=null), Menu(id=2, name=用户, parentId=0, childMenus=[Menu(id=2001, name=个人资料, parentId=2, childMenus=[], createTime=null, updateTime=null)], createTime=null, updateTime=null)]

collection 元素中:

  1. property="childMenus" 示意菜单中的子菜单列表;
  2. ofType="Menu" 示意返回数据的类型;
  3. select="getMenus" 指定 select 语句的 id;
  4. column="{parent_id=id}" 是参数的表达式。

这个 collection 整体的含意能够了解为:

通过 getMenus 这个 select 语句来获取一级菜单中的 childMenus 属性;在下面的 select 语句中,须要传递一个 parent_id 参数;这个参数的值就是一级菜单中的 id。

5. 主动填充关联对象

本节其实和 resultMap 的应用没有多少关系,大佬 @清幽之地 在这里给出了一种应用 resultType 代替 resultMap 的解决方案,也比拟有意思,所以咱们也实际一下。

MyBatis 解析返回值的具体过程:

  1. 获取返回值类型,拿到 Class 对象;
  2. 获取结构器,设置可拜访,而后调用构造方法并返回实例对象;
  3. 将对象包装成 MetaObject 对象;
  4. 从数据库中查问出数据;
  5. 调用 MetaObject.setValue(String name, Object value) 来填充对象。

步骤 5 中,MyBatis 会以 '.' 来分隔这个 name 属性。如果 name 属性中蕴含 '.' 符号,那么就以 '.' 作为分隔符,符号前的名称作为一个实体对象来解决,符号后的名称作为该对象的某个属性。如 name 为 'role.id',那么 'role' 会作为一个 Role 对象,而 'id' 会作为这个 Role 对象的属性。

以第 3 节中一个用户对应一个角色为例,User 类定义如下:

@Datapublic class User {    //省略用户属性...        //角色信息    private Role role;}

mapper 文件定义如下:

<?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="com.example.mapper.UserMapper">    <select id="findUserById" parameterType="Long" resultType="User">        select        u.id,        u.user_name,        u.age,        u.address,        u.gmt_create as createTime,        u.gmt_modified as updateTime,        r.id as 'role.id',        r.role_name as 'role.role_name'        from user as u        left join user_roles as ur on ur.user_id = u.id        left join role as r on ur.role_id = r.id        where u.id = #{id}    </select></mapper>

MyBatis 解析到 'role.id' 属性的时候,以 '.' 符号分隔之后发现,role 别名对应的是 Role 对象,则会先初始化 Role 对象(该 Role 对象就是 User 对象的属性),并将值赋予 id 属性。

执行 findUserById() 办法查问 User:

最初咱们也剖析一下 MetaObject.setValue(String name, Object value) 的源码:

public void setValue(String name, Object value) {    // 创立PropertyTokenizer对象, 依据分隔符.分隔name, 分隔为parent和children    PropertyTokenizer prop = new PropertyTokenizer(name);    // 如果.分隔符存在, 即children存在    if (prop.hasNext()) {        // 实例化parent对应的MetaObject, 如果parent为null, 则实例化为SystemMetaObject.NULL_META_OBJECT        MetaObject metaValue = this.metaObjectForProperty(prop.getIndexedName());        if (metaValue == SystemMetaObject.NULL_META_OBJECT) {            // 如果属性值为null, 则间接返回            if (value == null) {                return;            }            // 如果属性值不为null, 先实例化parent并从新结构MetaObject            metaValue = this.objectWrapper.instantiatePropertyValue(name, prop, this.objectFactory);        }        // 父属性处理完毕, 解决子属性(递归解决)        metaValue.setValue(prop.getChildren(), value);    } else {        // 不存在children, 间接赋值        this.objectWrapper.set(prop, value);    }}

另外举荐一个解说 PropertyTokenizer 的博客,见 MyBatis PropertyTokenizer。

欢送批评指正!!!