【问题标题】:Using JPA entities in JSF. Which is the best strategy to prevent LazyInitializationException?在 JSF 中使用 JPA 实体。防止 LazyInitializationException 的最佳策略是什么?
【发布时间】:2011-09-15 07:19:19
【问题描述】:

想听听专家关于从 JSF UI 编辑 JPA 实体的最佳实践。

所以,关于这个问题的几句话。

想象一下,我有一个持久化对象MyEntity,我获取它进行编辑。在 DAO 层我使用

return em.find(MyEntity.class, id);

它返回 MyEntity 实例,代理在“父”实体上 - 想象其中一个是 MyParentMyParent 被提取为@Access(AccessType.PROPERTY) 的代理问候语:

@Entity
public class MyParent {

    @Id
    @Access(AccessType.PROPERTY)    
    private Long id;
    //...
}

并且 MyEntity 有对它的引用:

@ManyToOne(fetch = FetchType.LAZY)
@LazyToOne(LazyToOneOption.PROXY)
private MyParent myParent;

到目前为止一切顺利。在 UI 中,我只是直接使用获取的对象而不创建任何值对象,并在选择列表中使用父对象:

<h:selectOneMenu value="#{myEntity.myParent.id}" id="office">
    <f:selectItems value="#{parents}"/>
</h:selectOneMenu>

一切正常,没有出现LazyInitializationException。但是当我保存对象时,我收到了

LazyInitializationException: could not initialize proxy - no Session

MyParent代理setId()方法上。

如果我将MyParent 关系更改为EAGER,我可以轻松解决问题

@ManyToOne(fetch = FetchType.EAGER)
private MyParent myParent;

或使用left join fetch p.myParent 获取对象(实际上我现在就是这样做的)。在这种情况下,保存操作正常,并且关系透明地更改为新的MyParent 对象。无需执行其他操作(手动复制、手动参考设置)。非常简单方便。

但是。如果对象引用了 10 个其他对象 - em.find() 将导致 10 个额外的连接,这不是一个好的数据库操作,尤其是当 我根本不使用引用对象状态时。我所需要的只是指向对象的链接,而不是它们的状态。

这是一个全球性问题,我想知道 JSF 专家如何在其应用程序中处理 JPA 实体,这是避免额外连接和LazyInitializationException 的最佳策略。

扩展持久性上下文不适合我。

谢谢!

【问题讨论】:

  • 您能做的最好的事情是在将实体处理为 JSF bean 之前重新映射/深度克隆实体。
  • @DanubianSailor 我希望看到完整的答案,并解释该建议的利弊。我目前也在考虑同样的事情。
  • 您使用的是什么版本的 JavaEE(EJB、CDI、JSF)?

标签: hibernate jsf jpa lazy-initialization


【解决方案1】:

您应该准确地提供视图所期望的模型。

如果 JPA 实体恰好匹配所需的模型,则立即使用它。

如果 JPA 实体恰好具有太少或太多属性,则使用 DTO(子类)和/或 constructor expression 和更具体的 JPQL 查询,如有必要,使用显式 FETCH JOIN。或者可能使用特定于 Hibernate 的 fetch profiles,或特定于 EclipseLink 的 attribute groups。否则,它可能会导致所有地方的延迟初始化异常,或者消耗过多的内存。

“在视图中打开会话”模式是一个糟糕的设计。您基本上在整个 HTTP 请求-响应处理期间保持单个数据库事务处于打开状态。对是否开始新的数据库事务的控制完全由您控制。当业务逻辑需要时,您不能在同一个 HTTP 请求期间生成多个事务。请记住,当单个查询在事务期间失败时,整个 事务将被回滚。另见When is it necessary or convenient to use Spring or EJB3 or all of them together?

从 JSF 的角度来看,“在视图中打开会话”模式还意味着可以在呈现响应的同时执行业务逻辑。这与其他异常处理一起不太好,其目的是向最终用户显示自定义错误页面。如果在呈现响应的中途抛出业务异常,最终用户因此已经收到响应标头和 HTML 的一部分,则服务器无法再清除响应以显示漂亮的错误页面。此外,根据Why JSF calls getters multiple times,在 getter 方法中执行业务逻辑在 JSF 中的做法是不受欢迎的。

只需在渲染响应阶段开始之前,通过托管 bean 操作/侦听器方法中的常规服务方法调用准确地准备视图所需的模型。例如,一种常见的情况是现有(非托管)父实体具有延迟加载的一对多子属性,并且您希望通过 ajax 操作在当前视图中呈现它,那么您应该只是让 ajax 监听方法在服务层获取并初始化它。

<f:ajax listener="#{bean.showLazyChildren(parent)}" render="children" />
public void showLazyChildren(Parent parent) {
    someParentService.fetchLazyChildren(parent);
}
public void fetchLazyChildren(Parent parent) {
    parent.setLazyChildren(em.merge(parent).getLazyChildren()); // Becomes managed.
    parent.getLazyChildren().size(); // Triggers lazy initialization.
}

