在JSF中使用JPA实体。哪个是防止LazyInitializationException的最佳策略?

时间:2011-06-15 07:11:16

标签: hibernate jsf jpa lazy-initialization

希望听取有关从JSF UI编辑JPA实体的最佳实践的专家。

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

想象一下,我有持久对象MyEntity,我将其取出进行编辑。在DAO层我使用

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

返回“{1}}实例与”父“实体上的代理 - 假设其中一个是MyEntityMyParent被提取为MyParent的代理问候语:

@Access(AccessType.PROPERTY)

并且MyEntity引用了它:

@Entity
public class MyParent {

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

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

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

一切正常,没有<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对象。不需要执行其他操作(手动复制,手动参考设置)。非常简单方便。

BUT 即可。如果对象引用了另外10个对象 - MyParent将导致 10个额外的连接,这不是一个好的数据库操作,特别是当我不使用引用对象状态时所有。我只需要 - 是对象的链接,而不是它们的状态。

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

扩展持久化上下文对我来说不合适。

谢谢!

6 个答案:

答案 0 :(得分:4)

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

如果JPA实体恰好匹配所需的模型,那么就马上使用它。

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

“在视图中打开会话”模式是一种糟糕的设计。这意味着在呈现响应时可以执行业务逻辑。这与异常处理不能很好地结合在一起,其目的是向最终用户显示自定义错误页面。如果在中途抛出业务异常呈现响应,从而最终用户已经收到了响应头和HTML的一部分,那么服务器就不能再清除响应以显示一个漂亮的错误页面。此外,在getter方法中执行业务逻辑是根据Why JSF calls getters multiple times在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需要在填充所选项目之前重新创建基础集合,但是,如果它恰好是持久层特定的延迟加载集合实现,那么也会抛出此异常。解决方案是将collectionType组件的UISelectMany属性显式设置为所需的“普通”类型。

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

详细询问并在org.hibernate.LazyInitializationException at com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel中回答。

另见:

答案 1 :(得分:2)

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

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

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

修改

与FSF here

有更好的关系

答案 2 :(得分:2)

延迟加载是一项重要功能,可以很好地提升性能。然而,这种可用性比它应该更糟糕。

特别是当您开始处理AJAX-Requests时,遇到未初始化的集合时,Annotation 只是有用来告诉Hibernate 不要立即加载。 Hibernate没有处理任何其他事情,但会向你抛出一个LazyInitializationException - 正如你所经历的那样。

我对此的解决方案 - 可能不完美或对所有人来说都是一场噩梦 - 在任何情况下都适用,通过应用以下规则(我必须承认,这是在一开始就写的) ,但从那时起工作):

使用fetch = FetchType.LAZY 的每个实体都有扩展LazyEntity,并在相关initializeCollection()的getter中调用collection之前它被退回了。 (自定义验证程序正在处理此约束,报告缺少的扩展名和/或对initializeCollection的调用)

示例 - 类(用户,其组已加载延迟):

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)的实现如下所示。内联注释应该让您了解哪种情况需要什么。该方法被同步以避免2个活动会话转移实体的所有权,而另一个会话当前正在获取数据。 (仅当在同一实例上进行并发Ajax-Requests时才会出现。)

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();
                }
            }
        }
    }
}

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

答案 3 :(得分:2)

对于Hibernate&gt; = 4.1.6,请阅读此https://stackoverflow.com/a/11913404/3252285

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

如果我们有一个实体存储在Session中或由会话Bean处理或从缓存中检索,并且其中一个集合尚未在同一个加载请求中初始化,那么我们可以随时获得Exception我们稍后称之为,即使我们使用OSIV设计模式。

让我们详细说明问题:

  • 任何hibernate Proxy都需要连接到Opened Session才能正常工作。
  • Hibernate没有提供任何工具(Listener or Handler)来重新接收代理,以防他的会话被关闭或者他已经脱离了自己的会话。

为什么hibernate dos不提供? :      因为它不容易识别哪个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" );
        ....
    }
...
}

NB:

  • 我没有解决所有可能性,例如JTA或其他情况。
  • 当您激活缓存时,此解决方案的效果会更好

答案 4 :(得分:1)

EJB3中的开放会话没有标准支持,请参阅此answer

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

select g from Group g fetch join g.students

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

答案 5 :(得分:1)

Open View 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() {

  }
}