MyBatis 结果映射总结

前言

结果映射指的是将数据表中的字段与实体类中的属性关联起来,这样 MyBatis 就可以根据查询到的数据来填充实体对象的属性,帮助我们完成赋值操作。其实 MyBatis 的官方文档对映射规则的讲解还是非常清楚的,但考虑到自己马上就会成为一名 SQL Boy,以后免不了经常跟 SQL 打交道(公司使用的也是 MyBatis),所以希望用更加通俗的语言对官方文档所介绍的常用映射规则做一个总结,既为刚入门的同学提供一个参考,也方便自己以后查阅。本文会结合一些常见的应用场景,并通过简单的示例来介绍不同的映射方法。如有理解错误,还请大家批评指正!

简单字段映射

MyBatis 中的 resultType 和 resultMap 均支持结果映射,对于一些简单的映射操作,我们可以直接使用 resultType 来完成。但如果实体类中的属性为复杂类型,或者属性名和字段名无法对应,那么我们就需要使用 resultMap 来创建自定义的映射关系。下面用一个示例来演示 resultType 和 resultMap 的使用方法。

首先创建实体类 User:

@Data
public class User {

    private int id;

    private String userName;

    private int age;

    private String address;

    private Date createTime;

    private Date updateTime;
}

然后创建 user 表:

