Usando entidades JPA en JSF. ¿Cuál es la mejor estrategia para prevenir LazyInitializationException?

Me gustaría escuchar a expertos sobre la mejor práctica de editar entidades JPA desde la interfaz de usuario de JSF.

Entonces, un par de palabras sobre el problema.

Imagine que tengo el objeto persistente MyEntity y lo MyEntity para editarlo. En la capa DAO que uso

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

Que devuelve la instancia de MyEntity con proxies en entidades “principales”; imagine que una de ellas es MyParent . MyParent se recupera como saludo proxy en @Access(AccessType.PROPERTY) :

 @Entity public class MyParent { @Id @Access(AccessType.PROPERTY) private Long id; //... } 

y MyEntity tiene la referencia al mismo:

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

Hasta aquí todo bien. En la interfaz de usuario simplemente uso el objeto recuperado directamente sin ningún objeto de valor creado y uso el objeto principal en la lista de selección:

    

Todo se representa bien, no se produce LazyInitializationException . Pero cuando guardo el objeto, recibo el

 LazyInitializationException: could not initialize proxy - no Session 

en el MyParent proxy setId() .

Puedo solucionar el problema fácilmente si cambio la relación de EAGER a EAGER

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

o busca el objeto usando la left join fetch p.myParent (de hecho, así es como lo hago ahora). En este caso, la operación de guardar funciona bien y la relación se cambia al nuevo objeto MyParent transparente. No es necesario realizar acciones adicionales (copias manuales, configuraciones de referencias manuales). Muy simple y conveniente.

PERO . Si el objeto hace referencia a otros 10 objetos, el em.find() dará como resultado 10 uniones adicionales , lo que no es una buena operación de base de datos, especialmente cuando no utilizo el estado de objetos de referencia . Todo lo que necesito es enlaces a objetos, no a su estado.

Este es un problema global, me gustaría saber cómo los especialistas JSF tratan con las entidades JPA en sus aplicaciones, que es la mejor estrategia para evitar tanto combinaciones adicionales como LazyInitializationException .

El contexto de persistencia extendida no está bien para mí.

¡Gracias!

Debe proporcionar exactamente el modelo que la vista espera.

Si la entidad JPA coincide exactamente con el modelo necesario, entonces úselo de inmediato.

Si la entidad JPA tiene muy pocas o demasiadas propiedades, utilice una DTO (subclase) y / o una expresión de constructor con una consulta JPQL más específica, si es necesario con un FETCH JOIN explícito. O tal vez con perfiles de búsqueda específicos de Hibernate o grupos de atributos específicos de EclipseLink. De lo contrario, puede causar excepciones de inicialización diferidas en cualquier lugar o consumir más memoria de la necesaria.

El patrón de “abrir sesión a la vista” es un diseño pobre. Esto implica que es posible llevar a cabo la lógica comercial mientras se procesa la respuesta. Esto no funciona muy bien junto con, entre otros, el manejo de excepciones, por lo que la intención es mostrar una página de error personalizada al usuario final. Si se lanza una excepción de negocios a la mitad de la respuesta, por la cual el usuario final ya ha recibido los encabezados de respuesta y una parte del HTML, entonces el servidor no puede borrar la respuesta para mostrar una página de error agradable. Además, llevar a cabo la lógica comercial en los métodos getter es una práctica desaprovechada en JSF según Why JSF llama getters varias veces .

Simplemente prepare exactamente el modelo que necesita la vista a través de las llamadas a los métodos de servicio habituales en los modos gestionados de acción / escucha de frijol, antes de que comience la fase de respuesta al renderizado. Por ejemplo, una situación común es tener una entidad principal existente (no administrada) en las manos con una propiedad de uno a muchos niños cargados de forma diferida, y le gustaría representarla en la vista actual a través de una acción ajax, entonces debería simplemente deje que el método de escucha ajax busque e inicialice en la capa de servicio.

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

Específicamente en JSF UISelectMany componentes, hay otra causa totalmente inesperada de LazyInitializationException : durante el almacenamiento de los elementos seleccionados, JSF necesita volver a crear la colección subyacente antes de rellenarla con los elementos seleccionados; sin embargo, si se trata de una capa de persistencia específica diferida la implementación de la colección cargada, esta excepción también se lanzará. La solución es establecer explícitamente el atributo collectionType del componente UISelectMany al tipo “plain” deseado.

  

Esto se solicita y contesta en detalle en org.hibernate.LazyInitializationException en com.sun.faces.renderkit.html_basic.MenuRenderer.convertSelectManyValuesForModel .

Ver también:

  • LazyInitializationException en selectManyCheckbox en @ManyToMany (fetch = LAZY)
  • ¿Qué es la carga diferida en Hibernate?

Un enfoque muy común es crear un administrador de entidad abierto en el filtro de vista . Spring proporciona uno (marque aquí ).

No veo que estés usando Spring, pero eso no es realmente un problema, puedes adaptar el código de esa clase para tus necesidades. También puede verificar el filtro Abrir sesión en Vista , que hace lo mismo, pero mantiene abierta una sesión de hibernación en lugar de un Administrador de entidades.

Este enfoque puede no ser bueno para su aplicación, hay algunas discusiones en SO sobre este patrón o antipatrón. Link1 . Creo que para la mayoría de las aplicaciones (usuarios pequeños, menos de 20 usuarios concurrentes) esta solución funciona bien.

Editar

Hay una clase de spring que se relaciona mejor con FSF aquí

La carga diferida es una característica importante que puede boost el rendimiento muy bien. Sin embargo, la usabilidad de esto es mucho peor de lo que debería ser.

Especialmente cuando comienzas a tratar con AJAX-Requests, encontrando colecciones no inicializadas, la Anotación es útil para decirle a Hibernate que no cargue esto de inmediato . Hibernate no se ocupa de nada más, pero lanzará una LazyInitializationException hacia usted, tal como lo experimentó.

Mi solución a esto -que podría no ser perfecta o una pesadilla sobre todo- funciona en cualquier escenario, aplicando las siguientes reglas (debo admitir que esto fue escrito desde el principio, pero funciona desde entonces):

Cada Entidad que está usando fetch = FetchType.LAZY tiene que extender LazyEntity y llamar a initializeCollection() en el getter de la collection en cuestión, antes de que se devuelva. (Un validador personalizado se está ocupando de estas restricciones, reportando extensiones faltantes y / o llamadas a initializeCollection )

Ejemplo-Clase (Usuario, que tiene grupos cargados perezosos):

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

Donde la implementación de initializeCollection(Collection collection) ve así. Los comentarios en línea deberían darle una idea de lo que se requiere para cada escenario. El método está sincronizado para evitar 2 sesiones activas que transfieren la propiedad de una entidad, mientras que otra sesión está actualmente obteniendo datos. (Solo aparece cuando concurren Ajax-Requests en la misma instancia).

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

Pero tenga en cuenta que este enfoque necesita que valide SI una Entidad está asociada a la sesión ACTUAL, cada vez que la guarde; de ​​lo contrario, debe mover todo el Árbol de Objetos a la sesión actual nuevamente antes de invocar merge() .

Para Hibernate> = 4.1.6, lea esto https://stackoverflow.com/a/11913404/3252285

Usar el filtro OpenSessionInView (patrón de diseño) es muy útil, pero en mi opinión no resuelve el problema completamente, aquí está el por qué:

Si tenemos una Entidad almacenada en Session o manejada por Session Bean o recuperada de la caché, y una de sus colecciones no se ha inicializado durante la misma solicitud de carga, entonces podríamos obtener la excepción en cualquier momento que la llamemos más tarde , incluso si usamos el patrón de diseño OSIV.

Vamos a detallar el problema:

  • Cualquier Proxy de hibernación debe adjuntarse a una Sesión abierta para que funcione correctamente.
  • Hibernate no ofrece ninguna herramienta ( Listener or Handler ) para volver a conectar el proxy en caso de que su sesión se cierre o se separe de su propia sesión.

¿Por qué Hibernate no ofrece eso? : debido a que no es fácil identificar a qué Sesión, el Proxy debería reagruparse, pero en muchos casos podríamos.

Entonces, ¿cómo volver a conectar el proxy cuando ocurre LazyInitializationException?

En mi ERP , JavassistLazyInitializer Clases: JavassistLazyInitializer y AbstractPersistentCollection , entonces ya no me importa esta Excepción (utilizada desde hace 3 años sin ningún error):

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

y el

 class AbstractPersistentCollection{ private  T withTemporarySessionIfNeeded(LazyInitializationWork 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:

  • No solucioné todas las posibilidades como JTA u otros casos.
  • Esta solución funciona aún mejor cuando activas el caché

No hay soporte estándar para la sesión abierta a la vista en EJB3, vea esta respuesta .

El tipo de búsqueda de asignaciones es solo una opción predeterminada, se puede anular en el momento de la consulta. Esto es un ejemplo:

 select g from Group g fetch join g.students 

Entonces, una alternativa en el EJB3 simple es asegurarse de que todos los datos necesarios para representar la vista se carguen antes de que comience el procesamiento, al consultar explícitamente los datos necesarios.

El patrón de diseño Open Session in View se puede implementar fácilmente en el entorno Java EE (sin dependencia de hibernación, spring u otra cosa fuera de Java EE). En su mayoría es lo mismo que en OpenSessionInView , pero en lugar de la sesión de Hibernate debe usar la transacción JTA

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