MyBatis3源码深度解析(二十四)级联映射与关联查询(一)级联映射的使用

发布于:2024-03-27 ⋅ 阅读:(82) ⋅ 点赞:(0)

前言

MyBatis其中一个比较强大的功能是支持查询结果的级联映射。

使用MyBatis级联映射,可以轻松地实现一对多、一对一、多对多关联查询,甚至实现懒加载。

所谓懒加载,就是当一个实体对象中关联了其他实体对象时,如果没有使用这个被关联的实体对象,则不为被关联的实体对象执行额外的查询操作。而一旦调用当前实体的Getter方法获取被关联的实体对象时,再执行一次额外的查询操作。通过这种方式在一定程度上能够减轻数据库的压力。

第十章 MyBatis级联映射与懒加载

10.1 MyBatis级联映射的使用

10.1.1 简单查询

10.1.1.1 数据准备

假设现在要开发一个电商应用,需要通过数据库记录用户信息和订单信息。用户和订单就是典型的一对多关系,一个用户可以有多个订单,一个订单只会属于一个用户。

为此,在数据库分别新建用户表user和订单表order,并插入一些数据:

CREATE TABLE `user` (
  `user_id` INT PRIMARY KEY AUTO_INCREMENT,
  `name` VARCHAR(20),
  `age` INT,
  `phone` VARCHAR(20),
  `birthday` DATETIME
)

CREATE TABLE `order`(
	`order_id` INT PRIMARY KEY AUTO_INCREMENT,
	`user_id` INT,
	`order_no` VARCHAR(32),
	`address` VARCHAR(20),
	`amount` INT
);

