Mybatis源码之美:3.5.2.负责一对一映射的association元素和负责一对多映射的collection元素

负责一对一映射的association元素和负责一对多映射的collection元素

集合啦

负责一对一映射的association元素

association元素的简单应用

在大多数业务场景下,我们的PO都是一个简单的javaBean定义,他的属性定义基本都是简单属性定义。

但是有些时候,我们可能会需要定义一个较为复杂的PO,这个PO中的某些属性可能会是另一个PO定义。

association元素就被应用在这种场景下,它用于关联两个具有一对一关系的复杂java对象。

为了简化描述和理解,我将外层对象称之为父对象,被关联的内部对象称之为子对象

翘脚

我们通过一个简单的示例来看一下association元素的用法:

在一个简单的用户对象中嵌套了一个角色对象:


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

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

在此处,User对象为父对象,Role对象为子对象。

他们对应的表结构和初始数据如下:

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

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

利用association元素配置UserRole两个对象之间的关系:




    

提供一个包含了UserRole数据的查询语句:


运行结果:

运行结果

具体的代码可以参见单元测试:单元测试AssociationTestone2One()方法。

详细访问地址:https://gitee.com/topanda/mybatis-3/tree/master/src/test/java/org/apache/learning/result_map/association

在上面的示例代码中,我们使用association元素绑定了User对象和Role对象之间的关系,并成功的在一次方法调用中获得了两个完整的对象。

工作使我快乐

association元素的DTD定义

association元素的DTD定义看起来要比result元素复杂的多:



睁大眼睛

如果仔细看上面的DTD定义,我们会发现association元素和resultMap元素具有完全相同的子元素的定义:



这一点意味着在某种角度上来讲association元素就是一个特殊的resultMap元素。

事实上也是如此,association元素除了不能像resultMap元素一样单独存在外,它具有resultMap元素所拥有的所有特性

除此之外,相较于resultMap元素,association元素还具有一些独有的属性定义,这些属性定义使得association元素甚至比resultMap元素更为强大。

要想让mybatis成功的处理一个子对象,我们就要明确的告知mybatis应该如何利用现有数据获取到子对象所需的数据,以及如何将所需数据转换为子对象

推眼镜

association元素提供了三种方式来描述这一过程,他们分别是:嵌套查询语句嵌套结果映射以及多结果集配置

上面三种方式可能听起来比较陌生,没关系,我们接下来就会详细的了解这三种不同的方式。

association元素的属性定义

association元素具有十三个属性定义,这些属性根据作用可以分为四类:

  • 通用型功能性查询属性定义
  • 描述嵌套查询语句的属性定义
  • 描述嵌套结果映射的属性定义
  • 描述多结果集的属性定义

通用型功能性查询属性定义

我们先来看通用型功能性查询属性定义。

result元素一样,association元素也定义了propertyjavaTypejdbcTypeTypeHandler四个属性。

这四个属性在定义和作用上都和result元素中完全一致,因此这里就不在赘述。

描述嵌套查询语句的属性定义

负责配置嵌套查询语句的是三个可选的属性,他们分别是columnselect以及fetchType

在使用嵌套查询语句的场景下columnselect两个属性均是必填的。

select属性指向一个标准select语句,比如:


select语句中可能会包含一些行内参数映射,比如selectRoleById中的#{id}定义,行内参数映射所需的数据我们可以通过column属性来进行配置。

association元素的column属性的作用和result元素中的稍有不同,association元素的column属性可以是普通的列名称定义,比如column="id",也可以是一个复合的属性描述,比如:column="{prop1=col1,prop2=col2}"

复合属性描述的语法定义为:以{开始,}结尾,中间通过,分隔多个属性描述,每个属性描述均由行内参数映射名,=,列名称三部分构成。

行内参数映射名对应的是select语句中的行内参数映射,列名称则对应着父对象中的数据列名称。

最后一个fetchType属性用于控制子对象的加载行为,他有lazyeager两个取值,分别对应着懒加载和立即加载。

fetchType属性的优先级要高于配置全局懒加载的属性lazyLoadingEnabled,当指定了fetchType属性之后,lazyLoadingEnabled的配置将会被忽略。

我们在上文中创建的单元测试中继续进行简单的测试工作。

测试常规嵌套查询

新增一个配置了嵌套查询的resultMap以及resultMap对应的两个select元素:


    





编写单元测试:

