LazyInitializationException en JPA e Hibernate

Sé que esta pregunta se ha formulado varias veces aquí y en Internet, y he leído muchas de esas respuestas, pero aún no entiendo la forma correcta de resolver este problema. Estoy experimentando con Spring MVC y JPA y cada vez que accedo a una propiedad cargada perezosamente obtengo una LazyInitializationException.

Aquí está algo del código con el que estoy experimentando:

@Repository
public class MyDAO {
    private static final Logger logger = LoggerFactory.getLogger(MyDAO.class);

    @PersistenceContext
    private EntityManager em;

    @Transactional
    public void logDOI() {
        DOI myDOI = em.find(DOI.class, Long.valueOf(1));

        // This line gives the expected output
        logger.info("Fetched DOI: " + myDOI.getDoiKey());

        // This line throws the LazyInitalizationException
        for(DOIMembership m : myDOI.getDoiMemberships()) {
            logger.info("Got DOI Membership id: " + m.getId());
        }
    }
}

La entidad a la que estoy accediendo:

@Entity
@Table(name="DOI")
public class DOI implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @SequenceGenerator(name="DOI_ID_GENERATOR", sequenceName="DOI_SEQ")
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="DOI_ID_GENERATOR")
    private long id;

    // Other properties omitted

    //bi-directional many-to-one association to DOIMembership
    @OneToMany(mappedBy="doi", fetch=FetchType.LAZY)
    private Set<DOIMembership> doiMemberships;

    public DOI() {
    }

    public long getId() {
        return this.id;
    }

    public void setId(long id) {
        this.id = id;
    }

    // Other Accessors Omitted

}

La entidad referenciada por el DOI

@Entity
@Table(name="DOI_MEMBERSHIP")
public class DOIMembership implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @SequenceGenerator(name="DOI_MEMBERSHIP_ID_GENERATOR", sequenceName="DOI_MEMBERSHIP_SEQ")
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="DOI_MEMBERSHIP_ID_GENERATOR")
    private long id;

    //bi-directional many-to-one association to DOI
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name="DOI_ID")
    private DOI doi;

    @Column(name="GROUP_ID")
    private BigDecimal groupId;

    @Column(name="DATA_SET")
    private BigDecimal dataSetId;

    public DOIMembership() {
    }

    public BigDecimal getGroupId() {
        return groupId;
    }

    public BigDecimal getDataSetId() {
        return dataSetId;
    }

    public void setDataSetId(BigDecimal dataSetId) {
        this.dataSetId = dataSetId;
    }

    public void setGroupId(BigDecimal groupId) {
        this.groupId = groupId;
    }

    public long getId() {
        return this.id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public DOI getDoi() {
        return this.doi;
    }

    public void setDoi(DOI doi) {
        this.doi = doi;
    }

}

El Spring MVC Controller:

@Controller
@RequestMapping("/summary")
public class DOISummaryController {
    @Autowired
    MyDAO myDAO;

    @RequestMapping()
    public String DOISummary() {
        myDAO.logDOI();

        return "home";
    }
}

Mi configuración de primavera:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:task="http://www.springframework.org/schema/task"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
        http://www.springframework.org/schema/task
        http://www.springframework.org/schema/task/spring-task-3.0.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <!-- Root Context: defines shared resources visible to all other web components -->

    <context:property-placeholder location="WEB-INF/spring/root-context.properties, WEB-INF/spring/datasource-context.properties"  />

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName">
            <value>oracle.jdbc.driver.OracleDriver</value>
        </property>
        <property name="url">
            <value>${Url}</value>
        </property>
        <property name="username">
            <value>${Username}</value>
        </property>
        <property name="password">
            <value>${Password}</value>
        </property>
    </bean>

    <bean id="emf" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter" />
        </property>
        <property name="packagesToScan" value="org.myorg.doi.domain" />
        <property name="jpaProperties">
            <props>
                <prop key="hibernate.dialect">
                    org.hibernate.dialect.Oracle10gDialect
                </prop>
                <prop key="hibernate.max_fetch_depth">3</prop>
                <prop key="hibernate.jdbc.fetch_size">50</prop>
                <prop key="hibernate.jdbc.batch_size">10</prop>
                <prop key="hibernate.show_sql">true</prop>
            </props>
        </property>
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="emf" />
    </bean>

    <tx:annotation-driven transaction-manager="transactionManager" />

    <context:annotation-config />

    <task:annotation-driven />

    <context:component-scan base-package="org.myorg.doi" />

</beans>

Y un rastro de pila, según lo solicitado:

SEVERE: Servlet.service() for servlet [appServlet] in context with path [/DOI] threw exception [Request processing failed; nested exception is org.hibernate.LazyInitializationException: could not initialize proxy - no Session] with root cause
org.hibernate.LazyInitializationException: could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:430)
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:121)
    at org.hibernate.collection.internal.PersistentSet.iterator(PersistentSet.java:180)
    at org.myorg.doi.dao.MyDAO.logDOI(MyDAO.java:27)
    at org.myorg.doi.web.DOISummaryController.DOISummary(DOISummaryController.java:29)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:213)
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:126)
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:96)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:617)
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:578)
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:923)
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:852)
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:778)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:621)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:722)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:305)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:225)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:472)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:168)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:927)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
    at com.springsource.insight.collection.tcserver.request.HttpRequestOperationCollectionValve.traceNextValve(HttpRequestOperationCollectionValve.java:116)
    at com.springsource.insight.collection.tcserver.request.HttpRequestOperationCollectionValve.invoke(HttpRequestOperationCollectionValve.java:98)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:407)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1001)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:585)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:310)
    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
    at java.lang.Thread.run(Thread.java:680)

Como puede ver, estoy tratando de usar JPA puro y solo uso Hibernate como proveedor de JPA.

Entiendo que la excepción se debe a que la sesión se ha separado de la entidad. Pero, pensé que eso no sucedería si actualmente estuviéramos en una transacción, lo que debería ser el caso, ya que el método logDOI está anotado con @Transactional.

Por supuesto, todo funciona perfectamente si cambio el FetchType a EAGER, pero parece que no debería tener que hacer eso.

También soy consciente de OpenEntityManagerInViewFilter pero parece que no debería tener que usar eso si mantengo todo el acceso a mis entidades en un DAO anotado con @Transactional (o por algún otro medio que no conozco).

Creo que podría estar abordando este problema incorrectamente pero no sé cuál es el enfoque correcto. ¿Cómo se supone que uno debe usar efectivamente las propiedades cargadas perezosamente?

Respuestas a la pregunta(4)

Su respuesta a la pregunta