insert into `user` (`name`, `age`, `phone`, `birthday`) values(孙悟空','1500','18705464523','0000-01-01 00:00:00');
insert into `user` (`name`, `age`, `phone`, `birthday`) values(猪八戒','1000','15235468789','0500-03-10 00:00:00');

insert into `order` (`user_id`, `order_no`, `address`, `amount`) values(1','order_01','广东广州','100');
insert into `order` (`user_id`, `order_no`, `address`, `amount`) values('1','order_02','广东河源','200');
insert into `order` (`user_id`, `order_no`, `address`, `amount`) values(2','order_03','广东佛山','300');

需要注意的是,order是MySQL的关键字,因此建表的时候需要加上反单引号;同时,用户和订单的主键均自动生成。

10.1.1.2 编写实体类

下面编写用户表user和订单表order对应的实体类:

public class User {

    private Integer userId;
    private String name;
    private Integer age;
    private String phone;
    private Date birthday;
    // 用户持有多个订单
    private List<Order> orderList;
    
    // getter setter toString ...
}

User类维护了一个List类型的orderList属性,用于存放用户相关联的订单信息,即一个用户可以对应多笔订单(一对多)。

public class Order {

    private Integer orderId;
    private Integer userId;
    private String orderNo;
    private String address;
    private Integer amount;
    // 一笔订单属于一个用户
    private User user;
    
    // getter setter toString ...
}

Order类维护了一个User属性,用于关联订单对应的用户信息,即一笔订单属于一个用户(一对一)。

10.1.1.3 编写Mapper接口及XML配置文件

下面编写UserMapper接口及其对应的XML配置文件,提供一个getUserById()方法,即根据用户ID查询用户信息:

public interface UserMapper {
    User getUserById(@Param("userId") Integer userId);
}
<!--UserMapper.xml-->
<mapper namespace="com.star.mybatis.mapper.UserMapper">
    <select id="getUserById" resultType="User">
        select * from user where user_id = #{userId}
    </select>
</mapper>

继续编写OrderMapper接口及其对应的XML配置文件,提供一个listOrderByUserId()方法,即根据用户ID查询订单信息:

public interface OrderMapper {
    List<Order> listOrderByUserId(@Param("userId") Integer userId);
}
<!--OrderMapper.xml-->
<mapper namespace="com.star.mybatis.mapper.OrderMapper">
    <select id="listOrderByUserId" resultType="Order">
        select * from `order` where user_id = #{userId}
    </select>
</mapper>
10.1.1.4 编写MyBatis主配置文件
<!--mybatis-config.xml-->
<configuration>
    <settings>
        <!--支持返回自动生成主键-->
        <setting name="useGeneratedKeys" value="true"/>
        <!--支持实体名驼峰原则-->
        <setting name="mapUnderscoreToCamelCase" value="true"/>
        <!--日志-->
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>
    <!--别名处理-->
    <typeAliases>
        <package name="com.star.mybatis.entity"/>
    </typeAliases>
    <!--数据源-->
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3306/mybatis_demo"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <!--映射文件-->
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
        <mapper resource="mapper/OrderMapper.xml"/>
    </mappers>
</configuration>
10.1.1.5 编写单元测试
public class Test01 {

    private SqlSession sqlSession;
    private UserMapper userMapper;
    private OrderMapper orderMapper;

    @Before
    public void init() throws IOException {
        InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        sqlSession = sqlSessionFactory.openSession();
        userMapper = sqlSession.getMapper(UserMapper.class);
        orderMapper = sqlSession.getMapper(OrderMapper.class);
    }

    @Test
    public void testSimpleQuery() {
        User user = userMapper.getUserById(1);
        System.out.println(user.toString());
        List<Order> orders = orderMapper.listOrderByUserId(1);
        orders.forEach(System.out::println);
    }

}

由于本案例会多次使用SqlSession对象、UserMapper对象和OrderMapper对象,因此将这3个对象的初始化使用标注了@Before注解的前置方法来完成,方便单元测试方法直接使用。

执行单元测试testSimpleQuery()方法,控制台打印执行结果:

Opening JDBC Connection
Created connection 1894601438.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@70ed52de]
==>  Preparing: select * from user where user_id = ?
==> Parameters: 1(Integer)
<==    Columns: user_id, name, age, phone, birthday
<==        Row: 1, 孙悟空, 1500, 18705464523, 0001-01-01 00:00:00.0
<==      Total: 1
User{userId=1, name='孙悟空', age=1500, phone='18705464523', birthday=Thu Jan 01 00:00:00 CST 1, orderList=null}
==>  Preparing: select * from `order` where user_id = ?
==> Parameters: 1(Integer)
<==    Columns: order_id, user_id, order_no, address, amount
<==        Row: 1, 1, order_01, 广东广州, 100
<==        Row: 2, 1, order_02, 广东河源, 200
<==      Total: 2
Order{orderId=1, userId=1, orderNo='order_01', address='广东广州', amount=100, user=null}
Order{orderId=2, userId=1, orderNo='order_02', address='广东河源', amount=200, user=null}

由于只是简单的查询操作,所以User对象中的orderList属性为空,Order对象中的user属性为空。

10.1.2 一对多级联映射

简单查询时,User对象中的orderList属性为空。那怎么在查询用户信息的同时,把该用户关联的订单信息也一并查询返回呢?

通过MyBatis的级联映射即可实现这种一对多的级联查询。

10.1.2.1 外部Mapper方式

MyBatis的Mapper配置中提供了一个<collection>标签,用于建立实体间一对多的关系。例如:

<!--UserMapper.xml-->
<resultMap id="fullUser" type="User">
    <id column="user_id" property="userId"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <result column="phone" property="phone"/>
    <result column="birthday" property="birthday"/>
    <collection property="orderList"
                select="com.star.mybatis.mapper.OrderMapper.listOrderByUserId"
                ofType="Order"
                javaType="List"
                column="user_id">
    </collection>
</resultMap>

<select id="getFullUserById" resultMap="fullUser">
    select * from user where user_id = #{userId}
</select>

在上述XML配置中,<collection>标签需要嵌套在<resultMap>标签中使用。它们的属性的作用是:

<resultMap>标签:

  • id:唯一标记一个<resultMap>标签,<select>标签的resultMap属性需要与之相对应。
  • type:指定返回实体的类型是User类。

<collection>标签:

  • property:指定User实体中要填充值的属性是orderList。
  • select:指定通过ID为com.star.mybatis.mapper.OrderMapper.listOrderByUserId的Mapper方法来为User实体的orderList属性填充值。
  • ofType:指定orderList属性中存放的类型是Order。
  • javaType:指定orderList属性的类型是List。
  • column:指定user_id字段作为参数传到listOrderByUserId()方法中。

接下来编写一个单元测试,调用UserMapper接口的getFullUserById()方法:

@Test
public void testOne2ManyQuery() {
    User user = userMapper.getFullUserById(1);
    System.out.println(user.toString());
}

控制台打印执行结果:

Opening JDBC Connection
Created connection 1574877131.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5ddeb7cb]
==>  Preparing: select * from user where user_id = ?
==> Parameters: 1(Integer)
<==    Columns: user_id, name, age, phone, birthday
<==        Row: 1, 孙悟空, 1500, 18705464523, 0001-01-01 00:00:00.0
====>  Preparing: select * from `order` where user_id = ?
====> Parameters: 1(Integer)
<====    Columns: order_id, user_id, order_no, address, amount
<====        Row: 1, 1, order_01, 广东广州, 100
<====        Row: 2, 1, order_02, 广东河源, 200
<====      Total: 2
<==      Total: 1
User{userId=1, name='孙悟空', age=1500, phone='18705464523', birthday=Thu Jan 01 00:00:00 CST 1, orderList=[Order{orderId=1, userId=1, orderNo='order_01', address='广东广州', amount=100, user=null}, Order{orderId=2, userId=1, orderNo='order_02', address='广东河源', amount=200, user=null}]}

由执行结果可知,在调用getFullUserById()方法时,MyBatis会先后执行两条SQL语句,第一条查询user表为User实体的属性赋值,第二条执行<collection>标签关联的查询语句为orderList属性赋值。

10.1.2.2 JOIN子句方式

除了使用<collection>标签,还可以使用JOIN子句实现一对多的查询。 例如:

<!--UserMapper.xml-->
<resultMap id="fullUserForJoin" type="User">
    <id column="user_id" property="userId"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <result column="phone" property="phone"/>
    <result column="birthday" property="birthday"/>
    <collection property="orderList"
                ofType="Order"
                javaType="List">
        <id property="orderId" column="order_id"/>
        <result property="userId" column="user_id"/>
        <result property="orderNo" column="order_no"/>
        <result property="address" column="address"/>
        <result property="amount" column="amount"/>
    </collection>
</resultMap>

<select id="getFullUserByIdForJoin" resultMap="fullUserForJoin">
    select u.*, o.* from user u
    left join `order` o on u.user_id = o.user_id
    where u.user_id = #{userId}
</select>

JOIN子句方式与外部Mapper方式不同的地方在于:

第一,JOIN子句方式不需要使用<collection>标签的select属性指定一个外部的Mapper方法为关联属性赋值;

第二,JOIN子句方式的<collection>标签内部,需要为order表的每一个字段设置映射关系,主键使用<id>标签,其他字段使用<result>标签;

接下来编写一个单元测试,调用UserMapper接口的getFullUserByIdForJoin()方法:

@Test
public void testOne2ManyForJoinQuery() {
    User user = userMapper.getFullUserByIdForJoin(1);
    System.out.println(user.toString());
}

控制台打印执行结果:

Opening JDBC Connection
Created connection 271800170.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1033576a]
==>  Preparing: select u.*, o.* from user u left join `order` o on u.user_id = o.user_id where u.user_id = ?
==> Parameters: 1(Integer)
<==    Columns: user_id, name, age, phone, birthday, order_id, user_id, order_no, address, amount
<==        Row: 1, 孙悟空, 1500, 18705464523, 0001-01-01 00:00:00.0, 1, 1, order_01, 广东广州, 100
<==        Row: 1, 孙悟空, 1500, 18705464523, 0001-01-01 00:00:00.0, 2, 1, order_02, 广东河源, 200
<==      Total: 2
User{userId=1, name='孙悟空', age=1500, phone='18705464523', birthday=Thu Jan 01 00:00:00 CST 1, orderList=[Order{orderId=1, userId=1, orderNo='order_01', address='广东广州', amount=100, user=null}, Order{orderId=2, userId=1, orderNo='order_02', address='广东河源', amount=200, user=null}]}

由执行结果可知,使用JOIN子句的方式,只会执行一条SQL语句,但仍然可以实现一对多的关联查询。

10.1.3 一对一关联映射

简单查询时,Order对象中的user属性也为空。要在查询Order对象是同时查询出关联的用户信息(一对一),也通过MyBatis的级联映射来实现。

10.1.3.1 外部Mapper方式

一对一级联映射查询跟一对多级联查询类似,不同的是不再使用<collection>标签,而是使用<association>标签。 例如:

<!--OrderMapper.xml-->
<resultMap id="fullOrder" type="Order">
    <id column="order_id" property="orderId"/>
    <result column="user_id" property="userId"/>
    <result column="order_no" property="orderNo"/>
    <result column="address" property="address"/>
    <result column="amount" property="amount"/>
    <association property="user"
                 select="com.star.mybatis.mapper.UserMapper.getUserById"
                 javaType="User"
                 column="user_id">
    </association>
</resultMap>

<select id="getOrderByNo" resultMap="fullOrder">
    select * from `order` where order_id = #{orderId}
</select>

<association>标签的各个属性和<collection>标签是一样的。

接下来编写一个单元测试,调用OrderMapper接口的getOrderByNo()方法:

@Test
public void testOne2ManyQueryForOrder() {
    Order order = orderMapper.getOrderByNo(1);
    System.out.println(order.toString());
}

控制台打印执行结果:

Opening JDBC Connection
Created connection 1574877131.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5ddeb7cb]
==>  Preparing: select * from `order` where order_id = ?
==> Parameters: 1(Integer)
<==    Columns: order_id, user_id, order_no, address, amount
<==        Row: 1, 1, order_01, 广东广州, 100
====>  Preparing: select * from user where user_id = ?
====> Parameters: 1(Integer)
<====    Columns: user_id, name, age, phone, birthday
<====        Row: 1, 孙悟空, 1500, 18705464523, 0001-01-01 00:00:00.0
<====      Total: 1
<==      Total: 1
Order{orderId=1, userId=1, orderNo='order_01', address='广东广州', amount=100, user=User{userId=1, name='孙悟空', age=1500, phone='18705464523', birthday=Thu Jan 01 00:00:00 CST 1, orderList=null}}

