11

想听听专家关于从 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.

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

谢谢!

4

6 回答 6

5

You should provide exactly the model the view expects.

If the JPA entity happens to match exactly the needed model, then just use it right away.

If the JPA entity happens to have too few or too much properties, then use a DTO (subclass) and/or a constructor expression with a more specific JPQL query, if necessary with an explicit FETCH JOIN. Or perhaps with Hibernate specific fetch profiles, or EclipseLink specific attribute groups. Otherwise, it may either cause lazy initializtion exceptions over all place, or consume more memory than necessary.

The "open session in view" pattern is a poor design. You're basically keeping a single DB transaction open during the entire HTTP request-response processing. Control over whether to start a new DB transaction or not is completely taken away from you. You cannot spawn multiple transactions during the same HTTP request when the business logic requires so. Keep in mind that when a single query fails during a transaction, then the entire transaction is rolled back. See also When is it necessary or convenient to use Spring or EJB3 or all of them together?

In JSF perspective, the "open session in view" pattern also implies that it's possible to perform business logic while rendering the response. This doesn't go very well together with among others exception handling whereby the intent is to show a custom error page to the enduser. If a business exception is thrown halfway rendering the response, whereby the enduser has thus already received the response headers and a part of the HTML, then the server cannot clear out the response anymore in order to show a nice error page. Also, performing business logic in getter methods is a frowned upon practice in JSF as per Why JSF calls getters multiple times.

Just prepare exactly the model the view needs via usual service method calls in managed bean action/listener methods, before render response phase starts. For example, a common situation is having an existing (unmanaged) parent entity at hands with a lazy loaded one-to-many children property, and you'd like to render it in the current view via an ajax action, then you should just let the ajax listener method fetch and initialize it in the service layer.

<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.
}

Specifically in JSF UISelectMany components, there's another, completely unexpected, probable cause for a LazyInitializationException: during saving the selected items, JSF needs to recreate the underlying collection before filling it with the selected items, however if it happens to be a persistence layer specific lazy loaded collection implementation, then this exception will also be thrown. The solution is to explicitly set the collectionType attribute of the UISelectMany component to the desired "plain" type.

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

This is in detail asked and answered in org.hibernate.LazyInitializationException at com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel.

See also:

于 2015-08-14T09:23:44.620 回答
3

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

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

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

让我们详细说明问题:

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

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

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

在我的ERP中,我修改了这些类:JavassistLazyInitializer然后AbstractPersistentCollection,我不再关心这个异常(使用 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 或其他情况。
  • 当您激活缓存时,此解决方案效果更好
于 2015-08-17T08:57:36.287 回答
2

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

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

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

编辑

这里有一个 Spring 类与 FSF 的关系更好

于 2011-06-15T07:39:40.637 回答
2

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

尤其是当您开始处理 AJAX 请求时,遇到未初始化的集合,注释只是告诉 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 请求在同一个实例上进行时才会出现。)

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 实体是否与当前会话相关联,无论何时保存它 - 否则您必须在调用之前再次将整个对象树移动到当前会话merge()

于 2015-08-16T20:28:08.933 回答
1

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

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

select g from Group g fetch join g.students

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

于 2013-12-12T13:43:54.333 回答
1

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

  }
}
于 2015-08-17T12:57:12.307 回答