Spring 引导数据 JPA 执行延迟加载 - 不是在关系上而是在加载的实体上?

Spring Boot Data JPA doing lazy loading - not on a relation but on the loaded entity?

我刚刚发现了一些我无法用任何其他方式描述的事情,只能说是奇怪。

我有一项服务可以执行此操作:

  1. 它获得了客户的外部标识符
  2. 它查找客户的内部 ID
  3. 然后加载 returns 客户

我正在使用可选项,因为有可能无法解析外部标识符。

@Transactional(readOnly = true)
public Optional<Customer> getCustomerByExternalReference(String externalId, ReferenceContext referenceContext) {
    return externalIdMappingService.resolve(externalId, referenceContext, InternalEntityType.CUSTOMER)
        .map(x->new CustomerId(x.getTarget()))
        .map(customerRepository::getById);
}

这里值得注意的是:externalIdMappingRepository.resolve returns 一个 Optional<ExternalIdReference> 对象。如果存在,我会尝试将其映射到我随后从数据库中查找的客户。 customerRepository 是一个常规的 spring 数据 JPA 存储库(下面的源代码)

但是,当我尝试在服务外部访问客户的属性时,出现如下异常:

org.hibernate.LazyInitializationException: could not initialize proxy [Customer#Customer$CustomerId@3e] - no Session
    at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:176)
    at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:322)
    at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:45)
    at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:95)
    at Customer$HibernateProxy$R0X59vMR.getIdName(Unknown Source)
    at CustomerApiModel.<init>(CustomerApiModel.java:27)

我理解这意味着 Hibernate 决定延迟加载该实体。一旦超出服务的事务边界,它就无法再为该对象加载数据。

我的问题是:为什么 Hibernate/Spring 数据尝试延迟获取策略,而我基本上只是通过 ID 从 Spring 数据加载特定对象存储库以及我如何以正确的方式禁用此行为。

我知道有几个解决方法可以解决这个问题(例如允许休眠随意打开会话,或访问服务内该对象的属性)。 我不想进行此类修复。我想了解这个问题并希望确保延迟提取只在应该发生的时候发生

这是给客户的代码(只是我认为有帮助的部分)

@Entity
@Table(name="customer")
@Getter
public class Customer  {
    @EmbeddedId
    private CustomerId id;

    @Embeddable
    @NoArgsConstructor
    @AllArgsConstructor
    @EqualsAndHashCode
    public static class CustomerId implements Serializable {

        private long id;

        public long asLong() {
            return id;
        }

    }
}

这里是存储库的源代码:

public interface CustomerRepository extends Repository<Customer, CustomerId> {
    List<Customer> findAll();       
    Customer getById(CustomerId id);
    Optional<Customer> findOneById(CustomerId id);
    Optional<Customer> findOneByIdName(String idName);
}

通过在您的 CustomerRepository 接口中声明方法 Customer getById(CustomerId id);,您选择让您的存储库有选择地公开具有来自 相同签名 的相应方法标准 spring-data 存储库方法,如 Repository java 文档所述:

Domain repositories extending this interface can selectively expose CRUD methods by simply declaring methods of the same signature as those declared in CrudRepository.

与文档所说的不同,这还包括来自 JpaRepository.

的方法

Customer getById(CustomerId id); 的情况下,您因此调用具有相同签名的 JpaRepository 方法:T getOne(ID id);,它只调用 EntityManager#getReference ,正如它的文档所建议的:

[...] Returns a reference to the entity with the given identifier. Depending on how the JPA persistence provider is implemented this is very likely to always return an instance and throw an {@link javax.persistence.EntityNotFoundException} on first access. Some of them will reject invalid identifiers immediately. [...]
@see EntityManager#getReference(Class, Object) for details on when an exception is thrown.

当调用 EntityManager#getReference 时,Hibernate 首先 return 是实体的 non-initialized 代理,根本不执行任何 SQL 语句,这就是为什么你的方法只 return 是 non-initialized 实体。

要解决此问题,您可以按如下方式更改服务逻辑:

@Transactional(readOnly = true)
public Optional<Customer> getCustomerByExternalReference(String externalId, ReferenceContext referenceContext) {
  return externalIdMappingService.resolve(externalId, referenceContext, InternalEntityType.CUSTOMER)
    .map(x->new CustomerId(x.getTarget()))
    .map(id -> customerRepository.findOneById(id).get()); // <-- changed call
}

这样,spring-data 会调用 CrudRepository#findById,它会在内部调用 EntityManager#find,因此 return 是一个初始化的实体(如果 none 在数据库中找到)。

相关:
When use getOne and findOne methods Spring Data JPA
(在同一事务中使用getOnefindById时注意)