Using JPA entities in JSF. Which is the best strategy to prevent LazyInitializationException?

痴心易碎 提交于 2019-12-17 06:45:36

问题


Would like to hear experts on best practice of editing JPA entities from JSF UI.

So, a couple of words about the problem.

Imagine I have the persisted object MyEntity and I fetch it for editing. In DAO layer I use

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

Which returns MyEntity instance with proxies on "parent" entities - imagine one of them is MyParent. MyParent is fetched as the proxy greeting to @Access(AccessType.PROPERTY):

@Entity
public class MyParent {

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

and MyEntity has the reference to it:

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

So far so good. In UI I simply use the fetched object directly without any value objects created and use the parent object in the select list:

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

Everything is rendered ok, no LazyInitializationException occurs. But when I save the object I recieve the

LazyInitializationException: could not initialize proxy - no Session

on MyParent proxy setId() method.

I can easily fix the problem if I change the MyParent relation to EAGER

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

or fetch the object using left join fetch p.myParent (actually that's how I do now). In this case the save operation works ok and the relation is changed to the new MyParent object transparently. No additional actions (manual copies, manual references settings) need to be done. Very simple and convenient.

BUT. If the object references 10 other object - the em.find() will result 10 additional joins, which isn't a good db operation, especially when I don't use references objects state at all. All I need - is links to objects, not their state.

This is a global issue, I would like to know, how JSF specialists deal with JPA entities in their applications, which is the best strategy to avoid both extra joins and LazyInitializationException.

Extended persistence context isn't ok for me.

Thanks!


回答1:


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. This 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 need 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:

  • LazyInitializationException in selectManyCheckbox on @ManyToMany(fetch=LAZY)
  • What is lazy loading in Hibernate?



回答2:


A very common approach is to create an open entity manager in view filter. Spring provides one (check here).

I can't see that you're using Spring, but that's not really a problem, you can adapt the code in that class for your needs. You can also check the filter Open Session in View, which does the same, but it keeps a hibernate session open rather than an Entity Manager.

This approach might not be good for your application, there're a few discussions in SO about this pattern or antipattern. Link1. I think that for most applications (smalish, less than 20 concurrent users) this solution works just fine.

Edit

There's a Spring class ties better with FSF here




回答3:


Lazy Loading is an important feature that can boost performance nicely. However the usability of this is way worse than it should be.

Especially when you start to deal with AJAX-Requests, encountering uninitialized collections, the Annotation ist just usefull to tell Hibernate don't load this right away. Hibernate is not taking care of anything else, but will throw a LazyInitializationException at you - as you experienced.

My solution to this - which might be not perfect or a nightmare over all - works in any scenario, by applying the following rules (I have to admit, that this was written at the very beginning, but works ever since):

Every Entity that is using fetch = FetchType.LAZY has to extend LazyEntity, and call initializeCollection() in the getter of the collection in question, before it is returned. (A custom validator is taking care of this constraints, reporting missing extensions and/or calls to initializeCollection)

Example-Class (User, which has groups loaded lazy):

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

Where the implementation of initializeCollection(Collection collection) looks like the following. The In-Line comments should give you an idea of what is required for which scenario. The method is synchronized to avoid 2 active sessions transfering ownership of an entity while another session is currently fetching data. (Only appears when concurrent Ajax-Requests are going on on the same instance.)

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

But be aware, that this approach needs you to validate IF an Entity is associated to the CURRENT session, whenever you save it - else you have to move the whole Object-Tree to the current session again before calling merge().




回答4:


For Hibernate >= 4.1.6 read this https://stackoverflow.com/a/11913404/3252285

Using the OpenSessionInView Filter (Design pattern) is very usefull, but in my opinion it dosn't solve the problem completely, here's why :

If we have an Entity stored in Session or handled by a Session Bean or retrieved from the cache, and one of its collections has not been initialized during the same loading request, then we could get the Exception at any time we call it later, even if we use the OSIV desing pattern.

Lets detail the problem:

  • Any hibernate Proxy need to be attached to an Opened Session to works correctly.
  • Hibernate is not offering any tool (Listener or Handler) to reatach the proxy in case his session is closed or he's detached from its own session.

Why hibernate dosn't offer that ? : because its not easy to identify to which Session, the Proxy should be reatached, but in many cases we could.

So how to reattach the proxy when the LazyInitializationException happens ?.

In my ERP, i modify thoses Classes : JavassistLazyInitializer and AbstractPersistentCollection, then i never care about this Exception any more (used from 3 years without any bug) :

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

and the

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 :

  • I didn't fix all the possiblities like JTA or other cases.
  • This solution works even better when you activate the cache



回答5:


There is no standard support for open session in view in EJB3, see this answer.

The fetch type of mappings is just a default option, i can be overriden at query time. This is an example:

select g from Group g fetch join g.students

So an alternative in plain EJB3 is to make sure that all the data necessary for rendering the view is loaded before the render starts, by explicitly querying for the needed data.




回答6:


Open Session in View design pattern can be easy implemented in Java EE environment (with no dependency to hibernate, spring or something else out side Java EE). It is mostly the same as in OpenSessionInView, but instead of Hibernate session you should use JTA transaction

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

  }
}


来源:https://stackoverflow.com/questions/6354265/using-jpa-entities-in-jsf-which-is-the-best-strategy-to-prevent-lazyinitializat

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!