频道
bg

Hibernate one-to-one 懒加载的问题

coding五月 21, 20161mins
Hibernate

问题H2

Hibernate在使用OneToOne的反向关联或者使用主键关联(PrimaryKeyJoinColumn)时,会出来懒加载不起的作用的情况。究其原因,Hibernate的能够进行懒加载的前提是返回的关联对象是个代理对象。如果Hibernate不能确定关联的对象是否为空,那么他们不能直接返回代理对象,因为代理对象本身就是不为空的,它不得不去检查关联对象是否存在。但是用SELECT语句去检查存在性,还不如索性直接把查询结果返回,这也就导致了懒加载的失效。

上面的两种情况都是无法确定关联对象是否存在的

  • 反向关联 由于关联的外键并不存在于当前表,而是在关联表,必须把当前记录的主键作为条件去关联表的外键中去匹配才能确定关联对象是否存在
  • 主键关联 由于关联的对象表的主键是使用的当前表的主键,单单查看当前表也无法确定关联对象的存在性

所以也就导致这两种情况下的关联属性的懒加载是无法生效的。

解决H2

强制存在H2

如果在映射文件中就明确定义出来关联对象不可能为空,那么Hibernate就没有必要去检查它的存在性,直接返回代理对象即可。 当前前提是业务的角度来说关联对象确实不可能为空。

使用OneToMany来替代OneToOneH2

使用假的OneToMany来替代OneToOne,实际使用的时候始终取集合的第一个元素。因为Many端永远不会返回空,最多返回空集合。但是这个方式会对JPQL/HQL产生一定的影响

使用字节码织入H2

不使用代理对象,而是使用字节码织入,最终调用关联对象的任何方法时才会触发懒加载。对于字节码的织入有编译时织入和运行时织入两种方式。编译时织入可以通过Hibernate提供的ant工具来实现。运行时织入则依赖的JavaEE环境或者使用Spring的运行时织入技术。

无论哪种情况,最终都需要使用@LazyToOne(LazyToOneOption.NO_PROXY) 注解来显示的告诉框架不适用代理对象,以及把Hibernate的hibernate.ejb.use_class_enhancer参数设置为true。

NOTE: Hibernate 5.2之后增加了字节码织入的功能,将USE_CLASS_ENHANCER分为

  • enableDirtyTracking
  • enableLazyInitialization
  • enableAssociationManagement 因此,还需要设置hibernate.enhancer.enableLazyInitialization参数为true

启用Spring’s LTWH3

Spring的LTW的核心组件是LoadTimeWeaver接口,LoadTimeWeaver的职责是负责在运行时添加一个和多个java.lang.instrument.ClassFileTransformersClassLoader中。

要启用Spring的LTW支持,首先需要配置LoadTimeWeaver

bash

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

上述的配置会定义并注册一系列的LTW相关的Bean。默认的LoadTimeWeaver的实现类DefaultContextLoadTimeWeaver,它是一个包装类,他会装饰自动检测到的LoadTimeWeaver的实现类。

  • 对于WebLogic, WebSphere, Resin, GlassFish, JBoss这些服务器的最近的一些版本提供的类加载器是支持本地织入的,所以只需要直接激活LTW即可。
  • 对于Tomcat6,默认的类加载器不支持class transformation,需要使用Spring提供的类加载器TomcatInstrumentableClassLoader

    bash

    <Context path="/myWebApp" docBase="/my/webApp/location">
    <Loader
    loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
    </Context>
  • 对于普通的Java应用,JDK agent是唯一的解决办法。Spring提供了InstrumentationLoadTimeWeaver,但是需要Spring特定的VM agent。

    bash

    -javaagent:/path/to/org.springframework.instrument-{version}.jar

给EntityManagerFactory设置LoadTimeWeaverH3

Spring 4.3之前可能需要手动的设置LoadTimeWeaver,这里是相关的问题。手动设置的方法是调用LocalContainerEntityManagerFactoryBean#setLoadTimeWeaver方法。对于Spring Boot,可以用下面的方式来设置

bash

@Bean
@ConditionalOnMissingBean
public EntityManagerFactoryBuilder entityManagerFactoryBuilder(
JpaVendorAdapter jpaVendorAdapter) {
EntityManagerFactoryBuilder builder = new EntityManagerFactoryBuilder(
jpaVendorAdapter, this.jpaProperties.getProperties(),
this.persistenceUnitManager);
builder.setCallback(getVendorCallback());
return builder;
}
protected EntityManagerFactoryBuilder.EntityManagerFactoryBeanCallback getVendorCallback() {
return factory -> factory.setLoadTimeWeaver(loadTimeWeaver);
}

使用bytecode instrumentation后对Jackson带来的影响H3

使用织入后,实体对象会包含额外的FieldHandler属性,Jackson进行序列化时会出错,需要忽略掉这行属性。

bash

@JsonIgnoreType
private class MixInForFieldInterceptor {
}
objectMapper.addMixIn(FieldHandler.class, MixInForFieldInterceptor.class);

使用@MapIdsH2

针对一对一的情况,更好的方案是使用共同的主键,即主键同时也是外键。这样首先在数据库方面脚减小的索引,其次可以使用@MapIds来解决Lazy失效的问题。 @MapsId

The MapsId annotation ask Hibernate to copy the identifier from another associated entity. In the Hibernate jargon, it is known as a foreign generator but the JPA mapping reads better and is encouraged

使用@MapsId后,不需要自己维护主键,在插入的时候会拷贝关联对象的主键;懒加载时由于自己的主键也是引用的对象的主键,需要产生额外的查询去获取Lazy对象的主键。 同时这里给出和@MapsId关联的@PrimaryKeyJoinColumn的定义

The PrimaryKeyJoinColumn annotation does say that the primary key of the entity is used as the foreign key value to the associated entity.

@PrimaryKeyJoinColumn是只读的(readable=false, updatable=fale),它相当于@JoinColumn+@Id

评论


新的评论

匹配您的Gravatar头像

Joen Yu

@2022 JoenYu, all rights reserved. Made with love.