为什么需要延迟加载
在某些情况下,我们在设计实体的时候,可能会将一些大字段设计到实体内部,比如一些超长的说明文字等。
以下时一个实体示例
@Getter
@Setter
@Entity(name = "user_")
public class User extends AbstractPersistable<Long> {
private String name;
@Lob
private String description;
}
name 是一个长度受限的字符串,descript是一个大字符串。
当前端页面需要一个列表,并且这个列表不需要展示descript时,我们通过设计数据传输对象,通过减少数据传输对象中的数据属性来规避服务器到客户端直接的网络流量。
但在使用JPA的时候,当我们获取某个对象时,总会向数据库服务器发送完整的sql语句,如果这个对象有Lob类型的字段,并且正巧里面存入了大量数据的话,就会导致发送大量的查询流量。但这些查询流量我们可能在某次请求中并不需要。
比如我们在某个地方调用了相应的Repository.findAll()方法,返回用户的列表。示例代码如下
@Test
public void loadOnlyName() {
List<User> users = userRepository.findAll();
users.forEach(i -> {
System.out.println(i.getName());
});
}
在代码中我们仅仅用到了对象的name属性。
这是相应的SQL语句
select user0_.id as id1_0_, user0_.description as descript2_0_, user0_.name as name3_0_ from user_ user0_
如果恰好这个字段的内容保存的比较多,可能简单的几个对象就可以吃爆你的内存,这应该不是我们想要的。我们需要当且仅当需要相应的字段内容的时候,才去查询相应的字段。
如何实现延迟加载
通常的解决方法是将单独的大字段文本单独保存为一个表,这里保存对相关字段的引用即可。
但在JPA的规范中,其实包含了相关的解决方案,可以让指定的字段延迟加载。
JPA规范中的字段延迟加载是通过@Basic(fetch = FetchType.LAZY)来规定的。
我们可以通过改造实体定义,并在属性上增加相应的注解让对应的字段延迟加载,改造后的代码如下。
@Getter
@Setter
@Entity(name = "user_")
public class User extends AbstractPersistable<Long> {
private String name;
@Lob
@Basic(fetch = FetchType.LAZY)
private String description;
}
但JPA规范明确规定,这个标准是可选的。也就是说,在各个JPA实现中,并不一定要对其进行支持。而且最常用的实现Hibernate默认情况下也是不对其进行处理的。要使这个注解生效,需要对整个项目进行配置。
以maven项目为例,需要增加构建配置,增加Hiernate字节码增强。具体的是在<build></build>中增加相应的配置。
<build>
<plugins>
<plugin>
<!--引入相应的组件-->
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<!--版本号要和项目中使用的hibernate版本一致-->
<version>5.4.9.Final</version>
<executions>
<execution>
<configuration>
<!--启用简单字段的延迟加载-->
<enableLazyInitialization>true</enableLazyInitialization>
</configuration>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
...other plugin
</plugins>
</build>
重新编译项目(因为这是对字节码的增强,所以必须重新构建项目,生成新的字节码才能生效)并运行上面的测试。观察SQL语句的输出
select user0_.id as id1_0_, user0_.name as name3_0_ from user_ user0_
此时输出的SQL语句就变为了只查询name字段。
当我们要使用对象的descript属性时,才会发出查询name的语句。示例:
@Test
// 这里必须加注解,因为这里会发送多条SQL语句,否则第二次延迟查询会丢失连接
@Transactional
public void loadAll() {
List<User> users = userRepository.findAll();
users.forEach(i -> {
System.out.println("username=[" + i.getName() + "]");
System.out.println("description=[" + i.getDescription() + "]");
});
}
运行代码片段,观察代码的输出
Hibernate: select user0_.id as id1_0_, user0_.name as name3_0_ from user_ user0_
username=[1]
Hibernate: select user_.description as description2_0_ from user_ user_ where user_.id=?
description=[descript1]
...more
当代码执行到findAll()时,会执行SQL语句,但不会查询descript字段,
当程序运行到 user.getDescript()时,会先发送SQL语句,查询该对象的descript属性。这就达到了延迟加载的效果。
进一步懒加载
比如我们重新改造上面的示例,给字段增加属性,并且设置为懒加载。重新运行测试代码。增加属性后的代码如下:
@Getter
@Setter
@Entity(name = "user_")
public class User extends AbstractPersistable<Long> {
private String name;
@Lob
@Basic(fetch = FetchType.LAZY)
private String description;
// 增加一个属性
@Lob
@Basic(fetch = FetchType.LAZY)
private String memo;
}
运行,观察代码SQL输出
Hibernate: select user0_.id as id1_0_, user0_.name as name4_0_ from user_ user0_
username=[1]
Hibernate: select user_.description as descript2_0_, user_.memo as memo3_0_ from user_ user_ where user_.id=?
descript=[descript1]
我们再代码中,并未使用memo属性,但第二条SQL在查询的时候,仍然会查询memo属性。
JPA规范本身并没有对该种情况给出相应的解决方法,但Hibernate实现提供了相应的解决方案。即@LazyGroup注解(注意:LazyGroup是Hibernate提供注解,如果使用其它JPA实现,请查询相关文档)。
LazyGoup提供了懒加载分组功能,即将相关的一组属性进行分组,当试图获取分组中的某个属性时,会同时加载该分组的其它属性。如果不希望某个属性和其它属性同时加载,将两个属性放在不同的组即可。
修改后的代码如下
@Getter
@Setter
@Entity(name = "user_")
public class User extends AbstractPersistable<Long> {
private String name;
@Lob
@Basic(fetch = FetchType.LAZY)
private String description;
@Lob
@Basic(fetch = FetchType.LAZY)
@LazyGroup("memo")
private String memo;
}
这时,description字段和memo字段就会分开单独加载了。
注意事项
在使用延迟加载的时候,会引发1+N问题。在使用的时候需要特别注意,设计不善的延迟加载,会引发性能问题。需要特别注意。
Gradle也可以使用相应的插件,详细信息参考Hibernate官方资料
完整的示例代码下载https://github.com/ldwqh0/jpa-lazy-lob.git