先提交后获取,异步调用?
还记得多线程中的Future和FutureTesk吗?就是先提交后获取的异步调用,线程使用的是Callable接口而不是Runnable接口。异步调用可以有效率地解决这些场景:假设一个计算任务需要耗费很长时间,它的计算结果我们并不急着需要,在等待该计算完成的过程中,我们想充分利用等待的时间,让CPU去做其他的事情,等我们需要这个计算结果时,再去拿这个计算任务的结果返回值。
毫无疑问这种方法可以省去很多不必要的等待时间,提高系统的效率,在MyBatis里有一种延迟加载机制,也类似于这样的思想。在一些关联查询里,我们查询的信息大都不在同一张表中,需要关联多张表查询,但是对于复杂的场景,多表关联查询效率不高,延迟加载机制可以解决这一问题。我们知道单表查询速度一定比多表查询快很多,所以可以通过延迟加载,一开始我们先做单表查询,等需要关联的数据时,再去做关联查询,例如我们在商场数据库中查询顾客的商品清单,并且关联查询这些清单对于的顾客信息,显然要连接商品信息表和顾客表两张表,使用延迟加载,先查询出商品信息表信息,等需要获取顾客信息时,再去单表查询出顾客信息,类似于异步调用的思想,不必一次查询出嵌套关联对象的信息,按需获取,无疑能提高查询的效率。
关于延迟加载,有一些要注意的是:
第一:使用延迟加载时,SQL语句要拆分成多句,例如关联嵌套查询,我们的SQL语句不能写成多表连接,一句完成查询,否则会变成直接加载。
第二:延迟加载对于主对象来说是直接加载的,只有关联对象例如association标签对中关联的对象,才是延迟加载。那么如何控制它延迟加载?如何在我需要时采取查询关联对象的信息?下面会说。
setting配置中的延迟加载和按需加载
延迟加载机制能提高查询效率,但它不是默认开启的,首先要在全局配置文件的settings标签对中配置延迟加载的属性为true。在settings标签中配置setting标签对,有两个属性可以给我们设置,lazyLoadingEnabled属性和aggressiveLazyLoading属性。lazyLoadingEnabled属性控制全局是否使用延迟加载机制,默认是false。aggressiveLazyLoading属性配置是否需要按需加载,默认是false,在设置按需加载为false后,加载了某一个对象,则这个对象的所有属性和关联的对象都会一起被加载,而如果按需加载设置为true,则当使用到某个对象时,才会去加载某个对象的属性。
对于这两个属性不同的配置方式,可以产生出不同的延迟加载效果,主要是由aggressiveLazyLoading按需加载属性决定的,分为侵入式延迟加载和深度延迟加载两种,不同的延迟加载,它们的查询包装类和Mapper映射配置,SQL语句都是一样的,下面我们就先来看这三个配置,假设现在有这么一个需求,查询购物清单表中的信息,以及关联查询购物清单对应的顾客信息:
查询包装类
首先是查询包装类,我们的主对象是购物清单信息:
public class ShoppingCart implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private int cartId; // 购物车id
private int userId; // 用户id
private int productId; // 商品id
private String productName; // 商品名
private int number; //商品数量
private double price; //商品价格
public ShoppingCart() {
// System.out.println("进入ShoppingCart类无参数构造方法");
}
public ShoppingCart(int cartId, int userId, int productId,
String productName, int number, double price) {
// System.out.println("进入ShoppingCart类有参数构造方法");
this.cartId = cartId;
this.userId = userId;
this.productId = productId;
this.productName = productName;
this.number = number;
this.price = price;
//this.user = user;
}
}
// 省略get()和set()方法
关联对象是顾客信息:
public class User implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private Integer id;
private String username; //顾客姓名
private String password; //密码
private String gender; //性别
private String email; //电子邮件
private String province; //省会
private String city; //城市
private Date birthday; //生日
private Integer age; //年龄
public User() {
}
// 构造方法初始化
public User(String username, String password, String gender,
String email, String province, String city, Date birthday) {
super();
this.username = username;
this.password = password;
this.gender = gender;
this.email = email;
this.province = province;
this.city = city;
this.birthday = birthday;
}
}
// 省略get()和set()方法
所以我们可以写一个查询包装类,这个类继承主对象实体类,然后里面嵌套一个关联对象的实例,这样查询包装类就即包含了主对象购物清单信息,也包含了关联对象顾客的信息:
// 延迟加载示例实体类:查询购物清单信息,以及某一清单对应的用户信息
public class ShoppingCartUserLazyLoading extends ShoppingCart implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
private User user; //关联的顾客实例
public ShoppingCartUserLazyLoading() {
}
public ShoppingCartUserLazyLoading(int cartId, int userId, int productId, String productName,
int number, double price, User user) {
super(cartId, userId, productId, productName, number, price);
this.user = user;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}
Mapper映射配置
查询包装类写好后,到配置Mapper映射了,在MyBatis中,association标签和collection标签都具有延迟映射功能,这里用association标签做示例,首先看映射配置:
<!-- 购物清单关联用户,延迟加载测试用例mapper配置 -->
<!-- 延迟加载resultMap -->
<mapper namespace="com.mybatis.po.ShoppingCartUserLazyLoadingTest">
<resultMap id="ShoppingCartUserLazyLoadingMap" type="shoppingCartUserLazyLoading">
<!-- 商品清单信息字段映射配置 -->
<id property="id" column="shoppingCart_id"/>
<result property="cartId" column="cart_id"/>
<result property="userId" column="cartUser_id"/>
<result property="productName" column="product_name"/>
<result property="price" column="product_price"/>
<result property="number" column="product_number"/>
<result property="productId" column="product_id"/>
<!-- association标签延迟加载商品清单信息 -->
<association property="user" column="cartUser_id" javaType="com.mybatis.po.User"
select="searchUserById">
</association>
</resultMap>
前面的字段-属性映射配置没什么问题,关键看association标签里的配置,property指定映射到查询包装类中的属性是user,也就是查询包装类中的顾客类实例,里面封装类该购物清单对象对应的顾客信息,column指定关联查询顾客信息中的哪一列,相当于作为参数传入到关联查询中,这里是关联查询顾客的主键id。select指定延迟加载时执行的SQL语句,它在外部的select标签对中。
SQL查询语句
在Mapper配置的最后,就是SQL语句了,SQL语句十分重要,还记得前面说过,对于延迟加载时的关联嵌套查询,SQL语句要拆分开来,不能一句SQL完成多表连接查询,否则会变成了直接加载。在这里,我们分为主对象查询的SQL语句和关联对象查询的SQL语句:
<!-- 延迟加载查询商品清单SQL语句 -->
<select id="searchLazyLoading" resultMap="ShoppingCartUserLazyLoadingMap">
SELECT
S.cartId as cart_id,
S.productName as product_name,
S.userId as cartUser_id,
S.price as product_price,
S.number as product_number
FROM ShoppingCart S
</select>
<!-- 延迟加载查询用户信息SQL语句 -->
<select id="searchUserById" parameterType="int" resultType="com.mybatis.po.User">
SELECT
U.username,
U.email,
U.city
FROM user U WHERE U.id = #{id}
</select>
</mapper>
在主对象查询的select标签对中,也就是查询商品清单信息,只配置了结果集映射resultMap,没有传入参数,SQL语句中也是没有涉及到多表连接,只查询了ShoppingCart表。而关联对象顾客信息查询的select标签对中,我们传入了商品清单表中的userid参数,完成与ShoppingCart表的连接查询。
包装类,映射关系和SQL语句都配置好后,似乎就到了最后一步,编写测试用例了,前面说到,除直接加载外,延迟加载有侵入式延迟加载和深度延迟加载两种,取决于aggressiveLazyLoading属性的设置不同,下面就来看这两种延迟加载,再分别编写不同的测试用例,看看结果有什么区别。
侵入式延迟加载
侵入式延迟加载的特性就是,访问主对象及主对象里面的属性时,不光会加载主对象(即从数据库中查询主对象的信息),还会一同加载关联对象。来看一个例子,首先是侵入式延迟加载的配置:
<!-- 侵入式延迟加载 -->
<!-- 打开延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 积极加载/按需加载配置 -->
<setting name="aggressiveLazyLoading" value="true"/>
延迟加载属性lazyLoadingEnabled设置为true,aggressiveLazyLoading属性也设置为true,即打开积极加载。
@Test
public void testLazyLoading() throws Exception {
SqlSession sqlSession = dataConn.getSqlSession();
StringBuffer result = new StringBuffer();
// ShoppingCartUserLazyLoading SU = null; // 主对象
// User user = null; // 关联对象
// 获取Mapper代理
ShoppingCartUserLazyLoadingTest lazyLoadingTest
= sqlSession.getMapper(ShoppingCartUserLazyLoadingTest.class);
// 侵入式延迟加载示例
List<ShoppingCartUserLazyLoading>
resultList = lazyLoadingTest.searchLazyLoading();
for(int i=0; i<resultList.size(); i++) {
// 先获得商品清单信息
// SU = resultList.get(i); // 访问主对象里的属性
// result.append("商品清单id: " + SU.getCartId() + "\r\n");
//
// user = SU.getUser();
// result.append("顾客名: " + user.getUsername() + "\r\n");
// result.append("城市: " + user.getCity() + "\r\n");
// result.append("电子邮件: " + user.getEmail() + "\r\n");
System.out.println(result.toString());
result.setLength(0);
}
sqlSession.close();
}
在测试用例中,第9行我们执行主对象的SQL查询,但不加载不访问主对象里面的属性,按照上面侵入式延迟加载特性的说法,此时不会加载主对象中的关联对象信息,来看看执行结果:
从日志信息中看到,只预编译了主对象的SQL语句,关联对象查询顾客信息的SQL语句并没有出现。
如果我们在SQL语句执行完后,访问主对象里面的属性,主对象必然会被加载,而关联对象的信息,虽然关联对象没有被访问,但一样会去数据库查询并加载这个关联对象:
@Test
public void testLazyLoading() throws Exception {
SqlSession sqlSession = dataConn.getSqlSession();
StringBuffer result = new StringBuffer();
ShoppingCartUserLazyLoading SU = null; // 主对象
// User user = null; // 关联对象
// 获取Mapper代理
ShoppingCartUserLazyLoadingTest lazyLoadingTest
= sqlSession.getMapper(ShoppingCartUserLazyLoadingTest.class);
// 侵入式延迟加载示例
List<ShoppingCartUserLazyLoading>
resultList = lazyLoadingTest.searchLazyLoading();
for(int i=0; i<resultList.size(); i++) {
// 先获得商品清单信息
SU = resultList.get(i); // 访问主对象里的属性
result.append("商品清单id: " + SU.getCartId() + "\r\n");
// user = SU.getUser();
// result.append("顾客名: " + user.getUsername() + "\r\n");
// result.append("城市: " + user.getCity() + "\r\n");
// result.append("电子邮件: " + user.getEmail() + "\r\n");
System.out.println(result.toString());
result.setLength(0);
}
sqlSession.close();
}
第13行我们循环拿出主对象的商品清单id并输出,我们访问了主对象,但并没有访问关联对象:
可是从执行输出的日志信息中看到,每一次循环都执行关联对象的SQL语句,访问了数据库,加载关联对象user,即便我们没有访问没有输出关联对象的任何数据。这就是侵入式延迟加载,访问主对象内属性时,会把关联对象一起加载。
深度延迟加载
深度延迟加载不同于侵入式延迟加载,在于它把积极加载换成了按需加载,侵入式延迟加载因为开启了积极加载,所以当访问主对象的属性时,会一起把关联对象也加载进来,即便你没有去访问关联对象的信息。深度延迟加载既然关闭了积极加载,换成了按需加载,显然特性就是,当访问主对象属性时,只加载主,只有当访问关联对象的属性时,才会去加载关联对象。来看示例:
<!-- 深度延迟加载 -->
<!-- 打开延迟加载 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 积极加载/按需加载配置 -->
<setting name="aggressiveLazyLoading" value="false"/>
深度延迟加载配置,把aggressiveLazyLoading属性设置为false,也就是取消积极加载,改为按需加载。接着看测试用例:
@Test
public void testLazyLoading() throws Exception {
SqlSession sqlSession = dataConn.getSqlSession();
StringBuffer result = new StringBuffer();
ShoppingCartUserLazyLoading SU = null; // 主对象
// User user = null; // 关联对象
// 获取Mapper代理
ShoppingCartUserLazyLoadingTest lazyLoadingTest
= sqlSession.getMapper(ShoppingCartUserLazyLoadingTest.class);
// 深度延迟加载示例
List<ShoppingCartUserLazyLoading>
resultList = lazyLoadingTest.searchLazyLoading();
for(int i=0; i<resultList.size(); i++) {
// 先获得商品清单信息
SU = resultList.get(i); // 访问主对象里的属性
result.append("商品清单id: " + SU.getCartId() + "\r\n");
// user = SU.getUser();
// result.append("顾客名: " + user.getUsername() + "\r\n");
// result.append("城市: " + user.getCity() + "\r\n");
// result.append("电子邮件: " + user.getEmail() + "\r\n");
System.out.println(result.toString());
result.setLength(0);
}
sqlSession.close();
}
第13行,首先我们只访问主对象(查询购物清单信息)里的属性,输出商品清单id,但不访问关联对象user的顾客信息,来看查询结果:
因为不访问关联对象的属性,所以只执行了对于主对象加载的查询,也就是查询ShoppingCart表的SQL语句,从日志信息中看到,每一次循环输出商品清单id,但没有预编译执行关联对象查询顾客信息的SQL语句。而当我们需要访问到关联对象user了,MyBatis才会去执行数据库查询:
@Test
public void testLazyLoading() throws Exception {
SqlSession sqlSession = dataConn.getSqlSession();
StringBuffer result = new StringBuffer();
ShoppingCartUserLazyLoading SU = null; // 主对象
User user = null; // 关联对象
// 获取Mapper代理
ShoppingCartUserLazyLoadingTest lazyLoadingTest
= sqlSession.getMapper(ShoppingCartUserLazyLoadingTest.class);
// 深度延迟加载示例
List<ShoppingCartUserLazyLoading>
resultList = lazyLoadingTest.searchLazyLoading();
for(int i=0; i<resultList.size(); i++) {
// 先获得商品清单信息
SU = resultList.get(i); // 访问主对象里的属性
result.append("商品清单id: " + SU.getCartId() + "\r\n");
user = SU.getUser();
result.append("顾客名: " + user.getUsername() + "\r\n");
result.append("城市: " + user.getCity() + "\r\n");
result.append("电子邮件: " + user.getEmail() + "\r\n");
System.out.println(result.toString());
result.setLength(0);
}
sqlSession.close();
}
看示例第16行,我们需要访问到关联对象user,并且输出顾客的一些信息,显然在每一次循环时,都会去查询数据库,加载关联对象:
完整代码已上传GitHub:
https://github.com/justinzengtm/SSM-Framework/tree/master/MyBatis