文章目录
前言
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源码深度解析