DROP TABLE IF EXISTS user;
CREATE TABLE user  (
  id int(11) NOT NULL AUTO_INCREMENT,
  user_name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  age int(11) DEFAULT NULL,
  address varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  gmt_create datetime(0) DEFAULT NULL,
  gmt_modified datetime(0) DEFAULT NULL,
  PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

接着向 user 表中插入数据:

MyBatis 结果映射总结

配置 MyBatis,启用别名和驼峰式命名映射:

#application.yaml
mybatis:
  mapper-locations: classpath:mapper/*  # 指定 mapper.xml 文件的路径,该文件用于编写 SQL 语句
  type-aliases-package: com.example.entity # 设置别名,它的作用是告诉 MyBatis 需要设置别名的实体类的所在的包。默认情况下,MyBatis 会使用实体类的非限定类名来作为它的别名,如将 com.example.entity.User 的别名设置为 User 或 user(别名不区分大小写)
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名自动映射,如将数据表中的字段 user_name 映射到实体对象的属性 userName

创建 mapper 文件,其内容如下:


    select
    id,
    user_name,
    age,
    address,
    gmt_create,
    gmt_modified
    from user
    where id = #{id}

上述代码中,我们使用 resultMap 来指定 SQL 语句的出参类型。默认情况下,如果数据表的字段名与实体类的属性名完全相同(如 id 对应 id),或者二者符合驼峰式命名映射的规则(如 user_name 对应 userName),那么 MyBatis 可以直接完成赋值操作。但 gmt_create 和 gmt_modified 不会映射为 createTime 和 updateTime,因此我们需要使用 resultMap 来创建新的映射关系。如果将 user 表的字段 gmt_create 和 gmt_modified 分别改为 create_time 和 update_time,那么就可以使用 resulType="User"resulType="user" 来替换 resultMap="UserMap"

调用 findUserById(参数 id 等于 1)查询用户信息,可得到如下结果:

{
    "address":"BUPT",
    "age":24,
    "createTime":1637164800000,
    "id":1,
    "updateTime":1637164800000,
    "userName":"John同学"
}

利用 constructor 指定构造方法

MyBatis 查询出数据后,会调用实体类的无参构造方法创建实体对象,然后为该对象的属性赋值。有时候我们会在实体类中重载多个构造方法,例如在不同的构造方法中执行不同的初始化操作,这种情况下我们希望 MyBatis 能够调用指定的构造方法来初始化对象。此外,如果实体类中仅有带参的构造方法,那么也需要通知 MyBatis 调用指定的构造方法。对于这两个问题,我们可以使用 MyBatis 提供的 constructor 元素来解决。

MyBatis 官方文档在介绍 constructor 时有提到, constructor 允许我们在始化对象时就为对象的属性赋值,这样可以不用暴露出公有方法。

首先在 User 类中添加带参的构造方法:

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

然后将 mapper 文件修改为:


    select
    id,
    user_name,
    age,
    address,
    gmt_create,
    gmt_modified
    from user
    where id = #{id}

注意, <arg></arg> 标签的定义顺序必须与构造方法中的参数顺序相同,因为 MyBatis 是根据 <constructor></constructor> 标签中的参数类型列表来匹配实体类的构造方法的,例如在本例中,匹配的构造方法为 User.<init>(java.lang.String, int)</init>。如果将 xml 文件中的两个 <arg></arg> 标签互换位置,那么 User 对象将不会被实例化成功,因为 User 类中并没有参数类型列表为 (int, java.lang.String) 的构造方法。如果我们不指定 javaType 属性的值,那么 MyBatis 默认将其置为 Object,此时构造方法中对应的参数类型也必须为 Object。

MyBatis 中的 _int 类型对应 Java 中的 int 类型,int 类型对应 Integer 类型。

经过上述配置,MyBatis 在实例化对象的时候就会调用我们指定的构造方法。另外,MyBatis 也支持跟据参数名称来匹配构造方法:


<arg></arg> 标签中的 name 属性用于设置构造方法的参数名称,如果我们设置了 name 的值,那么 MyBatis 会根据该属性匹配对应的构造方法,且 <arg></arg> 标签的位置可以随意放置。上述代码中,我们将两个 <arg></arg> 标签互换位置,然后调用 findUserById,仍然可以查询出用户的信息。

利用 association 关联一个复杂类型

博客系统中,每个用户都担任着某种角色,如普通用户、管理员、版主等。为了更好地描述用户信息,我们需要为 User 类添加一个 Role 类型的成员变量,记录当前用户所属的角色。但 Role 类型与 String、int 等类型不同,Role 对象本身也存储了一些特定的属性,如 id、roleName 等,默认情况下 MyBatis 无法为这些属性赋值。为了能够正确初始化 Role 变量,我们需要使用 association 元素将查询到的结果与 Role 对象的属性关联起来。

首先修改 User 类/创建 Role 类:


@Data
public class User {
    // 省略部分属性
    private Role role;
}

@Data
public class Role {

    private int id;

    private String roleName;

    private Date createTime;

    private Date updateTime;
}

然后创建 role 表(存储角色信息)和 user_roles 表(存储用户和角色的关联信息):

创建 role 表
DROP TABLE IF EXISTS role;
CREATE TABLE role  (
  id int(11) NOT NULL AUTO_INCREMENT,
  role_name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  gmt_create datetime(0) DEFAULT NULL,
  gmt_modified datetime(0) DEFAULT NULL,
  PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

创建 user_roles 表
DROP TABLE IF EXISTS user_roles;
CREATE TABLE user_roles  (
  id int(11) NOT NULL,
  user_id int(11) DEFAULT NULL,
  role_id int(11) DEFAULT NULL,
  gmt_create datetime(0) DEFAULT NULL,
  gmt_modified datetime(0) DEFAULT NULL,
  PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

接着向 role 表和 user_roles 表中插入数据:

MyBatis 结果映射总结

MyBatis 为我们提供了三种处理子对象(如 Role 对象)的方式,分别为 &#x5D4C;&#x5957;&#x7ED3;&#x679C;&#x6620;&#x5C04;&#x5D4C;&#x5957;&#x67E5;&#x8BE2;&#x5173;&#x8054;&#x591A;&#x4E2A;&#x7ED3;&#x679C;&#x96C6;

1. 嵌套结果映射

&#x5D4C;&#x5957;&#x7ED3;&#x679C;&#x6620;&#x5C04; 指的是在 resultMap 中嵌套一个映射关系,mapper 文件的内容如下:


    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}

上述代码中,我们将查询到的 role_id 和 role_name 分别映射到 Role 对象(User 对象的属性)的 id 和 roleName。

调用 findUserById 查询用户信息,可得到如下结果:

{
    "address":"BUPT",
    "age":24,
    "createTime":1637164800000,
    "id":1,
    "role":{
        "id":1,
        "roleName":"管理员"
    },
    "updateTime":1637164800000,
    "userName":"John同学"
}

我们也可以将 association 中的映射关系独立出来,改写为如下形式,方便复用:


2. 嵌套查询

&#x5D4C;&#x5957;&#x67E5;&#x8BE2; 指的是在 resultMap 中嵌套一个查询语句,mapper 文件的内容如下:


        select
        r.id,
        r.role_name
        from user_roles as ur
        left join role as r on ur.role_id = r.id
        where ur.user_id = #{user_id}

        select
        id,
        user_name,
        age,
        address,
        gmt_create,
        gmt_modified
        from user
        where id = #{id}

resultMap 中嵌套了一个子查询 selectUserRole,MyBatis 首先从 user 表中查询出 id、user_name 等信息,然后将 user_id 作为参数传递给 selectUserRoleselectUserRole 负责从 role 表和 user_roles 表中查询出当前用户的角色信息。 column="{user_id=id}" 指的是将查询到的 id 赋值给变量 user_id,然后将 user_id 作为子查询的入参(如果直接将 id 作为入参,那么 User 对象的 id 属性将不会被赋值),如果需要传入多个参数,那么可以使用一个复合属性,如 column="{param1=value1, param2=value2}"。注意,嵌套子查询时,子查询中的 parameterType 必须设置为 Map 或省略不写。

3. 关联多个结果集

&#x5173;&#x8054;&#x591A;&#x4E2A;&#x7ED3;&#x679C;&#x96C6; 指的是一次性执行多个查询语句,并得到多个结果集,然后利用某个结果集的数据来填充对象的属性。

首先在 MySQL 数据库中创建存储过程 findUserAndRole:

-- 将结束标志符更改为 $$
delimiter $$
create procedure findUserAndRole(in user_id int)
begin
    select
    id,
    user_name,
    age,
    address,
    gmt_create,
    gmt_modified
    from user
    where id = user_id;

    select
    r.id as role_id,
    r.role_name as role_name,
    ur.user_id as user_id
    from user_roles as ur
    left join role as r
    on ur.role_id = r.id;
end $$
-- 将结束标志符改回 ;
delimiter ;

然后修改 mapper 文件的内容:


        {call findUserAndRole(#{user_id,jdbcType=INTEGER,mode=IN})}

解释一下上述操作的含义,我们在存储过程 findUserAndRole 中定义了两条 SQL 语句,第一条的执行逻辑是利用 user_id 从 user 表中查询出当前用户的 id,user_name 等信息;第二条的执行逻辑是利用关联查询从 role 表和 user_roles 表中查询出 user_id、role_id 以及 role_name 等信息。我们将两次查询得到的结果集分别表示为 user 和 role,即 resultSets="user,role",然后通过 association 将结果集 role 中的 role_id 和 role_name 分别映射到 Role 对象的 id 和 roleName 属性。 column="id" foreignColumn="user_id" 用于关联两个结果集中的数据,因为结果集 role 中包含了所有用户的角色信息(虽然本例中我们只设置了一个用户,但实际上结果集 role 中包含着所有用户的信息),因此在进行属性填充之前,我们需要指明利用哪一个角色信息进行属性填充, column="id" foreignColumn="user_id" 的作用就是从结果集 role 中筛选出 user_id 为 id 的角色信息。

resultSets 中不同的结果集之间用逗号分隔,中间千万不能加空格!

利用 collection 关联多个复杂类型

上文中我们分析了一个用户担任一种角色的情况,然而在实际开发中,每个用户都有可能同时担任多种角色,例如 “John同学” 既可以是管理员,又可以是版主。此时使用 association 无法正确查询出用户的角色信息,因为 association 处理的是一对一的映射关系。当需要关联多个对象时,我们需要使用 collection 元素。

首先修改实体类:

@Data
public class User {
    // 省略部分属性
    private List roles;
}

然后在 user_roles 表中插入一条记录:

MyBatis 结果映射总结

collection 的使用方法和 association 非常相似,在上文中介绍的三种方法中,我们只需要做一些简单的修改,就可以查询出用户的所有角色信息。

1. 嵌套结果映射

mapper 文件的内容如下:


    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}

与上文中使用 association 嵌套结果映射的区别在于,我们将 javaType 替换为了 ofType,以此来指定 Java 集合中的泛型类型。

调用 findUserById 查询用户信息,可得到如下结果:

{
    "address":"BUPT",
    "age":24,
    "createTime":1637164800000,
    "id":1,
    "roles":[
        {
            "id":1,
            "roleName":"管理员"
        },
        {
            "id":2,
            "roleName":"版主"
        }
    ],
    "updateTime":1637164800000,
    "userName":"John同学"
}

2. 嵌套查询

mapper 文件的内容如下:


        select
        r.id,
        r.role_name
        from user_roles as ur
        left join role as r on ur.role_id = r.id
        where ur.user_id = #{user_id}

        select
        id,
        user_name,
        age,
        address,
        gmt_create,
        gmt_modified
        from user
        where id = #{id}

同样地,我们将 javaType 改为 ofType。

3. 关联多个结果集

mapper 文件的内容如下:


        {call findUserAndRole(#{user_id,jdbcType=INTEGER,mode=IN})}

同理,存储过程中的执行逻辑保持不变,只需将 javaType 改为 ofType。

改用 collection 后,还要注意将 property 由 role 改为 roles。当然,这个名称可自由定义。

查询具有树形结构的数据

树形结构数据在实际开发中非常常见,比较典型的就是菜单表,每个父菜单都可能包含一个或多个子菜单,而每个子菜单也可能包含孙子菜单。有时候我们希望查询出某个菜单下的所有子菜单,并分级展示,这种情况应该如何处理呢?其实上文中介绍的三种方法均支持多级结果映射,我们只需要在 mapper 文件中做一些简单的处理。

首先创建 Menu 类:

@Data
public class Menu {

    private long id;

    private String name;

    private long parentId;

    private List childMenus;

   private Date createTime;

   private Date updateTime;
}

然后创建 menu 表:

DROP TABLE IF EXISTS menu;
CREATE TABLE menu  (
  id int(11) NOT NULL,
  name varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
  parent_id int(11) DEFAULT NULL,
  gmt_create datetime(0) DEFAULT NULL,
  gmt_modified datetime(0) DEFAULT NULL,
  PRIMARY KEY (id) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

接着向 menu 表中插入数据:

MyBatis 结果映射总结

为了更直观地展示各层级菜单之间的关系,我们将数据整理在下面的表格中:

id

name

parent_id

1 文章 0 11 所有文章 1 12 写文章 1 121 载入草稿 12 2 用户 0 21 个人资料 2 3 附件 0

可以看到,菜单表总共有三个层级(不包含第 0 级),第一级的 “所有文章” 下有子菜单 “写文章”,第二级的 “写文章” 下有子菜单 “载入草稿”。每个层级的菜单都可能有零个、一个或多个子菜单,为了将所有的菜单查询出来,我们既要修改 SQL 语句,又要修改 resultMap 中的映射关系,下面介绍三种查询方式。

1. 嵌套结果映射

mapper 文件的内容如下:


    select
    m1.id as id,
    m1.name as name,
    m1.parent_id as parent_id,
    m2.id as id2,
    m2.name as name2,
    m2.parent_id as parent_id2,
    m3.id as id3,
    m3.name as name3,
    m3.parent_id as parent_id3
    from
    menu as m1
    left join menu as m2 on m1.id = m2.parent_id
    left join menu as m3 on m2.id = m3.parent_id
    where m1.parent_id = #{menu_id}

因为菜单表中最多有三个层级,所以我们在 SQL 语句中使用了三表联查,分别从表 m1、m2、m3(均为 menu 表)中查询出各个级别(从上到下)的菜单,然后在 collection 中新增一个嵌套,表 m2 和表 m3 中查出的数据均用于填充前一级别的 childMenus 属性。

调用 findMenus(参数 menu_id 等于 0)查询菜单信息,可得到如下结果:

[
    {
        "childMenus":[
            {
                "childMenus":[
                    {
                        "id":121,
                        "name":"载入草稿",
                        "parentId":12
                    }
                ],
                "id":12,
                "name":"写文章",
                "parentId":1
            },
            {
                "childMenus":[

                ],
                "id":11,
                "name":"所有文章",
                "parentId":1
            }
        ],
        "id":1,
        "name":"文章",
        "parentId":0
    },
    {
        "childMenus":[
            {
                "childMenus":[

                ],
                "id":21,
                "name":"个人资料",
                "parentId":2
            }
        ],
        "id":2,
        "name":"用户",
        "parentId":0
    },
    {
        "childMenus":[

        ],
        "id":3,
        "name":"附件",
        "parentId":0
    }
]

注意, &#x5D4C;&#x5957;&#x7ED3;&#x679C;&#x6620;&#x5C04; 的方式不具备通用性,因为菜单表的结构可能不止三层。如果有多个层级的菜单,那么我们就需要继续修改 SQL 语句并新增嵌套。

2. 嵌套查询

mapper 文件的内容如下:


    select
    id,
    name,
    parent_id
    from
    menu
    where parent_id = #{menu_id}

上述代码中,我们将嵌套的子查询设置为 findMenus 本身,MyBatis 首先调用 findMenus 查询出 parent_id 为 menu_id 的菜单,然后将查询出的菜单的 id 赋值给 menu_id,继续调用 findMenus 查询出下一层级的菜单。此种方式可以递归的查询出所有菜单,无论菜单表有多少个层级。

3. 关联多个结果集

首先创建存储过程 findMenu:

delimiter $$
create procedure findMenu(in menu_id int)
begin
    select
    id as id1,
    name as name1,
    parent_id as parent_id1
    from menu
    where parent_id = menu_id;

    select
    id as id2,
    name as name2,
    parent_id as parent_id2
    from menu;

    select
    id as id3,
    name as name3,
    parent_id as parent_id3
    from menu;

end $$
-- 将结束标志符改回 ;
delimiter ;

然后将 mapper 文件的内容修改为:


        {call findMenu(#{menu_id,jdbcType=INTEGER,mode=IN})}

findMenu 中定义了三条 SQL 语句,第一条的执行逻辑是从 menu 表中查询出 parent_id 为 menu_id 的菜单,其它两条的执行逻辑是从 menu 表中查询出所有的菜单。我们将三条查询返回的结果集分别表示为 menu1、menu2 和 menu3,然后利用 menu2 和 menu3 中的数据分别填充子菜单和孙子菜单的属性。

&#x5173;&#x8054;&#x591A;&#x4E2A;&#x7ED3;&#x679C;&#x96C6;&#x5D4C;&#x5957;&#x7ED3;&#x679C;&#x6620;&#x5C04; 一样,在查询树形结构数据时不具备通用性。若菜单表的层级大于 3,那么我们就需要修改存储过程和映射关系。

参考资料

MyBatis 官方文档 Mybatis 中强大的 resultMap

Original: https://www.cnblogs.com/johnlearning/p/16295408.html
Author: John同学
Title: MyBatis 结果映射总结

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/592864/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球