@Test
public void nestedQueryTest() {
    @Cleanup
    SqlSession sqlSession = sqlSessionFactory.openSession();
    associationMapper = sqlSession.getMapper(AssociationMapper.class);

    User user=associationMapper.selectUserByIdNestedQuery(1);
    Assertions.assertNotNull(user.getRole());
}
测试复合属性描述

创建一个association元素的column属性为{id=role_id}resultMap定义:



    



编写单元测试:

@Test
public void selectUserNestedQueryWithCompoundPropertyTest() {
    sqlSessionFactory.getConfiguration().addMapper(AssociationMapper.class);
    @Cleanup
    SqlSession sqlSession = sqlSessionFactory.openSession();
    associationMapper = sqlSession.getMapper(AssociationMapper.class);
    User user = associationMapper.selectUserNestedQueryWithCompoundProperty(1);
    Assertions.assertNotNull(user.getRole());
}
测试懒加载属性

复用上面的代码创建一个启用了懒加载resultMap:



    



编写单元测试:

@Test
public void selectUserNestedQueryWithLazyTest() {
    // 禁用全局懒加载
    sqlSessionFactory.getConfiguration().setLazyLoadingEnabled(false);
    sqlSessionFactory.getConfiguration().addMapper(AssociationMapper.class);

    @Cleanup
    SqlSession sqlSession = sqlSessionFactory.openSession();
    associationMapper = sqlSession.getMapper(AssociationMapper.class);
    User user = associationMapper.selectUserNestedQueryWithLazy(1);

    System.out.println("====   Lazy Load  ====");

    Assertions.assertNotNull(user.getRole());
}

运行结果:

懒加载

可以看到,虽然我们禁用了全局懒加载配置,但是在本次方法调用中依然成功启用了懒加载。

描述嵌套结果映射的属性定义

在本篇文章的最开始我们就已经接触到了嵌套结果映射的使用方式。

负责配置嵌套结果映射的是四个可选的属性resultMap,columnPrefix,notNullColumn以及autoMapping

属性resultMap指向了一个标准的resultMap元素配置。

mybatis将会根据resultMap元素配置将查询到的数据映射为子对象

比如在本篇开始使用的示例中:




    

根据association元素的配置,User对象的role属性将会根据名为roleresultMap配置来生成。

在示例中,association元素还配置了columnPrefix属性的值为role_,这是因为我们的USERROLE两张表中都定义了idname属性:

create table USER
(
    id   int,
    name varchar(20),
    role_id int
);

create table ROLE
(
    id   int,
    name varchar(20)
);

为了区分二者的区别,我们在查询数据时为ROLE表中的列指定了别名,别名的生成规则是统一添加role_前缀:


查询到的结果:

id name role_id role_name
1 Panda 1 普通用户

但是添加了role_前缀之后,查询到的数据就无法和Role中的属性定义相匹配。

名称不匹配
咋办

为了解决这个问题,association元素提供了columnPrefix属性。

columnPrefix属性的值将会作用在被引用的resultMap配置上,在匹配其column属性时,会先添加统一的前缀,之后再进行匹配操作。

association元素还有一个可选的notNullColumn属性,默认情况下,只有在至少一个属性不为空的前提下才会创建子对象,但是我们可以通过notNullColumn属性来控制这一行为,notNullColumn属性的取值是以,分隔的多个属性名称,只有在这些属性均不为空的前提下,子对象才会被创建。

比如在我们的示例代码中,如果我们为association元素指定了notNullColumn的值为name:


    



那么只有在ROLE表的name列不为null时才会实例化User对象的role属性,我们新增两条数据:

insert into USER (id, name,role_id) values (2, 'Panda2', 2);
insert into ROLE (id, name) values (2, null);

编写一个新的单元测试:

@Test
public void selectUserRoleByIdWithNotNullColumnTest() {

    sqlSessionFactory.getConfiguration().addMapper(AssociationMapper.class);
    @Cleanup
    SqlSession sqlSession = sqlSessionFactory.openSession();
    associationMapper = sqlSession.getMapper(AssociationMapper.class);

    User u = associationMapper.selectUserRoleByIdWithNotNullColumn(1);
    log.debug("id为1的User对象:{}",u);
    Assertions.assertNotNull(u.getRole());

    User u2 = associationMapper.selectUserRoleByIdWithNotNullColumn(2);
    log.debug("id为2的User对象:{}",u2);
    Assertions.assertNull(u2.getRole());
}

