在 JSF 中使用 JPA 实体.这是防止 LazyInitializationException 的最佳策略



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

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

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

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

它返回MyEntity带有"父"实体代理的实例 - 想象一下其中一个是MyParent . MyParent 作为代理问候语获取给@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的最佳策略。

扩展持久性上下文对我来说是不行的。

谢谢!

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

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

如果 JPA 实体碰巧具有太少或太多的属性,则使用带有更具体的 JPQL 查询的 DTO(子类(和/或构造函数表达式,如有必要,请使用显式FETCH JOIN。或者可能使用Hibernate特定的获取配置文件,或EclipseLink特定的属性组。否则,它可能会导致所有位置的延迟初始化异常,或者消耗不必要的内存。

"视图中打开会话"模式是一个糟糕的设计。在整个HTTP请求-响应处理过程中,您基本上保持单个数据库事务处于打开状态。对是否开始新的数据库事务的控制完全被剥夺了。当业务逻辑需要时,不能在同一 HTTP 请求期间生成多个事务。请记住,当单个查询在事务期间失败时,将回滚整个事务。参见 什么时候有必要或方便地使用 Spring 或 EJB3 或所有这些?

在 JSF 透视图中,"视图中打开会话"模式还意味着可以在呈现响应时执行业务逻辑。这与其他异常处理不太配合,后者的目的是向最终用户显示自定义错误页面。如果在呈现响应的中途抛出业务异常,最终用户因此已经收到了响应标头和 HTML 的一部分,则服务器无法再清除响应以显示一个漂亮的错误页面。此外,在 getter 方法中执行业务逻辑在 JSF 中是一种不受欢迎的做法,因为 JSF 会多次调用 getter。

只需在呈现响应阶段开始之前,通过托管 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中详细询问和回答,网址为com.sun.faces.renderkit.html_basic。MenuRenderer.convertSelectManyValuesForModel.

另请参阅:

  • LazyInitializationException in selectManyCheckbox on @ManyToMany(fetch=LAZY(
  • 什么是休眠中的延迟加载?

对于休眠>= 4.1.6,请阅读此 https://stackoverflow.com/a/11913404/3252285

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

如果我们有一个实体存储在会话中或由会话 Bean 处理或从缓存中检索,并且它的一个集合尚未在同一加载请求期间初始化,那么我们以后随时调用它时都会获得异常,即使我们使用 OSIV 设计模式。

让我们详细说明问题:

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

为什么休眠不提供?因为要确定哪个会话并不容易,代理应该被重新分配,但在许多情况下我们可以。

那么当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或其他情况那样解决所有的可能性。
  • 当您激活缓存时,此解决方案的效果会更好

一种非常常见的方法是在视图筛选器中创建打开的实体管理器。春天提供了一个(检查这里(。

我看不出你正在使用 Spring,但这不是一个真正的问题,你可以根据自己的需要调整该类中的代码。您还可以检查筛选器"在视图中打开会话",该筛选器执行相同的操作,但它使休眠会话保持打开状态,而不是实体管理器。

此方法可能不适合您的应用程序,SO 中有一些关于此模式或反模式的讨论。链接 1.我认为对于大多数应用程序(smalish,少于 20 个并发用户(,此解决方案运行良好。

编辑

这里有一个春季班与 FSF 的联系更好

EJB3 中没有对开放会话的标准支持,请参阅此答案。

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

select g from Group g fetch join g.students

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

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

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

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

每个使用 fetch = FetchType.LAZY 的实体都必须扩展LazyEntity,并在相关collection的 getter 中调用 initializeCollection(),然后才能返回。(自定义验证器正在处理此约束,报告缺少的扩展和/或对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 请求在同一实例上进行时显示。

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()之前再次将整个对象树移动到当前会话。

视图中的开放会话设计模式可以在Java EE环境中轻松实现(不依赖于休眠,弹簧或其他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() {
  }
}

相关内容

  • 没有找到相关文章

最新更新