延迟加载:侵入式延迟和深度延迟

先提交后获取,异步调用?

还记得多线程中的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

发布了97 篇原创文章 · 获赞 71 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/justinzengTM/article/details/100189481