Jpa对Lob型字段进行延迟加载

为什么需要延迟加载

在某些情况下,我们在设计实体的时候,可能会将一些大字段设计到实体内部,比如一些超长的说明文字等。
以下时一个实体示例

@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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容