关键运行日志:

DEBUG [main] - id为1的User对象:User(id=1, name=Panda, role=Role(id=1, name=普通用户))
DEBUG [main] - id为2的User对象:User(id=2, name=Panda2, role=null)

数据:

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

我们会发现id2的用户数据,因为Rolename属性没有设置,所以他的role也没有被实例化。

除了上面的三个属性之外,association元素还有一个比较特殊的属性autoMapping

我们前面说过association元素是一个特殊的resultMap元素,它具有和resultMap元素一样的子元素定义,因此我们可以直接通过association元素的子元素来声明一个嵌套结果映射:


  
  

association元素的autoMapping属性的行为和resultMap元素的类似,都是用于配置当前结果映射的自动映射行为。

需要注意的是,通过selectresultMap属性引用的结果映射是不受该属性的影响的。

描述多结果集的属性定义

association元素的最后一类属性是用来描述多结果集的.

多结果集就目前来看,在实际业务中,我几乎没有用到过.但是这并不妨碍我们去学习和了解他,有些时候,这些偏门的知识可能会有大用处哟.

摊手

用于描述多结果集的属性有三个,他们分别是column,foreignColumn以及resultSet.

多结果集

在了解这些属性的作用之前,我们先了解一下什么是多结果集?


路过

多结果集就是:我们可以通过执行一次数据库操作,获取到多个ResultSet对象.

根据JDBC规范,我们可以通过connection.getMetaData().supportsMultipleResultSets();方法来查看当前数据源是否支持多结果集:

hsqldb

通常来讲,我们一次数据库操作只能得到一个ResultSet对象,但是部分数据库支持在一次查询中返回多个结果集.

还有部分数据库支持在存储过程中返回多个结果集,或者支持一次性执行多个语句,每个语句都对应一个结果集.


脑袋一片空白

对应的场景可能有些多,这里我们主要还是看存储过程中的多结果集配置:

我们先创建一个MultiResultSetStoredProcedures对象,该对象用来给hsqldb提供一个存储过程实现:

public class MultiResultSetStoredProcedures {

    public static void getAllUserAndRoles(Connection connection, ResultSet[] resultSets ,ResultSet[] resultSets2) throws SQLException {
        Statement statement=connection.createStatement();
        resultSets[0] = statement.executeQuery("SELECT * FROM USER");
        resultSets2[0] = statement.executeQuery("SELECT * FROM ROLE");

    }
}

MultiResultSetStoredProceduresgetAllUserAndRoles()方法在实现上会分别查询出USERROLE两个表中的数据赋值给两个ResultSet对象.

关于更多hsqldb存储过程的内容,可以访问链接进行学习:http://hsqldb.org/doc/2.0/guide/sqlroutines-chapt.html#src_psm_handlers.

之后我们在CreateDB.sql新增一条关于存储过程的配置:

DROP PROCEDURE getAllUserAndRoles IF EXISTS;
CREATE PROCEDURE getAllUserAndRoles()
    READS SQL DATA
    LANGUAGE JAVA
    DYNAMIC RESULT SETS 1
    EXTERNAL NAME 'CLASSPATH:org.apache.learning.result_map.association.MultiResultSetStoredProcedures.getAllUserAndRoles';

最后,我们创建一个名为testMultiResultSet()的单元测试:

@Test
@SneakyThrows
public void testMultiResultSet() {
    @Cleanup
    Connection connection = sqlSessionFactory.openSession().getConnection();

    CallableStatement statement = connection.prepareCall("call getAllUserAndRoles()");

    ResultSet resultSet = statement.executeQuery();
    log.debug("===========ResultSet FOR USER   ===============");
    while (resultSet.next()) {
        log.debug("USER={id:{},name:{},roleId:{}}", resultSet.getInt("id"),resultSet.getString("name"),resultSet.getString("role_id"));
    }

    log.debug("===========ResultSet FOR ROLE   ===============");
    assert statement.getMoreResults();
    resultSet = statement.getResultSet();
    while (resultSet.next()) {
        log.debug("ROLE={id:{},name:{}}", resultSet.getInt("id"),resultSet.getString("name"));
    }
}

在该单元测试中,我们将会依次读取存储过程getAllUserAndRoles()返回的两个ResultSet,并打印出来.

关键运行日志:

DEBUG [main] - ===========ResultSet FOR USER   ===============
DEBUG [main] - USER={id:1,name:Panda,roleId:1}
DEBUG [main] - USER={id:2,name:Panda2,roleId:2}
DEBUG [main] - ===========ResultSet FOR ROLE   ===============
DEBUG [main] - ROLE={id:1,name:普通用户}
DEBUG [main] - ROLE={id:2,name:null}

由此可见,我们的存储过程getAllUserAndRoles()成功的返回两个结果集.

属性

在了解resultSet属性之前,我们需要简单补充一下select元素的resultSets属性相关的知识.

默认情况下,一条select语句对应一个结果集,因此我们不需要关注结果集相关的问题.

但是,通过实验,我们已经成功的在一条select语句中返回了多个结果集,如果我们想操作不同的结果集的数据,我们就有必要区分出每个结果集对象.

mybaits为这种场景提供了一个解决方案,它允许我们在配置select元素的时候,通过配置其resultSets属性来为每个结果集指定名称.

抱住我的小猪猪

结果集的名称和resultSets属性定义顺序对应.如果有多个结果集的名称需要配置,名称之间使用,进行分隔.

比如,在下面的示例代码中,第一个ResultSet名为users,第二个ResultSet名为roles:


association元素提供的resultSet属性读取的就是resultSets属性定义的名称,当前association元素将会使用resultSet属性对应的ResultSet对象来加载.

需要注意的是,association元素的column属性在多结果集模式下的表现和在嵌套查询语句模式下的表现稍有不同.

多结果集模式下,column属性将会配合着foreignColumn属性一起使用.

有趣

foreignColumn属性用于指定在映射时需要使用的父对象的数据列名称,如果有多个数据列,使用,进行分隔.

column属性的命名规则同foreignColumn属性一致,它用于指定在映射时需要使用的子对象的数据列名称.

foreignColumn属性和column属性之间是顺序关联的.

多结果集模式的应用

最后,通过一个简单的测试,来实际看一下多结果集模式的应用.

复用之前的代码,我们在AssociationMapper.xml文件中新增一个调用存储过程的方法声明以及相应的resultMap配置:



    



并在AssociationMapper.java中添加对应的方法声明:

List selectAllUserAndRole();

最后编辑一个单元测试,来看一下实际运行情况:

@Test
public void selectAllUserAndRoleTest() {
    sqlSessionFactory.getConfiguration().addMapper(AssociationMapper.class);
    @Cleanup
    SqlSession sqlSession = sqlSessionFactory.openSession();
    associationMapper = sqlSession.getMapper(AssociationMapper.class);
    List users = associationMapper.selectAllUserAndRole();
    log.debug("users-{}",users);
    assert  users.get(0).getRole().getId()==1;
    assert  users.get(1).getRole().getId()==2;
}

单元测试成功运行,并输出下列关键日志:

...省略

DEBUG [main] - ==>  Preparing: {call getAllUserAndRoles() }
DEBUG [main] - ==> Parameters:
DEBUG [main] - <==      Total: 2
DEBUG [main] - <==      Total: 2
DEBUG [main] - users-[User(id=1, name=Panda, role=Role(id=1, name=普通用户)), User(id=2, name=Panda2, role=Role(id=2, name=null))]

...省略

总结

到这里我们就了解了association元素的所有属性定义.

至于association元素的子元素定义,因为在定义上和用法上都和resultMap元素完全一致.

所以在我们了解完resultMap元素的子元素之后,自然而然就了解了关于association元素的子元素定义.

