使用JPA和Hibernate延迟加载实体属性的最佳方法 - Vlad Mihalcea

19-02-21 banq
              

获取实体时,也会加载所有属性。这是因为每个隐式使用@Basic实体属性提取策略都默认FetchType.EAGER。

但是,属性获取策略可以设置为FetchType.LAZY,在这种情况下,实体属性只有在第一次访问时才加载,通过select语言的执行。

@Basic(fetch = FetchType.LAZY)

仅此配置是不够的,因为Hibernate需要字节码检测来拦截属性访问请求并按需发出select语句。

使用Maven字节码增强插件时,enableLazyInitialization必须将配置属性设置true为以下示例中所示:

<plugin>
    <groupId>org.hibernate.orm.tooling</groupId>
    <artifactId>hibernate-enhance-maven-plugin</artifactId>
    <version>${hibernate.version}</version>
    <executions>
        <execution>
            <configuration>
                <failOnError>true</failOnError>
                <enableLazyInitialization>true</enableLazyInitialization>
            </configuration>
            <goals>
                <goal>enhance</goal>
            </goals>
        </execution>
    </executions>
</plugin>

有了这个配置,所有JPA实体类都将使用延迟属性获取进行检测。此过程发生在构建时,就在从关联的源文件编译实体类之后。

与存储大量数据的列类型处理当属性延迟抓取机构是非常有用的(例如BLOB,CLOB,VARBINARY)。这样,可以在不自动从基础大型列类型中加载数据的情况下获取实体,从而提高性能。

演示

为了演示属性延迟提取的工作原理,以下示例将使用Attachment可以存储任何媒体类型的实体(例如PNG,PDF,MPEG)。

@Entity @Table(name = "attachment")
public class Attachment {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String name;
 
    @Enumerated
    @Column(name = "media_type")
    private MediaType mediaType;
 
    @Lob
    @Basic(fetch = FetchType.LAZY)
    private byte[] content;
 
    //Getters and setters omitted for brevity
}

在每个实体负载上都要急切地获取诸如实体标识符,名称或媒体类型之类的属性。另一方面,只有在被应用程序代码访问时,才应该懒惰地获取媒体文件内容。

在检测Attachment实体后,类字节码更改如下:

@Transient
private transient PersistentAttributeInterceptor
    $$_hibernate_attributeInterceptor;
 
public byte[] getContent() {
    return $$_hibernate_read_content();
}
 
public byte[] $$_hibernate_read_content() {
    if ($$_hibernate_attributeInterceptor != null) {
        this.content = ((byte[])
            $$_hibernate_attributeInterceptor.readObject(
                this, "content", this.content));
    }
    return this.content;
}

执行以下测试用例时:

Attachment book = entityManager.find(
    Attachment.class, bookId);
 
LOGGER.debug("Fetched book: {}", book.getName());
 
assertArrayEquals(
    Files.readAllBytes(bookFilePath),
    book.getContent()
);

Hibernate生成以下SQL查询:

SELECT a.id AS id1_0_0_,
       a.media_type AS media_ty3_0_0_,
       a.name AS name4_0_0_
FROM   attachment a
WHERE  a.id = 1
 
-- Fetched book: High-Performance Java Persistence
 
SELECT a.content AS content2_0_
FROM   attachment a
WHERE  a.id = 1

因为它标记有FetchType.LAZY注释并且启用了延迟提取字节码增强,所以content不会提取该列以及初始化Attachment实体的所有其他列。只有当数据访问层尝试访问该content属性时,Hibernate才会发出辅助选择以加载此属性。

就像FetchType.LAZY关联一样,这种技术很容易出现N + 1个查询问题,因此建议谨慎行事。字节码增强机制的一个细微缺点是所有实体属性,而不仅仅是标记有FetchType.LAZY注释的属性,将被转换,如前所述。

获取子实体

另一种避免加载相当大的表列的方法是将多个子实体映射到同一个数据库表。

比如BaseAttachment有两个子类Attachment实体和AttachmentSummary。

无论是Attachment实体和AttachmentSummary子实体继承BaseAttachment所有公共属性

@MappedSuperclass
public class BaseAttachment {
 
    @Id
    @GeneratedValue
    private Long id;
 
    private String name;
 
    @Enumerated
    @Column(name = "media_type")
    private MediaType mediaType;
 
    //Getters and setters omitted for brevity
}

虽然AttachmentSummary扩展BaseAttachment而没有声明任何新属性:

@Entity @Table(name = "attachment")
public class AttachmentSummary
    extends BaseAttachment {}

Attachment实体继承超类BaseAttachment所有基本属性并映射content列。

@Entity @Table(name = "attachment")
public class Attachment
    extends BaseAttachment {
 
    @Lob
    private byte[] content;
 
    //Getters and setters omitted for brevity
}

当抓取AttachmentSummary子实体时:

AttachmentSummary bookSummary = entityManager.find(
    AttachmentSummary.class, bookId);

产生SQL:

SELECT a.id as id1_0_0_,
       a.media_type as media_ty2_0_0_,
       a.name as name3_0_0_
FROM attachment a
WHERE  a.id = 1

当抓取Attachment 实体时:

Attachment book = entityManager.find(
    Attachment.class, bookId);

Hibernate将从底层数据库表中获取所有列:

SELECT a.id as id1_0_0_,
       a.media_type as media_ty2_0_0_,
       a.name as name3_0_0_,
       a.content as content4_0_0_
FROM attachment a
WHERE  a.id = 1

结论

对于延迟获取实体属性,您可以使用字节码增强或子实体两种方式。

虽然字节码检测允许您每个表只使用一个实体,但子实体更灵活,甚至可以提供更好的性能,因为它们在读取实体属性时不涉及拦截器调用。

子实体方式其实就是将由大数据如图片或文件的对象和文字小数据的对象分开。

在读取数据时,子实体与DTO投影非常相似。但是,与DTO投影不同,子实体可以跟踪状态更改并将它们传播到数据库。