获取" org.hibernate.LazyInitializationException"从我的二级ehcache中检索项目后的异常

时间:2016-03-14 20:35:02

标签: java spring hibernate ehcache lazy-initialization

我正在使用Hibernate 5.1.0.Final和ehcache以及Spring 3.2.11.RELEASE。我在@Cacheable s之一中设置了以下DAO注释:

@Override
@Cacheable(value = "main")
public Item findItemById(String id)
{
    return entityManager.find(Item.class, id);
}

返回的项目有许多关联,其中一些是懒惰的。例如,它(最终)引用了字段:

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "product_category", joinColumns = { @JoinColumn(name = "PRODUCT_ID") }, inverseJoinColumns = { @JoinColumn(name = "CATEGORY_ID") })
private List<Category> categories;

我注意到在我标记为@Transactional的一个方法中,当从二级缓存中检索到上述方法时,在尝试迭代categories字段时,我得到以下异常:

@Transactional(readOnly=true)
public UserContentDto getContent(String itemId, String pageNumber) throws IOException
{
    Item Item = contentDao.findItemById(ItemId);
   …
   // Below line causes a “LazyInitializationException” exception
   for (Category category : item.getParent().getProduct().getCategories())
    {

堆栈跟踪是:

16:29:42,557 INFO  [org.directwebremoting.log.accessLog] (ajp-/127.0.0.1:8009-18) Method execution failed: : org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: org.mainco.subco.ecom.domain.Product.standardCategories, could not initialize proxy - no Session
    at org.hibernate.collection.internal.AbstractPersistentCollection.throwLazyInitializationException(AbstractPersistentCollection.java:579) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.withTemporarySessionIfNeeded(AbstractPersistentCollection.java:203) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.initialize(AbstractPersistentCollection.java:558) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.hibernate.collection.internal.AbstractPersistentCollection.read(AbstractPersistentCollection.java:131) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.hibernate.collection.internal.PersistentBag.iterator(PersistentBag.java:277) [hibernate-myproject-5.1.0.Final.jar:5.1.0.Final]
    at org.mainco.subco.ebook.service.ContentServiceImpl.getCorrelationsByItem(ContentServiceImpl.java:957) [myproject-90.0.0-SNAPSHOT.jar:]
    at org.mainco.subco.ebook.service.ContentServiceImpl.getContent(ContentServiceImpl.java:501) [myproject-90.0.0-SNAPSHOT.jar:]
    at sun.reflect.GeneratedMethodAccessor819.invoke(Unknown Source) [:1.6.0_65]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) [rt.jar:1.6.0_65]
    at java.lang.reflect.Method.invoke(Method.java:597) [rt.jar:1.6.0_65]
    at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:96) [spring-tx-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:260) [spring-tx-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94) [spring-tx-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:91) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204) [spring-aop-3.2.11.RELEASE.jar:3.2.11.RELEASE]
    at com.sun.proxy.$Proxy126.getContent(Unknown Source)

我理解Hibernate会话的关闭 - 我不关心为什么会这样。此外,它不是一个选项o使上述关联渴望(而不是懒惰)。鉴于此,我该如何解决这个问题?

编辑:以下是我的ehccahe.xml的配置方式......

<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../config/ehcache.xsd" updateCheck="false">
    <!-- This is a default configuration for 256Mb of cached data using the JVM's heap, but it must be adjusted
         according to specific requirement and heap sizes -->
    <defaultCache maxElementsInMemory="10000"
         eternal="false"
         timeToIdleSeconds="86400"
         timeToLiveSeconds="86400"
         overflowToDisk="false"
         memoryStoreEvictionPolicy="LRU">
    </defaultCache> 
    <cache name="main" maxElementsInMemory="10000" />   
     <cacheManagerPeerProviderFactory
         class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
         properties="peerDiscovery=automatic, multicastGroupAddress=230.0.0.1,
         multicastGroupPort=4446, timeToLive=32"/>
    <cacheManagerPeerListenerFactory
        class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"
        properties="hostName=localhost, port=40001,
        socketTimeoutMillis=2000"/>    
</ehcache>

以下是我如何将其插入我的Spring上下文...

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="packagesToScan" value="org.mainco.subco" />
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
    </property>
    <property name="dataSource" ref="dataSource"/>
    <property name="jpaPropertyMap" ref="jpaPropertyMap" />
</bean>

<cache:annotation-driven key-generator="cacheKeyGenerator" />

<bean id="cacheKeyGenerator" class="org.mainco.subco.myproject.util.CacheKeyGenerator" />

<bean id="cacheManager"
        class="org.springframework.cache.ehcache.EhCacheCacheManager"
        p:cacheManager-ref="ehcache"/>

<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
        p:configLocation="classpath:ehcache.xml"
        p:shared="true" />

<util:map id="jpaPropertyMap">
    <entry key="hibernate.show_sql" value="false" />
    <entry key="hibernate.hbm2ddl.auto" value="validate"/>
        <entry key="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect"/>
        <entry key="hibernate.transaction.manager_lookup_class" value="org.hibernate.transaction.JBossTransactionManagerLookup" />
        <entry key="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
        <entry key="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/>
        <entry key="hibernate.cache.use_second_level_cache" value="true" />
        <entry key="hibernate.cache.use_query_cache" value="false" />
        <entry key="hibernate.generate_statistics" value="false" />
