JAVA

Mybatis中的resultMap讲解

前言

使用mybatis,有两个属性标签<resultType><resultMap>可以提供结果映射。

虽然resultType 属性在大部分情况下都够用,但是在一些特殊情况下无能为力,比如属性名和列名不一致,为一些连接的复杂语句编写映射代码。

遇到这些情况,我们要使用<resultMap>标签,一份 <resultMap> 能够代替实现同等功能的数千行代码。

resultMap 是 Mybatis 最强大的元素之一,它可以将查询到的复杂数据(比如查询到几个表中数据)映射到一个结果集当中。如在实际应用中,有一个表为(用户角色表),通过查询用户表信息展示页面,在(用户表)中存在用户角色表 id ,在实际列表页的展示中,用户关注的是用户角色名称,而不是角色 id。解决此类问题可以通过 resultMap 来映射自定义结果。 使用 resultMap 做自定义结果映射,字段名可以不一致,并且还可以指定要显示的列,比较灵活,应用也广泛(推荐使用)。

resultMap 元素是 MyBatis 中最重要最强大的元素。
resultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

resultMap 知识点

resultMap 元素用来描述如何将结果集映射到 Java 对象,使用 resultMap 对列表展示所需的必要字段来进行自动映射,特别是当数据库的字段名和实体类 POJO 中的属性名不一致的情况下,比如角色名称,字段名/列名 column 是 roleName,而 User 对象的属性名则为 userRoleName ,此时就需要做映射。

resultMap 元素的属性值和子节点

  • id 属性:唯一标识,此 id 值用于 select 元素 resultMap 属性的引用。
  • type 属性:表示该 resultMap 的映射结果类型。
  • result 子节点:用于标识一些简单属性,其中 column 属性表示从数据库中查询的字段名或别名, property 属性则表示查询出来的字段对应的值赋给实体对象的哪个属性。

说明:MyBatis 中在对查询进行 select 映射的时候,返回类型可以用 resultType 也可以用 resultMap ,resultType和 resultMap 有一定关联和区别,应用场景也不同。

resultType 和 resultMap 区别

resultType:直接表示返回类型,包括基础数据类型和复杂数据类型。
resultMap 则是对外部 resultMap 定义的引用,对应外部 resultMap 的 id,表示返回结果映射到哪一个 resultMap 上。它的应用场景一般是:数据库字段信息与对象属性不一致或者需要做复杂的联合查询以便自由控制映射结果。

说明:注意,MyBatis 中使用 resultType 做自动映射,一定要注意:字段名和 JavaBean 的属性名必须一致。若不一致,则需要给字段起别名,保证别名与属性名一致。

resultType 和 resultMap 关联

在 MyBatis 进行查询映射的时候,其实查询出来的每个字段值都放在一个对应的 Map 里面,其中键是字段名,值则是其对应的值。当 select 元素提供的返回类型属性是 resultType 的时候, MyBatis 会将 Map 里面的键值对取出赋给 resultType 所指定的对象对应的属性(即调用对应的对象里的属性的 setter 方法进行填充)。正因为如此,当使用 resultType 的时候,直接在后台就能接收到其相应的对象属性值。由此可看出,其实 MyBatis 的每个查询映射的返回类型都是 resultMap ,只是当我们提供的返回类型属性是 resultType 的时候, MyBatis 会自动把对应的值赋给 resultType 所指定对象的属性; 而当我们提供的返回类型是 resultMap 的时候,因为 Map 不能很好地表示领域模型,我们就需要通过进一步的定义把它转化为对应的实体对象。
  当返回类型是 resultMap 时,也是非常有用的,这主要用在进行复杂联合查询上,当然在进行简单查询时是没有什么必要的,使用 resultType 足以。

  • Mybatis 是对返回的结果的每一行做映射的,因此在指定 resultType 或者 resultMap 返回类型时应特别注意是一行的类型而不是所有。
  • MyBatis 中在查询进行 select 映射的时候,返回类型可以用 resultType,也可以用 resultMap,resultType 是直接表示返回类型的,而 resultMap 则是对外部 resultMap 的引用,在 MyBatis 的 select 元素中,resultType 和 resultMap 本质上是一样的,都是 Map 数据结构。但需要明确一点: resultType 属性和 resultMap 属性绝对不能同时存在,只能二者选 其一使用。
  • 在 MyBatis 中,使用 resultMap 能够进行自动映射匹配的前提是字段名和属性名需要一致,在默认映射级别(PARTIAL)情况下,若一致,即使没有做属性名和字段名的匹配,也可以在后台获取到未匹配过的属性值;若不一致,且在 resultMap 里没有做映射,那么就无法在后台获取并输出。

案例解析:根据用户名和用户角色 id 查询出用户表中用户信息,要求用户角色要显示角色名称而不是角色 id。

方式 1 :使用 resultType 做自动映射 (了解即可)