由执行结果可知,MyBatis会先后执行两条SQL语句,第一条查询order表为Order实体的属性赋值,第二条执行<association>标签关联的查询语句为user属性赋值。

10.1.3.2 JOIN子句方式

同样,一对一级联查询也可以通过JOIN子句的方式实现。

<!--OrderMapper.xml-->
<resultMap id="fullOrderForJoin" type="Order">
    <id column="order_id" property="orderId"/>
    <result column="user_id" property="userId"/>
    <result column="order_no" property="orderNo"/>
    <result column="address" property="address"/>
    <result column="amount" property="amount"/>
    <association property="user" javaType="User">
        <id column="user_id" property="userId"/>
        <result column="name" property="name"/>
        <result column="age" property="age"/>
        <result column="phone" property="phone"/>
        <result column="birthday" property="birthday"/>
    </association>
</resultMap>

<select id="getOrderByNoForJoin" resultMap="fullOrderForJoin">
    select o.*, u.* from `order` o
    left join user u on o.user_id = u.user_id
    where order_id = #{orderId}
</select>

单元测试代码:

@Test
public void testOne2ManyQueryForOrderForJoin() {
    Order order = orderMapper.getOrderByNoForJoin(1);
    System.out.println(order.toString());
}

控制台打印执行结果:

Opening JDBC Connection
Created connection 1574877131.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@5ddeb7cb]
==>  Preparing: select o.*, u.* from `order` o left join user u on o.user_id = u.user_id where order_id = ?
==> Parameters: 1(Integer)
<==    Columns: order_id, user_id, order_no, address, amount, user_id, name, age, phone, birthday
<==        Row: 1, 1, order_01, 广东广州, 100, 1, 孙悟空, 1500, 18705464523, 0001-01-01 00:00:00.0
<==      Total: 1
Order{orderId=1, userId=1, orderNo='order_01', address='广东广州', amount=100, user=User{userId=1, name='孙悟空', age=1500, phone='18705464523', birthday=Thu Jan 01 00:00:00 CST 1, orderList=null}}

由执行结果可知,使用JOIN子句的方式,只会执行一条SQL语句,但仍然可以实现一对一的关联查询。

10.1.4 Discriminator

在MyBatis的级联映射中还有一个比较重要的概念:Discriminator,意思是“鉴别器”,它类似于Java中的switch语法,能够根据数据库记录中某个字段的值映射到不同的ResultMap。

假设现在有这样一个需求:当查询用户信息时,如果用户年龄为1000岁(目前数据库中孙悟空1500岁,猪八戒1000岁),则查询用户信息,否则不查询。