</util:map>

<bean id="entityManager" class="org.springframework.orm.jpa.support.SharedEntityManagerBean">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

4 个答案:

答案 0 :(得分:9)

看看similar question。基本上,您的缓存不是Hibernate二级缓存。您正在分离的实体实例上访问延迟的未初始化关联,因此预计会抛出LazyInitializationException

您可以尝试使用hibernate.enable_lazy_load_no_trans,但建议的方法是配置Hibernate second level cache,以便:

  • 缓存的实体会自动附加到加载它们的后续会话中。
  • 缓存中的缓存数据在更改后会自动刷新/无效。
  • 考虑事务语义,同步对高速缓存实例的更改。具有所需级别的cache / db consistency guarantees的其他会话/事务可以看到更改。
  • 当缓存实例从与其关联的其他实体导航到缓存时,会自动从缓存中提取缓存实例。

修改

如果你仍然想为此目的使用Spring缓存,或者你的要求是一个适当的解决方案,那么请记住,Hibernate托管实体不是线程安全的,所以你必须存储和返回分离进出自定义缓存的实体。此外,在分离之前,您需要初始化您希望在实体分离时访问的所有延迟关联。

要实现这一目标,您可以:

  1. 使用EntityManager.detach明确分离托管实体。您还需要将分离操作分离或级联到关联实体,并确保对来自其他管理实体的分离实体的引用进行适当处理。
  2. 或者,您可以在单独的事务中执行此操作,以确保所有内容都已分离,并且您不会在当前持久性上下文中引用托管实体的分离实体:

    @Override
    @Cacheable(value = "main")
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public Item findItemById(String id) {
        Item result = entityManager.find(Item.class, id);
        Hibernate.initialize(result.getAssociation1());
        Hibernate.initialize(result.getAssociation2());
        return result;
    }
    

    因为可能发生Spring事务代理(拦截器)在缓存代理之前执行(两者都具有相同的默认order值:transaction; cache),那么你会总是启动嵌套事务,无论是真正获取实体,还是只返回缓存实例。

    虽然我们可以得出结论,启动不需要的嵌套事务的性能损失很小,但这里的问题是当缓存中存在托管实例时,您会留下一个小时间窗口。

    为避免这种情况,您可以更改默认订单值:

    <tx:annotation-driven order="200"/>
    <cache:annotation-driven order="100"/>
    

    这样缓存拦截器总是放在事务处理器之前。

    或者,为了避免对配置更改进行排序,您可以简单地将调用从@Cacheable方法委托给另一个bean上的@Transactional(propagation = Propagation.REQUIRES_NEW)方法。

答案 1 :(得分:9)

您在代码段中实现的是基于spring-cache的自定义缓存。使用您的实现,您需要处理缓存驱逐,确保在您的对象图将被缓存时,它们被正确加载等等。一旦它们被缓存并且加载它们的原始hibernate会话关闭它们&#39 ; ll变得分离,你不能再导航无关联的懒惰关联。此外,处于当前状态的自定义缓存解决方案会缓存实体图,这可能不是您想要的,因为该图的任何部分可能在给定时间发生更改,并且您的缓存解决方案需要注意该图表所有部分的变化,以正确处理驱逐。

您在问题中发布的配置不是Hibernate二级缓存

管理缓存是一项复杂的工作,我不建议您自己做,除非您完全确定自己在做什么(但之后你就赢了)。在Stackoverflow上问这个问题。

