英文:
How to implement lazy loading in Hibernate where the model is created in a different transaction to where the properties are used
问题
我知道这个问题之前已经被问过很多次,但是我没有找到一个确切的答案来解决这个特定的情况:
我有一个控制器类处理 @RequestMapping(params = "type", method = RequestMethod.GET) onLoad(...)
和 @RequestMapping(method = RequestMethod.POST) onSubmit(...)
分别在两个不同的方法中。
这些方法然后调用
@Transactional()
void load(TypeOfForm form, Long id, TypeOfSessionParams sessionParams);
和
@Transactional()
void store(TypeOfForm form);
在逻辑类中分别调用。
load 方法会到 dao 中获取一个数据库中的 Model 实例;但是该模型包含以下内容:
@OneToMany(mappedBy = "company", cascade = CascadeType.PERSIST)
public Set<CompanyLocations> getCompanyLocations() {
return companyLocations;
}
在 store()
方法调用之前,不会调用它。由于它是延迟加载,我得到了以下错误:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: org.orgName.modelName.companyLocations, could not initialize proxy - no Session
我在周围看到的典型答案有:
- 添加
@Transactional()
注解
我已经在 load()
和 store()
方法上添加了这个注解。我是不是做错了什么?因为似乎没有帮助(我认为是因为 getCompanyLocations
方法是在 store()
中被调用的,在这个方法中的事务和最初创建模型对象时的事务不同)。
- 在模型属性的 get 方法中添加
, fetch = FetchType.EAGER
这可能会起作用,但我不确定,因为加载页面的时间变得很长,我放弃了它,所以这不是一个有效的解决方案。
- 使用 OpenSessionInViewInterceptor/Filter
实际上,应用程序目前都在使用,据我所见:
在 applicationContext-hibernate.xml 文件中:
<bean id="openSessionInViewInterceptor"
class="org.springframework.orm.hibernate4.support.OpenSessionInViewInterceptor">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="transactionInterceptor" class="org.orgName.util.TransactionalWebRequestInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttribute" value="PROPAGATION_REQUIRES_NEW"/>
</bean>
在 web.xml 中:
<filter>
<filter-name>open-session-in-view</filter-name>
<filter-class>org.springframework.orm.hibernate4.support.OpenSessionInViewFilter</filter-class>
<init-param>
<param-name>sessionFactoryBeanName</param-name>
<param-value>sessionFactory</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>open-session-in-view</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
- 在 sessionFactory 属性中添加
<prop key="hibernate.enable_lazy_load_no_trans">true</prop>
这是唯一有效的解决方案,但是我在任何地方都看到说这是一个非常糟糕的主意。
那么,什么是正确的做法呢?
如果有帮助的话,这是 TransactionalWebRequestInterceptor
类:
public class TransactionalWebRequestInterceptor implements WebRequestInterceptor, TransactionDao {
// ... 这里是类的定义和方法,省略了具体内容 ...
}
英文:
I know this has been asked many, MANY times before - but I can't see an exact answer to this particular situation:
I have a controller class which deals with @RequestMapping(params = "type", method = RequestMethod.GET) onLoad(...)
and @RequestMapping(method = RequestMethod.POST) onSubmit(...)
in two separate methods.
Those methods then call
@Transactional()
void load(TypeOfForm form, Long id, TypeOfSessionParams sessionParams);
and
@Transactional()
void store(TypeOfForm form);
on the logic class respectively.
The load method goes off to the dao and gets an instance of a Model from the database; but the model contains the following:
@OneToMany(mappedBy = "company", cascade = CascadeType.PERSIST)
public Set<CompanyLocations> getCompanyLocations() {
return companyLocations;}
This isn't called until the store()
method. Because it's lazy loading, I'm getting:
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: org.orgName.modelName.companyLocations, could not initialize proxy - no Session
The typical answers I see around are:
- add
@Transactional()
annotation
I have this already, on the load() and store() methods. Am I doing this wrong? Because it doesn't seem to help (I think because the getCompanyLocations method is called in store(), which is not the same transaction as when the model object was initially created)
- add
, fetch = FetchType.EAGER
to the model property's get method
This may work, I don't know - it made the page load time so long that I gave up with it - so it's not a valid solution anyway.
- Use an OpenSessionInViewInterceptor/Filter
Actually, the application currently uses both, as far as I can see:
In applicationContext-hibernate.xml file:
<bean id="openSessionInViewInterceptor"
class="org.springframework.orm.hibernate4.support.OpenSessionInViewInterceptor">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="transactionInterceptor" class="org.orgName.util.TransactionalWebRequestInterceptor">
<property name="transactionManager" ref="transactionManager"/>
<property name="transactionAttribute" value="PROPAGATION_REQUIRES_NEW"/>
</bean>
In web.xml:
<filter>
<filter-name>open-session-in-view</filter-name>
<filter-class>org.springframework.orm.hibernate4.support.OpenSessionInViewFilter</filter-class>
<init-param>
<param-name>sessionFactoryBeanName</param-name>
<param-value>sessionFactory</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>open-session-in-view</filter-name>
<url-pattern>*.jsp</url-pattern>
</filter-mapping>
- add
<prop key="hibernate.enable_lazy_load_no_trans">true</prop>
to my sessionFactory properties
This is the only solution that works, but everywhere I've looked says it's a REALLY BAD idea.
So, what would be the correct way to do this?
If it helps, here's the TransactionalWebRequestInterceptor
class:
public class TransactionalWebRequestInterceptor implements WebRequestInterceptor, TransactionDao {
private static final Log log = LogFactory.getLog(TransactionalWebRequestInterceptor.class);
private PlatformTransactionManager transactionManager;
private TransactionAttribute transactionAttribute;
private ThreadLocal<TransactionStatus> threadTransactionStatus = new ThreadLocal<TransactionStatus>();
public void setTransactionManager(PlatformTransactionManager transactionManager) {
this.transactionManager = transactionManager;
}
public void setTransactionAttribute(TransactionAttribute transactionAttribute) {
this.transactionAttribute = transactionAttribute;
}
public void preHandle(WebRequest request) throws Exception {
log.debug("preHandle");
beginTransaction();
}
public void postHandle(WebRequest request, ModelMap model) throws Exception {
log.debug("postHandle");
commitTransactionIfNoErrors();
}
public void afterCompletion(WebRequest request, Exception e) throws Exception {
log.debug("afterCompletion");
rollBackTransactionIfInProgress();
}
public void setRollbackOnly() {
log.debug("setRollbackOnly");
TransactionStatus status = threadTransactionStatus.get();
if (status == null) {
log.debug("rollback requested but no transaction in progress");
return;
}
status.setRollbackOnly();
}
private void beginTransaction() {
if (threadTransactionStatus.get() != null)
throw new IllegalStateException("transaction already in progress");
TransactionStatus status = transactionManager.getTransaction(transactionAttribute);
threadTransactionStatus.set(status);
log.debug("transaction begun");
}
private void commitTransactionIfNoErrors() {
TransactionStatus status = threadTransactionStatus.get();
if (status == null)
throw new IllegalStateException("no transaction in progress");
if (status.isRollbackOnly()) {
log.debug("commitTransactionIfNoErrors: transaction is rollback-only; not committing");
return;
}
UserAttributes.getCurrent().getUser().setIsTransactionCompleted(true);
threadTransactionStatus.set(null);
transactionManager.commit(status);
log.debug("transaction committed");
}
private void rollBackTransactionIfInProgress() {
TransactionStatus status = threadTransactionStatus.get();
if (status == null) {
log.debug("rollBackTransactionIfInProgress: no transaction in progress");
return;
}
threadTransactionStatus.set(null);
transactionManager.rollback(status);
log.debug("transaction rolled back");
}
}
答案1
得分: 0
你应该在同一个事务中进行加载和存储操作,因此调用加载和存储的方法都应该标记为 @Transactional
。
懒加载问题通常通过使用专门的DTO模型来解决,该模型仅获取所需的数据。关于一些解决方案以及它们的优缺点,我在这里写了一些内容:
- https://blazebit.com/blog/2016/getting-started-with-blaze-persistence-entity-views.html
- https://blazebit.com/blog/2017/entity-view-subview-as-rescue-for-lazyinitializationexception.html
如果你有两个请求,那么你有两个选项。在存储方法中使用EntityManager.merge,将状态按原样应用于数据库,或者使用EntityManager.find加载现有数据,并在存储方法的事务中将更改后的数据应用于该实例。
英文:
You should do the loading and the storing in the same transaction, so whatever method calls load and store should be @Transactional
.
Lazy loading issues are usually solved by using a dedicated DTO model that fetches exactly what is needed. I wrote about some solutions and their pros and cons here:
- https://blazebit.com/blog/2016/getting-started-with-blaze-persistence-entity-views.html
- https://blazebit.com/blog/2017/entity-view-subview-as-rescue-for-lazyinitializationexception.html
If you have two requests, then you have two options. Use EntityManager.merge in store to apply the state as-is to the DB or use EntityManager.find to load the existing data and apply the changed data onto that instance within the transaction of the store method.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论