Spring 引导数据 JPA 执行延迟加载 - 不是在关系上而是在加载的实体上?
Spring Boot Data JPA doing lazy loading - not on a relation but on the loaded entity?
我刚刚发现了一些我无法用任何其他方式描述的事情,只能说是奇怪。
我有一项服务可以执行此操作:
- 它获得了客户的外部标识符
- 它查找客户的内部 ID
- 然后加载 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
(在同一事务中使用getOne
和findById
时注意)
我刚刚发现了一些我无法用任何其他方式描述的事情,只能说是奇怪。
我有一项服务可以执行此操作:
- 它获得了客户的外部标识符
- 它查找客户的内部 ID
- 然后加载 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
getOne
和findById
时注意)