注意,MyBatis 中使用 resultType 做自动映射,一定要注意:字段名和 JavaBean 的属性名必须一致。若不一致,则需要给字段起别名,保证别名与属性名一致。

(1)修改实体类 POJO:User.java,增加 userRoleName 属性,添加对应的 get 和 set 方法。
  
   private String userRoleName; //用户角色名称
    
   public String getUserRoleName() {
        return userRoleName;
    }
    public void setUserRoleName(String userRoleName) {
        this.userRoleName = userRoleName;
    }

(2)UserMapper.java 接口中编写查询用户列表的 getUserList()方法。

  /**
     * 根据用户名称(模糊查询)和用户角色查询用户列表(要求用户角色要显示角色名称而不是角色 id)
     * @param userName 用户名称
     * @param userRole 用户角色
     * @return
     */
    public List<User> getUserList(@Param("uName")String userName,@Param("uRole")int userRole); 

(3)UserMapper.xml 中,编写查询用户列表的 SQL 语句,对表(mbms_user)和角色表(mbms_role)进行连表查询,使用 resultType 做自动映射。

<!-- 根据用户名称(模糊查询)和用户角色查询用户列表(要求用户角色要显示角色名称而不是角色 id) -->
    <select id="getUserList" resultType="user">
        SELECT u.*,r.roleName AS userRoleName FROM `smbms_user` u,`smbms_role` r  
        WHERE u.`userRole`=r.`id`
         AND userRole=#{uRole}
         AND userName LIKE CONCAT('%',#{uName},'%')
    </select>

说明:使用 resultType 做自动映射,要求数据库字段名必须和 POJO 属性名一致。因此要给数据库字段取别名,别名为 userRoleName 。

(4)单元测试类 

 @Test //测试根据用户名称(模糊查询)和用户角色查询用户列表(要求用户角色要显示角色名称而不是角色 id)
 public void testGetUserList(){
      List<User> userList=new ArrayList<User>();
      String userName="赵";
       int userRole=2;
       userList=session.getMapper(UserMapper.class).getUserList(userName, userRole);
       for (User user : userList) {
             System.out.println(user);
       }
 }
    
   说明:此种方法必须给 r.roleName 取别名为  userRoleName 。

方式 2 :使用 resultMap 来自定义映射结果(推荐使用)

通过 resultMap 来自定义映射结果。 使用 resultMap 做自定义结果映射,字段名可以不一致,并且还可以指定要显示的列,比较灵活,应用也广泛(推荐使用)

(1)修改实体类 POJO:User.java,增加 userRoleName 属性,添加对应的 get 和 set 方法。
  
   private String userRoleName; //用户角色名称
    
   public String getUserRoleName() {
        return userRoleName;
    }
    public void setUserRoleName(String userRoleName) {
        this.userRoleName = userRoleName;
    }

(2)UserMapper.java 接口中编写查询用户列表的 getUserList()方法。

  /**
     * 根据用户名称(模糊查询)和用户角色查询用户列表(要求用户角色要显示角色名称而不是角色 id)
     * @param userName 用户名称
     * @param userRole 用户角色
     * @return
     */
    public List<User> getUserList(@Param("uName")String userName,@Param("uRole")int userRole); 

(3)UserMapper.xml 中,编写查询用户列表的 SQL 语句,对表(mbms_user)和角色表(mbms_role)进行连表查询,使用 resultMap 做自定义结果映射。

<!-- 根据用户名称(模糊查询)和用户角色查询用户列表(要求用户角色要显示角色名称而不是角色 id) -->
    <select id="getUserList" resultMap="userList">
        SELECT u.*,r.roleName FROM
        `smbms_user` u,`smbms_role` r
        WHERE u.`userRole`=r.`id`
        AND userRole=#{uRole}
        AND userName LIKE CONCAT('%',#{uName},'%')
    </select>

    <resultMap type="user" id="userList">
        <result property="id" column="id" />
        <result property="userCode" column="userCode" />
        <result property="userName" column="userName" />
        <result property="phone" column="phone" />
        <result property="birthday" column="birthday" />
        <result property="gender" column="gender" />
        <result property="userRole" column="userRole" />

        <result property="userRoleName" column="roleName" />
    </resultMap>

(4)单元测试类
@Test //测试根据用户名称(模糊查询)和用户角色查询用户列表(要求用户角色要显示角色名称而不是角色 id)
 public void testGetUserList(){
      List<User> userList=new ArrayList<User>();
      String userName="赵";
       int userRole=2;
       userList=session.getMapper(UserMapper.class).getUserList(userName, userRole);
       for (User user : userList) {
             System.out.println(user);
       }
 }
   说明:resultMap 做自定义结果映射,与 MyBatis 的自动映射级别(autoMappingBehavior)有关。
补充的说明:属性名和列名不一致

比如开发过程中常见的情境,JavaBean 属性使用驼峰命名,数据库列名单词之间加入下划线。

public class User {
  private int id;
  private String username;
  private String hashedPassword;

  public int getId() {
    return id;
  }
  public void setId(int id) {
    this.id = id;
  }
  public String getUsername() {
    return username;
  }
  public void setUsername(String username) {
    this.username = username;
  }
  public String getHashedPassword() {
    return hashedPassword;
  }
  public void setHashedPassword(String hashedPassword) {
    this.hashedPassword = hashedPassword;
  }
}
<select id="selectUsers" resultType="User">
  select
    user_id,user_name,hashed_password
  from some_table
  where id = #{id}
</select>

为了解决上述问题,我们只需要在 <resultMap>中做一下简单的配置,然后在引用它的语句中设置 <resultMap> 属性就行了。

<resultMap id="userResultMap" type="User">
  <id property="id" column="user_id" />
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap> 
<select id="selectUsers" resultMap="userResultMap">
  select user_id, user_name, hashed_password
  from some_table
  where id = #{id}
</select>

注意:这里去掉了<reslutType>属性,用<resultMap>代替,二者只能选择其中的一个。

补充的说明:高级结果映射

MyBatis 创建时的一个思想是:数据库不可能永远是你所想或所需的那个样子。

我们希望每个数据库都具备良好的第三范式或 BCNF 范式,可惜它们并不都是那样。

如果能有一种数据库映射模式,完美适配所有的应用程序,那就太好了,可惜没有。 而 ResultMap 就是 MyBatis 对这个问题的答案。

一对一映射

比如,我们如何映射下面这个语句?

<select id="selectBlogDetails" resultMap="detailedBlogResultMap">
  select
       B.id as blog_id,
       B.title as blog_title,
       B.author_id as blog_author_id,
       A.id as author_id,
       A.username as author_username,
       A.password as author_password,
       A.email as author_email,
       A.bio as author_bio,
       A.favourite_section as author_favourite_section
  from Blog B
  left outer join Author A on B.author_id = A.id
  where B.id = #{id}
</select>

这是典型的一对一的关联关系情况,我们通过<association>配置就可以解决这个问题。

<resultMap id="detailedBlogResultMap" type="Blog">
  <constructor>
    <idArg column="blog_id" javaType="int"/>
  </constructor>
  <result property="title" column="blog_title"/>
  <association property="author" javaType="Author">
    <id property="id" column="author_id"/>
    <result property="username" column="author_username"/>
    <result property="password" column="author_password"/>
    <result property="email" column="author_email"/>
    <result property="bio" column="author_bio"/>
    <result property="favouriteSection" column="author_favourite_section"/>
  </association>
</resultMap>

映射在 javaBean 中的表示:

private Author author;

接下来,我们一步一步来看这些元素的意义。

reslutMap 的属性

  • id:当前命名空间中的一个唯一标识,用于标识一个结果映射。
  • type:类的完全限定名, 或者一个类型别名。

id 和 result:id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。

这两者之间的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。

两者的一些属性:

  • property:映射到列结果的字段或属性。
  • column:数据库中的列名,或者是列的别名。
  • javaType:一个 Java 类的全限定名,或一个类型别名。通常不会配置,mybatis 能够根据参数信息自动识别,如果你映射到的是 HashMap,那么你应该明确地指定 javaType 来保证行为与期望的相一致。
  • jdbcType:JDBC 类型,所支持的 JDBC 类型参见这个表格之后的“支持的 JDBC 类型”。

多对多映射

首先来看对应的 SQL 语句:

<select id="selectBlog" resultMap="blogResult">
  select
  B.id as blog_id,
  B.title as blog_title,
  B.author_id as blog_author_id,
  P.id as post_id,
  P.subject as post_subject,
  P.body as post_body,
  from Blog B
  left outer join Post P on B.id = P.blog_id
  where B.id = #{id}
</select>
<resultMap id="blogResult" type="Blog">
  <id property="id" column="blog_id" />
  <result property="title" column="blog_title"/>
  <collection property="posts" ofType="Post">
    <id property="id" column="post_id"/>
    <result property="subject" column="post_subject"/>
    <result property="body" column="post_body"/>
  </collection>
</resultMap>

javaBean 中我们这样表示集合:

private List<Post> posts;

大部分都和我们上面学习过的关联元素非常相似,这里只是新增了一个 ofType属性。

这个属性非常重要,它用来将 JavaBean(或字段)属性的类型和集合存储的类型区分开来。 所以你可以按照下面这样来阅读映射:

<collection property="posts" javaType="ArrayList" ofType="Post"/>

“posts 是一个存储 Post 的 ArrayList 集合”。在一般情况下,MyBatis 可以推断 javaType 属性,因此并不需要填写。所以很多时候你可以简略成:

<collection property="posts" ofType="Post"/>

高级关联和集合映射是一个深度话题。文章的介绍只能到此为止。配合少许的实践,你会很快了解全部的用法。