特别是在 JSF UISelectMany 组件中,LazyInitializationException 还有另一个完全出乎意料的可能原因:在保存所选项目期间,JSF 需要在用所选项目填充之前重新创建底层集合,但是如果发生这种情况是一个持久层特定的延迟加载集合实现,那么这个异常也会被抛出。解决方案是将UISelectMany 组件的collectionType 属性显式设置为所需的“普通”类型。

<h:selectManyCheckbox ... collectionType="java.util.ArrayList">

这个在org.hibernate.LazyInitializationException at com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel有详细的询问和回答。

另见:

【讨论】:

  • 不要声明“如果 JPA 实体恰好与所需的模型匹配,则立即使用它。”和“视图模式中的开放会话是一个糟糕的设计”相互矛盾。据我了解,如果您在 JSF 支持 bean 中使用 JPA 实体,那么您需要“在视图过滤器中打开会话”?
  • @RaphaelRoth:如果模型由服务正确准备,则不会。
  • with"prepared",你的意思是懒加载的属性已经初始化了吗?
【解决方案2】:

对于 Hibernate >= 4.1.6 阅读此https://stackoverflow.com/a/11913404/3252285

使用 OpenSessionInView 过滤器(设计模式)非常有用,但在我看来它并不能完全解决问题,原因如下:

如果我们有一个 Entity 存储在 Session 中或由 Session Bean 处理或从缓存中检索,并且它的集合之一在同一个加载请求期间没有被初始化,那么我们可以随时得到 Exception我们稍后会调用它,即使我们使用 OSIV 设计模式。

让我们详细说明问题:

  • 任何休眠代理都需要附加到打开的会话才能正常工作。
  • Hibernate 不提供任何工具 (Listener or Handler) 来重新连接代理,以防他的会话关闭或他与自己的会话分离。

为什么hibernate不提供这个? : 因为它不容易识别到哪个 Session,应该重新连接代理,但在许多情况下我们可以。

那么当 LazyInitializationException 发生时如何重新附加代理呢?

在我的 ERP 中,我修改了这些类:JavassistLazyInitializerAbstractPersistentCollection,然后我不再关心这个异常(使用 3 年没有任何错误):