最后,我们总结一下association元素的属性作用:

  • 通用型功能性查询属性定义

    属性名称 必填 类型 描述
    property false String PO对象的属性名称
    javaType false String PO对象的属性类型
    jdbcType false String 数据库中的列类型
    typeHandler false String 负责将数据库数据转换为PO对象的类型转换器
  • 描述嵌套查询语句的属性定义

    属性名称 必填 类型 描述
    column true String 用于配置行内参数映射,column属性可以是普通的列名称定义,比如column="id",也可以是一个复合的属性描述,比如:column="{prop1=col1,prop2=col2}"
    select true String 用于加载复杂类型属性的映射语句的 ID,它会从 column 属性指定的列中检索数据,作为参数传递给目标 select 语句。
    fetchType false String fetchType属性用于控制子对象的加载行为,他有lazy和eager两个取值,分别对应着懒加载和立即加载. fetchType属性的优先级要高于配置全局懒加载的属性lazyLoadingEnabled,当指定了fetchType属性之后,lazyLoadingEnabled的配置将会被忽略。
  • 描述嵌套结果映射的属性定义

    属性名称 必填 类型 描述
    resultMap false String 它指向了一个标准的resultMap元素配置
    columnPrefix false String columnPrefix属性的值将会作用在被引用的resultMap配置上,在匹配其column属性时,会先添加统一的前缀,之后再进行匹配操作。
    notNullColumn false String notNullColumn属性的取值是以,分隔的多个属性名称,只有在这些属性均不为空的前提下,子对象才会被创建.
    autoMapping false boolean autoMapping属性的行为和resultMap元素的类似,都是用于配置当前结果映射的自动映射行为。 需要注意的是,通过select和resultMap属性引用的结果映射是不受该属性的影响的。
  • 描述多结果集的属性定义

    属性名称 必填 类型 描述
    resultSet true String 当前association元素将会使用resultSet属性对应的ResultSet对象来加载
    foreignColumn true String foreignColumn属性用于指定在映射时需要使用的父对象的数据列名称,如果有多个数据列,使用,进行分隔.
    column true String column属性的命名规则同foreignColumn属性一致,它用于指定在映射时需要使用的子对象的数据列名称.

负责一对多映射的collection元素

既然有一对一的复杂对象关系,那自然也会有一对多的复杂对象关系,association元素用来配置一对一的复杂关系,collection元素则是用来配置一对多的复杂对象关系.

collection元素和association元素几乎完全一样:



除了collection元素多了一个ofType属性之外,二者的子元素和属性定义完全一致.

两者的属性含义也完全相同,因此,本篇不会再大费笔墨的去一个个的了解collection元素的完整定义,而是对比着association元素来看二者的不同之处.

靓仔开车

因为collection元素用于表示一对多的复杂对象关系,根据javaType属性的定义,javaType属性应该指向一个集合类型,因此,我们需要一个字段来描述集合中存储的对象类型.

mybatiscollection元素添加了一个额外的ofType属性,这个属性的作用就是用来描述集合中对象的类型的.

我们看一个简单的完整示例.

我们变更在associationUserRole对象的关系,改为一个用户可以拥有多个角色.

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

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

用户和角色关系通过一张用户角色关系表来维护:

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

/* ========================  插入角色数据   =============================*/
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);

编写对应的Mapper对象及其配置文件:

CollectionMapper.java:

public interface CollectionMapper {
    User selectUserRoleById(Integer id);
}

CollectionMapper.xml:






    






需要注意的是,我们在配置collection元素的时候,定义了他的column属性为:{id=id},这样做的原因是因为如果我们直接将列名称id赋值给column属性,User对象的id属性将不会被赋值.

产生这种差异的原因在于,为column属性直接赋值列名称将会覆盖指定列的默认行为.

最后我们编写一个单元测试,查看我们collection元素的映射结果:

@Test
public void selectUserTest() {
    sqlSessionFactory.getConfiguration().addMapper(CollectionMapper.class);
    @Cleanup
    SqlSession sqlSession = sqlSessionFactory.openSession();

    CollectionMapper collectionMapper = sqlSession.getMapper(CollectionMapper.class);

    User user = collectionMapper.selectUserRoleById(1);

    assert user.getId() == 1;
    assert user.getRoles() != null;
    assert user.getRoles().size() == 2;
    log.debug("user={}",user);
}

单元测试运行的关键日志为:

... 省略 ...
DEBUG [main] - ==>  Preparing: SELECT * FROM USER u WHERE u.id = ? 
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - ====>  Preparing: SELECT * FROM ROLE r LEFT JOIN USER_ROLE ur ON r.id = ur.role_id WHERE ur.user_id = ? 
DEBUG [main] - ====> Parameters: 1(Integer)
DEBUG [main] - <====      Total: 2
DEBUG [main] - <==      Total: 1
DEBUG [main] - user=User(id=1, name=Panda, roles=[Role(id=1, name=管理员), Role(id=2, name=普通用户)])
... 省略 ...

user对象的数据:

User对象

结束

至此,我们也算是初步了解了association元素和collection元素了.

告辞

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

关注我

你可能感兴趣的:(Mybatis源码之美:3.5.2.负责一对一映射的association元素和负责一对多映射的collection元素)