Search code examples
javaspringhibernatetransactionslazy-loading

How to implement lazy loading in Hibernate where the model is created in a different transaction to where the properties are used


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:

  1. 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)

  1. 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.

  1. 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>
  1. 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");
    }
}

Solution

  • 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:

    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.