让我解释当您获得LazyInitializationException时发生的事情:您使用@org.springframework.cache.annotation.Cacheable标记了一个dao方法。在这种情况下会发生以下情况:

  1. Spring将拦截器附加到托管bean。拦截器将拦截dao方法调用,它将根据拦截器方法和实际方法参数创建缓存键(这可以自定义),并查找缓存以查看缓存中是否有任何条目为那把钥匙。如果有条目,它将返回该条目而不实际调用您的方法。如果该密钥没有缓存条目,它将调用您的方法,序列化返回值并将其存储在缓存中。
  2. 对于密钥没有缓存条目的情况,将调用您的方法。您的方法使用弹簧提供的单例代理到线程绑定EntityManager,这是在Spring遇到第一个@Transactional方法调用时分配的。在您的情况下,这是另一个spring服务bean的getContent(...)方法。因此,您的方法会使用EntityManager.find()加载实体。这将为您提供一个部分加载的实体图,其中包含尚未初始化的代理和集合到尚未由持久性上下文加载的其他关联实体。
  3. 您的方法返回部分加载的实体图,spring将立即为您序列化并将其存储在缓存中。请注意,序列化部分加载的实体图将反序列化为部分加载的实体图。
  4. 在使用相同参数标记有@Cacheable的dao方法的第二次调用时,Spring将发现缓存中确实存在与该键对应的条目,并将加载和反序列化该条目。您的dao方法将不会被调用,因为它使用缓存的条目。现在您遇到了问题:当您存储在缓存中时,您的反序列化缓存实体图仅被部分加载,并且只要触摸图中任何未初始化的部分,您就会获得LazyInitializationException。反序列化的实体将始终被分离,因此即使原始EntityManager仍然打开(不是),您仍然会得到相同的异常。
  5. 现在的问题是:你可以做些什么来避免LazyInitializationException。好吧,我的建议是你忘了实现自定义缓存,只是配置Hibernate来为你做缓存。我将在稍后讨论如何做到这一点。如果您想坚持使用您尝试实施的自定义缓存,请按以下步骤操作:

    浏览整个代码库,找到@Cacheable dao方法的所有调用。遵循所加载的实体图传递的所有可能代码路径,并标记实体图的所有部分,这些部分都会被客户端代码触及。现在返回到您的@Cacheable方法并对其进行修改,以便加载并初始化可能触及的实体图形的所有部分。因为一旦你返回并且它被序列化并在以后反序列化,它将始终处于分离状态,因此最好确保正确加载所有可能的图形路径。你应该已经觉得这最终会变得多么不切实际。如果仍然没有说服你不要遵循这个方向,那么这是另一个论点。

    由于您加载了一大块可能的数据库,因此在实际加载和缓存时,您将在给定时间获得该数据库部分的快照。现在,无论何时使用数据库这一大块的缓存版本,都存在使用该数据的陈旧版本的风险。为了防范这种情况,您需要监视刚刚缓存的数据库的大块当前版本中的任何更改,并从缓存中逐出整个实体图。因此,您几乎需要考虑哪些实体是实体图的一部分,并在这些实体被更改并逐出整个图时设置一些事件侦听器。这些问题都不存在于Hibernate二级缓存中。

    现在回到我的建议:设置Hibernate二级缓存

    Hibernate二级缓存由Hibernate管理,您可以自动从hibernate进行逐出管理。如果启用了Hibernate二级缓存,Hibernate将缓存重建实体所需的数据,如果 - 当试图从数据库加载实体时 - 它会发现它有一个有效的缓存条目对于您的实体,它将跳过命中数据库并从其缓存重建您的实体。 (标记在实体图中使用其自定义缓存解决方案中可能未获取的关联和未初始化的代理进行缓存的差异)。更新实体时,它还将替换过时的缓存条目。它可以处理与管理缓存相关的各种事情,因此您不必担心它。

    在这里,您如何启用Hibernate二级缓存:除了您的配置,请执行以下操作:

    1. 除了已经用于二级管理的hibernate属性,即

      <entry key="hibernate.cache.region.factory_class" value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>
      <entry key="hibernate.cache.provider_class" value="org.hibernate.cache.EhCacheProvider"/>
      <entry key="hibernate.cache.use_second_level_cache" value="true" />
      

      添加以下条目:

      <entry key="javax.persistence.sharedCache.mode" value="ENABLE_SELECTIVE" />
      

      或者,您可以为shared-cache-mode添加persistence.xml配置选项(因为您没有发布,我认为您不能使用因此,它是以前的替代方案;但是下面的一个是首选的):

      <persistence-unit name="default">
          <!-- other configuration lines stripped -->
      
          <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
      
          <!-- other configuration lines stripped -->
      </persistence-unit>
      
    2. javax.persistence.@Cacheable注释添加到您希望可缓存的@Entity类中。
    3. 如果要为Hibernate默认不缓存的集合值关联添加缓存,可以为每个此类集合添加@org.hibernate.annotations.Cache注释(具有适当的缓存并发策略选项) :

      @ManyToMany(fetch = FetchType.LAZY)
      @JoinTable(name = "product_category", joinColumns = { @JoinColumn(name = "PRODUCT_ID")
                 }, inverseJoinColumns = { @JoinColumn(name = "CATEGORY_ID") })
      @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
      private List<Category> categories;
      
    4. 有关详细信息,请参阅Hibernate参考文档中的Improving performance/The Second Level Cache

      这是一篇关于这个主题的好文章:Pitfalls of the Hibernate Second-Level / Query Caches

      我根据您发布的代码段整理了一个small project,您可以查看这些代码段以查看正在运行的Hibernate二级缓存。

答案 2 :(得分:3)

问题是你正在缓存对懒惰加载的对象的引用。在对象全部加载后缓存对象或根本不使用缓存。

以下是在缓存之前手动加载类别的方法:

Item item = entityManager.find(Item.class, id);
item.getParent().getProduct().getCategories();
return item;

另一个更好的缓存策略是将缓存放在应用程序的服务级别而不是DAO级别或根本没有缓存。

您的问题是由以下事件引起的:

正在检索没有其类别的项目,然后将其放入事务1的缓存中。在事务2中,您调用相同的方法并检索该项目并尝试读取其类别。在那一刻,hibernate尝试从事务1中读取与Item对象关联的类别,但事务1已经完成,因此失败。

答案 3 :(得分:0)

我在此配置中使用了简单类型缓存,如下所示:

spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true

spring.jpa.open-in-view=true

spring.cache.type=simple