class JavassistLazyInitializer{
     @Override
     public Object invoke(
                        final Object proxy,
                        final Method thisMethod,
                        final Method proceed,
                        final Object[] args) throws Throwable {
            if ( this.constructed ) {
                Object result;
                try {
                    result = this.invoke( thisMethod, args, proxy );
                }
                catch ( Throwable t ) {
                    throw new Exception( t.getCause() );
                }           
                if ( result == INVOKE_IMPLEMENTATION ) {
                    Object target = null;
                    try{
                        target = getImplementation();
                    }catch ( LazyInitializationException lze ) {
              /* Catching the LazyInitException and reatach the proxy to the right Session */
                    EntityManager em = ContextConfig.getCurrent().getDAO(
                                        BaseBean.getWcx(), 
                                        HibernateProxyHelper.getClassWithoutInitializingProxy(proxy)).
                                        getEm();
                                ((Session)em.getDelegate()).refresh(proxy);// attaching the proxy                   
                    }   
                    try{                
                        if (target==null)
                            target = getImplementation();
                            .....
                    }
        ....
     }

class AbstractPersistentCollection{
private <T> T withTemporarySessionIfNeeded(LazyInitializationWork<T> lazyInitializationWork) {
        SessionImplementor originalSession = null;
        boolean isTempSession = false;
        boolean isJTA = false;      
        if ( session == null ) {
            if ( allowLoadOutsideTransaction ) {
                session = openTemporarySessionForLoading();
                isTempSession = true;
            }
            else {
    /* Let try to reatach the proxy to the right Session */
                try{
                session = ((SessionImplementor)ContextConfig.getCurrent().getDAO(
                        BaseBean.getWcx(), HibernateProxyHelper.getClassWithoutInitializingProxy(
                        owner)).getEm().getDelegate());             
                SessionFactoryImplementor impl = (SessionFactoryImplementor) ((SessionImpl)session).getSessionFactory();            
                ((SessionImpl)session).getPersistenceContext().addUninitializedDetachedCollection(
                        impl.getCollectionPersister(role), this);
                }catch(Exception e){
                        e.printStackTrace();        
                }
                if (session==null)
                    throwLazyInitializationException( "could not initialize proxy - no Session" );
            }
        }
        if (session==null)
            throwLazyInitializationException( "could not initialize proxy - no Session" );
        ....
    }
...
}

注意:

  • 我没有解决所有可能的问题,例如 JTA 或其他情况。
  • 激活缓存后,此解决方案效果更好

【讨论】:

    【解决方案3】:

    Open Session in View 设计模式可以在 Java EE 环境中轻松实现(不依赖于 hibernate、spring 或 Java EE 之外的其他东西)。它与OpenSessionInView 中的大部分相同,但您应该使用 JTA 事务而不是 Hibernate 会话

    @WebFilter(urlPatterns = {"*"})
    public class JTAFilter implements Filter{
    
        @Resource
        private UserTransaction ut;
    
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            try{
               ut.begin();
               chain.doFilter(request, response);
            }catch(NotSupportedException | SystemException e){
                throw new ServletException("", e);
            } finally {
                try {
                   if(ut.getStatus()!= Status.STATUS_MARKED_ROLLBACK){
                       ut.commit();
                   }
                } catch (Exception e) {
                    throw new ServletException("", e);
                }
           }
      }
    
      @Override
      public void destroy() {
    
      }
    }
    

    【讨论】:

      【解决方案4】:

      延迟加载是一项重要功能,可以很好地提高性能。然而,它的可用性比它应该的要差得多。

      特别是当您开始处理 AJAX 请求时,遇到未初始化的集合,注解 只是有助于告诉 Hibernate不要立即加载它。 Hibernate 不会处理其他任何事情,但会向您抛出 LazyInitializationException - 正如您所经历的那样。

      我对此的解决方案 - 这可能并不完美,也可能是一场噩梦 - 通过应用以下 规则 在任何情况下都有效(我不得不承认,这是在一开始就写的, 但从那时起就一直有效):

      每个使用fetch = FetchType.LAZY的实体必须扩展LazyEntity,并在相关collection的getter中调用initializeCollection(),然后再返回。 (自定义验证器负责处理此约束,报告缺少的扩展和/或对initializeCollection 的调用)

      Example-Class(用户,其组被延迟加载):

      public class User extends LazyEntity{
           @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
           @BatchSize(size = 5)
           List<Group> groups; 
      
           public List<Group> getGroups(){
             initializeCollection(this.groups);
             return this.groups;
           }
      }
      

      initializeCollection(Collection collection) 的实现如下所示。 In-Line cmets 应该让您了解哪种情况需要什么。该方法被同步以避免 2 个活动会话转移实体的所有权,而另一个会话当前正在获取数据。 (仅当并发 Ajax 请求在同一实例上进行时才会出现。)

      public abstract class LazyEntity {
      
          @SuppressWarnings("rawtypes")
          protected synchronized void initializeCollection(Collection collection) {
              if (collection instanceof AbstractPersistentCollection) {
                   //Already loaded?
                   if (!Hibernate.isInitialized(collection)) {
                      AbstractPersistentCollection ps = (AbstractPersistentCollection) collection;
      
                      //Is current Session closed? Then this is an ajax call, need new session!
                      //Else, Hibernate will know what to do.
                      if (ps.getSession() == null) {
                          //get an OPEN em. This needs to be handled according to your application.
                          EntityManager em = ContextHelper.getBean(ServiceProvider.class).getEntityManager();
      
                          //get any Session to obtain SessionFactory
                          Session anySession = em.unwrap(Session.class);
                          SessionFactory sf = anySession.getSessionFactory();
      
                          //get a new session    
                          Session newSession = sf.openSession();
      
                          //move "this" to the new session.
                          newSession.update(this);
      
                          //let hibernate do its work on the current session.
                          Hibernate.initialize(collection);
      
                          //done, we can abandon the "new Session".
                          newSession.close();
                      }
                  }
              }
          }
      }
      

      但请注意,此方法需要您验证是否某个实体与当前会话相关联,无论何时保存它 - 否则您必须在调用 merge() 之前再次将整个对象树移动到当前会话。

      【讨论】:

        【解决方案5】:

        EJB3 中的视图中没有对打开会话的标准支持,请参阅answer

        映射的获取类型只是一个默认选项,我可以在查询时被覆盖。这是一个例子:

        select g from Group g fetch join g.students
        

        因此,在普通 EJB3 中的替代方法是通过显式查询所需数据,确保在渲染开始之前加载渲染视图所需的所有数据。

        【讨论】:

          【解决方案6】:

          一种非常常见的方法是创建一个在视图过滤器中打开实体管理器。 Spring 提供了一个(检查here)。

          我看不到您正在使用 Spring,但这并不是真正的问题,您可以根据需要调整该类中的代码。您还可以检查过滤器Open Session in View,它的作用相同,但它保持休眠会话打开而不是实体管理器。

          这种方法可能不适合您的应用程序,SO 中有一些关于这种模式或反模式的讨论。 Link1。我认为对于大多数应用程序(小型,少于 20 个并发用户)这个解决方案工作得很好。

          编辑

          有一个 Spring 类与 FSF here 关系更好

          【讨论】:

          • 嗨。谢谢你的回答。我看着这个模式。实际上我使用 EJB 容器管理的事务,只是注入实体管理器和容器划分事务。视图类中的 Spring 打开会话需要将我的逻辑完全转移到 Spring 事务管理器和手动事务划分或我认为的 @Transactional 的 Spring 托管事务。虽然目前我只将 Spring 用于我的应用程序的几个部分。是否有任何简单的方法可以将 OSIV 模式应用于 EJP 容器管理的持久性上下文?
          • @bitec 不幸的是,我不知道如何在 EJB 容器中执行此操作。我对 EJB 的过时知识表明,所有实体在离开服务时都应该进行水合(通常这是“手动”完成的),或者应该将它们转换为 DTO。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-01-08
          • 2019-10-06
          • 2010-09-08
          • 1970-01-01
          • 2011-04-16
          • 2013-07-26
          相关资源
          最近更新 更多