文章目录
- 前言
- 第十章 MyBatis级联映射与懒加载
- 10.1 MyBatis级联映射的使用
- 10.1.1 简单查询
- 10.1.1.1 数据准备
- 10.1.1.2 编写实体类
- 10.1.1.3 编写Mapper接口及XML配置文件
- 10.1.1.4 编写MyBatis主配置文件
- 10.1.1.5 编写单元测试
- 10.1.2 一对多级联映射
- 10.1.2.1 外部Mapper方式
- 10.1.2.2 JOIN子句方式
- 10.1.3 一对一关联映射
- 10.1.3.1 外部Mapper方式
- 10.1.3.2 JOIN子句方式
- 10.1.4 Discriminator
前言
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;@Beforepublic 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);}@Testpublic 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 uleft join `order` o on u.user_id = o.user_idwhere 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` oleft join user u on o.user_id = u.user_idwhere 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源码深度解析