Mybatis源码之美:3.5.4.唯一标标识符--id元素

949 阅读8分钟

解析id元素

mybatis的官方文档中,是将idresult两个元素归为一类进行介绍的:

id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。 这两者之间的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。

上面的话很容易让人产生一种误解id元素的配置会影响mybatis的一级缓存和二级缓存。

但是事实上,这里提到的缓存和mybatis一级\二级缓存不是同一个概念,具体的区别我们在后面分析代码的时候会指出。

idresult两个元素比较类似,他们DTD结构定义一致,属性配置一致:

<!ELEMENT id EMPTY>
<!ATTLIST id
property CDATA #IMPLIED
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>

<!ELEMENT result EMPTY>
<!ATTLIST result
property CDATA #IMPLIED
javaType CDATA #IMPLIED
column CDATA #IMPLIED
jdbcType CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>

就连他们的属性的作用也基本一致,因此,本篇文章不会浪费笔墨重新介绍id元素的属性配置,而是着重于介绍和梳理idresult两个元素之间的不同的之处。

mybatis官方文档中,明确指出:id元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。

并给出了使用id元素的作用:这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。

惊讶

但是事实上,上面对于id元素作用的描述并不准确,或者说太过笼统,我认为更准确的描述应该是:使用id元素可以通过临时缓存对象来提高在嵌套结果映射中处理数据的性能

我尝试着去搜索关于id元素的相关资料,很遗憾的是,除了官方文档中给出的些许描述之外,我没能找到更多有效的、关于描述idresult两个元素不同之处的文章。

遗憾

因此,在没有额外资料的前提下,我选择自己梳理源码,来获得id元素那些没有被明确告知的特性。

源码的梳理并不复杂,但是却涉及到了很多目前还没有接触的源码,因此,这里只是给出结论,不会详细的深入到源码中,探讨id元素的解析和使用。

首先,我们要明确一点,使用id元素来提升数据处理的性能有一个大前提:id元素在使用时,必须是作为嵌套结果映射存在的。

如果id元素在运行时,作为返回结果的顶级结果映射,并不会提升数据处理的性能.

体验一下ID元素的用法

通过前面的学习,我们已经知道我们可以通过collection,association以及discriminator元素的case子元素来实现resultMap的嵌套使用。

接下来,我们就通过一个简单的实验了解一下id元素的用法.

我们新建一个单元测试包org.apache.learning.result_map.id,并在包下新建两个简单的实体:UserRole,二者的关系是1:n:

@Data
public class Role {
    private Integer id;
    private String name;
}

@Data
public class User {
    private Integer id;
    private String name;
    private List<Role> roles;
}

对应的初始化脚本和数据:

/* ========================  插入用户数据   =============================*/
drop table USER if exists;
create table USER
(
    id      int,
    name    varchar(20)
);
insert into USER (id, name) values (1, 'Panda');
insert into USER (id, name) values (1, 'Panda2');

/* ========================  插入角色数据   =============================*/
drop table ROLE if exists;
create table ROLE
(
    id   int,
    name varchar(20)
);
insert into ROLE (id, name) values (1, '管理员');
insert into ROLE (id, name) values (2, '普通用户');

/* ========================  插入用户角色数据   =============================*/
drop table USER_ROLE if exists;
create table USER_ROLE
(
    user_id   int,
    role_id   int
);
insert into USER_ROLE (user_id, role_id) values (1, 1);
insert into USER_ROLE (user_id, role_id) values (1, 2);

UserRole之间通过一张USER_ROLE表进行关联.

这是使用到的数据:

  • USER表的数据:

    id name
    1 Panda
    1 Panda2

    需要注意的是,在这里两个用户数据的ID都是1.

  • ROLE表的数据:

    id name
    1 管理员
    2 普通用户
  • USER_ROLE表的数据:

    user_id role_id
    1 1
    2 2

然后提供了一个IDMapper.java文件:

IDMapper.java:

public interface IDMapper {
    List<User> selectAllUserRole();

    List<User> selectAllUserRoleWillLose();
}

IDMapper中定义了两个方法,其中selectAllUserRole()方法能够正确的按照预期获取数据,但是selectAllUserRoleWillLose()方法却会丢失掉一条用户数据.

之后我们在IDMapper.xml文件中为两个方法提供配置:

<!-- User 和 Role 对象的标准 ResultMap定义 -->
<resultMap id="role" type="org.apache.learning.result_map.id.Role">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
</resultMap>

<resultMap id="user" type="org.apache.learning.result_map.id.User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
</resultMap>

<!-- ===================== 分割线 ===================== -->
<!--  通过内嵌select语句获取角色数据 -->
<select id="selectRolesByUserID" resultMap="role">
    SELECT *
    FROM ROLE r
                LEFT JOIN USER_ROLE ur ON r.id = ur.role_id
    WHERE ur.user_id = #{id}