即根据user表中的age字段的值不同,做不同的映射处理,则可以使用<discriminator>标签来完成。例如:

<!--UserMapper.xml-->
<resultMap id="fullUserForDis" type="User">
    <id property="userId" column="user_id"/>
    <result column="name" property="name"/>
    <result column="age" property="age"/>
    <result column="phone" property="phone"/>
    <result column="birthday" property="birthday"/>
    <!--判断age字段-->
    <discriminator javaType="Integer" column="age">
        <case value="1000" resultType="User">
            <collection property="orderList"
                        select="com.star.mybatis.mapper.OrderMapper.listOrderByUserId"
                        ofType="Order"
                        javaType="List"
                        column="user_id">
                <id property="orderId" column="order_id"/>
                <result property="userId" column="user_id"/>
                <result property="orderNo" column="order_no"/>
                <result property="address" column="address"/>
                <result property="amount" column="amount"/>
            </collection>
        </case>
    </discriminator>
</resultMap>

<select id="getFullUserByIdForDis" resultMap="fullUserForDis">
    select * from user where user_id = #{userId}
</select>

如上面的配置所示,使用<discriminator>标签对user表的age字段进行映射。当age字段值为1000时,为User实体的orderList属性关联一个外部的查询Mapper;当age字段值不为1000时,则不做映射处理。

单元测试代码:

@Test
public void testDiscriminator() {
    User user1 = userMapper.getFullUserByIdForDis(1);
    System.out.println(user1.toString());
    System.out.println("-------111----------");
    User user2 = userMapper.getFullUserByIdForDis(2);
    System.out.println(user2.toString());
    System.out.println("--------222---------");
}

控制台打印执行结果:

Opening JDBC Connection
Created connection 271800170.
Setting autocommit to false on JDBC Connection [com.mysql.jdbc.JDBC4Connection@1033576a]
==>  Preparing: select * from user where user_id = ?
==> Parameters: 1(Integer)
<==    Columns: user_id, name, age, phone, birthday
<==        Row: 1, 孙悟空, 1500, 18705464523, 0001-01-01 00:00:00.0
<==      Total: 1
User{userId=1, name='孙悟空', age=1500, phone='18705464523', birthday=Thu Jan 01 00:00:00 CST 1, orderList=null}
-------111----------
==>  Preparing: select * from user where user_id = ?
==> Parameters: 2(Integer)
<==    Columns: user_id, name, age, phone, birthday
<==        Row: 2, 猪八戒, 1000, 15235468789, 0500-03-10 00:00:00.0
====>  Preparing: select * from `order` where user_id = ?
====> Parameters: 2(Integer)
<====    Columns: order_id, user_id, order_no, address, amount
<====        Row: 3, 2, order_03, 广东佛山, 300
<====      Total: 1
<==      Total: 1
User{userId=2, name='猪八戒', age=1000, phone='15235468789', birthday=Fri Mar 10 00:00:00 CST 500, orderList=[Order{orderId=3, userId=2, orderNo='order_03', address='广东佛山', amount=300, user=null}]}
--------222---------

由结果可知,第一次查询由于age字段值为1500,所以没有继续查询关联的order表;第二次查询由于age字段值为1000,触发关联查询order表。

······

本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到