</select>
<resultMap id="userRoleWithSelect" type="org.apache.learning.result_map.id.User" extends="user">
    <collection property="roles" column="{id=id}" select="selectRolesByUserID"/>
</resultMap>
<select id="selectAllUserRole" resultMap="userRoleWithSelect">
    SELECT *
    FROM USER
</select>

<!-- ===================== 分割线 ===================== -->
<!-- 通过内嵌结果映射获取角色数据 -->
<resultMap id="userRoleWithResultMap" type="org.apache.learning.result_map.id.User" extends="user">
    <collection property="roles" columnPrefix="role_" resultMap="role"/>
</resultMap>
<select id="selectAllUserRoleWillLose" resultMap="userRoleWithResultMap">
    SELECT u.*,r.id as role_id,r.name as role_name
    FROM USER u
                INNER JOIN USER_ROLE ur ON u.id = ur.user_id
                LEFT JOIN ROLE r ON ur.role_id = r.id
</select>

配置内容看起来很多,但是实际上只有三部分,第一部分是:UserRole对象对应的的两个标准的ReultMap定义,需要注意的是这两个对象的id属性我们是通过id元素配置的.

标准配置

第二部分是selectAllUserRole()方法对应的配置,在这里,根据我们的定义,mybaits将会通过名为selectRolesByUserIDselect语句获取用户对应的角色数据:

内嵌select查询

第三部分是selectAllUserRoleWillLose()方法对应的配置,这里配置用户角色数据是通过内嵌结果映射来实现的:

内嵌结果映射

注意一下,我们前面说过:id元素可以通过临时缓存对象来提高在嵌套结果映射中处理数据的性能.

就是因为这句话,导致我们的selectAllUserRoleWillLose()方法会丢失掉一条用户数据.

然后我们编写一个单元测试IDMapperTest,来调用IDMapper中的方法定义:

@Test
public void cacheTest() {
    log.debug("========================= selectAllTeacher  =========================");
    List<User> fullUsers = idMapper.selectAllUserRole();
    assert  fullUsers.size()==2;
    log.debug("fullUsers={}", fullUsers);

    log.debug("========================= selectAllTeacher2  =========================");
    // 同ID 将会命中错误 缓存
    List<User> loseUsers = idMapper.selectAllUserRoleWillLose();
    assert  loseUsers.size()==1;
    log.debug("loseUsers={}", loseUsers);
}

单元测试能够按照预期运行:

运行结果

注意看上图中被框起来的地方,selectAllUserRoleWillLose()方法的的确确丢失了namePanda2的用户数据.

疑惑

这是为什么呢?

挠头

这是因为id元素除了具有result元素所拥有的的特性之外,它还唯一标志着一个结果对象.

id元素标识的属性将会作为对象的标识符,该标识符会在比较对象实例的时候被使用

在上面的示例中,我们的执行的查询语句:

 SELECT u.*,r.id as role_id,r.name as role_name
    FROM USER u
                INNER JOIN USER_ROLE ur ON u.id = ur.user_id
                LEFT JOIN ROLE r ON ur.role_id = r.id

将会获取到的这些记录:

id name role_id role_name
1 Panda 1 管理员
1 Panda 2 普通用户
1 Panda2 1 管理员
1 Panda2 2 普通用户

mybatis在处理时,会依次遍历上面的四条数据,在处理第一条记录时,idname属性将会被实例化为一个User对象,并缓存起来.

第一条记录

在进行缓存操作时,mybatis会根据运行上下文来为User对象创建一个CacheKey对象,这个CacheKey对象用来从缓存中读取User对象.

我们这里不对CacheKey对象做深入的了解,简单将缓存理解为Map集合,CacheKey就是key,User对象就是value.

因为我们通过id元素指定了User对象的标识符为id属性,所以在缓存User对象时,id属性将会成为创建CacheKey对象的关键因子.

在处理第二行记录的时候,我们直接根据id属性命中了缓存起来的User对象,避免重复创建相同的User对象,这样做不会有任何问题,而且能大大提升运行效率.

但是,在处理第三行记录的时候,因为我们提供的数据中两个用户的id都是1,所以第三行的记录还是能够创建出一样的CacheKey,继续命中缓存,使用同一个User对象.

第三行记录

但是,这样就出问题了,因为我们第三行记录的name值为Panda2,而缓存中的数据namePanda,这二者显然不是同一个对象.

到了第四行依旧如此,因为命中了错误的缓存,进而导致我们丢失了name值为Panda2的记录.

出现的问题,原因不仅在于我们为不同的记录提供了相同的id属性值,还在于,我们在id属性不能作为一条记录的唯一标志时,还依然将使用了id元素来配置id属性.

所以,id虽然能够提升执行效率,但也不能乱用,一定要和实际数据相匹配哟.

开心

就酱,告辞!

告辞

关注我,一起学习更多知识

关注我