2010-07-22 15 views
12

Estoy luchando con el uso real de JPA (Hibernate, EclipseLink, etc.) en una aplicación de escritorio Swing.Cómo evitar el bloqueo de EDT con carga diferida JPA en las aplicaciones de escritorio Swing

JPA parece una gran idea, pero se basa en la carga diferida para mayor eficiencia. La carga diferida requiere que el administrador de entidades exista durante el tiempo de vida de los beans de entidad, y no ofrece control sobre qué subproceso se usa para cargar o de ninguna manera para realizar la carga en segundo plano mientras el EDT comienza con otras cosas. Acceder a una propiedad que está cargada de forma perezosa en el EDT bloqueará la interfaz de usuario de su aplicación en el acceso a la base de datos, sin siquiera la oportunidad de establecer un cursor ocupado. Si la aplicación se ejecuta en wifi/3G o en Internet lenta, puede hacer que parezca que se ha bloqueado.

Para evitar la carga lenta, detener el EDT Tengo que trabajar con entidades separadas. Entonces, si realmente necesito el valor de una propiedad perezosa, todos mis componentes (incluso aquellos que supuestamente deberían desconocer la base de datos) deben estar preparados para manejar excepciones de carga diferida o usar PersistenceUtil para probar el estado de la propiedad. Deben enviar entidades de vuelta al hilo de trabajo de la base de datos para fusionarlas y tener las propiedades cargadas antes de separarlas y devolverlas nuevamente.

Para hacerlo eficiente, mis componentes necesitan saber por adelantado qué propiedades de un frijol se requieren.

Por lo tanto, verá todos estos brillantes tutoriales que muestran cómo desarrollar una aplicación CRUD simple en la plataforma NetBeans, Eclipse RCP, Swing App Framework, etc. usando JPA, pero en realidad los enfoques demostrados violan las prácticas básicas de Swing (no bloquee el EDT) y son completamente inviables en el mundo real.

(más detalle en la escritura hasta aquí: http://soapyfrogs.blogspot.com/2010/07/jpa-and-hibernateeclipselinkopenjpaetc.html)

Hay algunas preguntas relacionadas con respuestas algo útil, pero ninguno de ellos realmente cubren las cuestiones de gestión gestor de toda la vida de bloqueo EDT/carga lenta/entidad juntos.

Lazy/Eager loading strategies in remoting cases (JPA)

¿Cómo están los demás solución de este? ¿Estoy ladrando el árbol equivocado al tratar de usar JPA en una aplicación de escritorio? ¿O hay soluciones obvias que me estoy perdiendo? ¿Cómo evita bloquear el EDT y mantener su aplicación receptiva al usar JPA para el acceso transparente a la base de datos?

Respuesta

3

He encontrado el mismo problema. Mi solución fue desactivar la carga diferida y asegurarme de que todas las entidades estén completamente inicializadas antes de que sean devueltas desde la capa de la base de datos. Las implicaciones de esto es que necesita diseñar cuidadosamente sus entidades para que puedan cargarse en fragmentos. Debe limitar el número de asociaciones x-to-many, de lo contrario, terminará recuperando la mitad de la base de datos en cada búsqueda.

No sé si esta es la mejor solución, pero funciona. JPA ha sido diseñado principalmente para una aplicación de solicitud de respuesta sin estado. Todavía es muy útil en una aplicación Swing con estado: hace que su programa sea portátil para múltiples bases de datos y ahorra muchos códigos repetitivos.Sin embargo, debes ser mucho más cuidadoso usándolo en ese entorno.

+0

Desafortunadamente, no usar este tipo de carga diferida no es viable en este entorno. Tengo 10.000 clientes con cientos de miles de entidades relacionadas en la base de datos. Algunas entidades necesariamente tienen datos grandes o complejos asociados que son requeridos por algunas partes de la aplicación, pero no la mayoría. Este artículo cubre algunos de los problemas de diseño con el uso de JPA/Hibernate/etc en una aplicación de escritorio de 2 niveles: http://blog.schauderhaft.de/2008/09/28/hibernate-sessions-in-two-tier -rich-client-applications/ ... y concluye que, realmente, simplemente no hay buenas soluciones. Me inclino a estar de acuerdo. –

+1

En ese caso, lo único que puedo sugerir es que construya una capa sobre sus entidades que use un mecanismo asincrónico para obtener acceso a las propiedades cargadas diferidas. Entonces tendrías algo así como un método get, pero pasarías una interfaz que recibiría una notificación de la respuesta cuando esté lista. Si solo utilizó esta capa superior de su GUI, evitaría el bloqueo de EDT y evitaría que su GUI tuviera que lidiar con el desordenado proceso de fusión y comprobación del estado de la propiedad. Sin embargo, probablemente requeriría una reescritura significativa de la GUI. –

6

Solo he usado JPA con una base de datos incrustada, donde la latencia en el EDT no era un problema. En un contexto JDBC, he usado SwingWorker para manejar el procesamiento en segundo plano con notificación GUI. No lo he probado con JPA, pero aquí hay un JDBC trivial example.

Adición: Gracias a @Ash por mencionar esto SwingWorkerbug. Una solución alternativa es build from source ha sido submitted.

+3

Como referencia, la versión de SwingWorker en el Sun/Oracle JVMs de JDK6u17 aparentemente tiene un error bastante importante: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6880336. Hay algunas soluciones al final de los comentarios. – Ash

+0

En realidad, hacer el procesamiento de fondo es fácil. El problema surge, con JPA, porque no sabe * cuándo * se requiere ese procesamiento en segundo plano, ya que es todo "transparente" con carga lenta a través de proxies cglib. Cuando trabaje con código compatible con la base de datos y carga de fondo explícita, es fácil saber cuándo llamar al ejecutable de la base de datos en segundo plano con una devolución de llamada Runnable, usar SwingWorker o lo que sea. Con JPA, sin embargo, no sabe que necesita cargar algo hasta que la IU ya esté bloqueada esperando a que se cargue. –

+1

Error de SwingWorker: normalmente utilizo el sistema Executor directamente de todos modos, con un trabajador de base de datos en segundo plano que procesa ejecutables con devoluciones de llamadas a la finalización. Pero gracias por el consejo. –

1

Incluimos todas las operaciones importantes en SwingWorkers que pueden desencadenar la carga diferida de objetos únicos o colecciones. Esto es molesto, pero no puede ser ayudado.

1

¡Siento llegar tarde!

Como cualquier otro desarrollador de swing, supongo que todos llegamos a este tipo de problema cuando se incorpora JPA esperando tratar todos los aspectos de persistencia, encapsulando toda esa lógica en un único nivel aislado, también promoviendo una separación más limpia de preocupaciones, creyendo que es totalmente gratis ... pero la verdad es que definitivamente no lo es.

Como mencionó anteriormente, hay un problema con las entidades independientes que nos hace crear soluciones para resolver este problema. El problema no es solo trabajar con colecciones perezosas, hay un problema al trabajar con la entidad misma, en primer lugar, cualquier cambio que hagamos a nuestra entidad debe reflejarse en el repositorio (y con una separación esto no va a suceder). No soy un experto en esto ... pero trataré de resaltar mis pensamientos sobre esto y exponer varias soluciones (muchas de ellas habían sido previamente anunciadas por otras personas).

Desde el nivel de presentación (es decir, el código donde reside toda la interfaz de usuario e interacciones, esto incluye los controladores) accedemos al nivel del repositorio para realizar operaciones CRUD simples, a pesar del repositorio particular y la presentación particular, creo este es un hecho estándar aceptado por la comunidad. [Supongo que esto es una noción escrita muy bien por Robert Martin en uno de los libros de DDD]

Así que, básicamente, uno puede vagar "si mi entidad está separada, por qué no lo dejo conectado" al hacerlo, se mantendrá sincronizado con mi repositorio y todos los cambios hechos a la entidad se reflejarán "inmediatamente" en mi repositorio. Y sí ... ahí es donde aparece una primera respuesta a este problema.

1) Use un único objeto administrador de entidades y manténgalo abierto desde el inicio de la aplicación hasta el final.

  • A primera vista parece muy simple (y lo es, basta con abrir un EntityManager y almacenar su referencia a nivel mundial y tener acceso a la misma instancia por todas partes en la aplicación)
  • no recomendados por la comunidad, ya que no es seguro mantener abierto un administrador de entidades por mucho tiempo. La conexión del repositorio (por lo tanto session/entityManager) puede caer debido a varias razones.

Así que desprecio es simple, no son las mejores opciones ... así que pasemos a otra solución proporcionada por la API de JPA.

2) Use la carga de campos ansiosa, por lo que no es necesario que se adjunte al repositorio.

  • Esto funciona bien, pero si desea agregar o quitar a una colección de la entidad, o modificar algún valor campo directamente, esto no se refleja en el repositorio .. tendrá que fusionar o actualización manualmente la entidad usando algún método. Por lo tanto, si está trabajando con aplicaciones de varios niveles, desde el nivel de presentación debe incluir una llamada adicional al nivel de depósito. Está contaminando el código del nivel de presentación que se va a adjuntar a un repositorio concreto que funciona con JPA (lo que sucede es el repositorio ¿Es solo una colección de entidades en memoria? ... ¿necesita un repositorio de memoria una llamada adicional para "actualizar" una colección de un objeto ...la respuesta es no, así que esta es una buena práctica pero se hace para hacer que las cosas "finalmente" funcionen)
  • También debes tener en cuenta que lo que sucede es que el gráfico objeto recuperado es demasiado grande para ser almacenado al mismo tiempo en la memoria, por lo que probablemente fallaría. (Exactamente como comentó Craig)

De nuevo .. esto no resuelve el problema.

3) Utilizando el patrón de diseño proxy, puede extraer la interfaz de la entidad (llamémosla EntityInterface) y trabajar en su capa de presentación con esas interfaces (suponiendo que pueda forzar al cliente de su código a esto) . Puede ser genial y usar proxy dinámico o estáticos (realmente no me importa) para crear un ProxyEntity en el nivel del repositorio para devolver el objeto que implementa esa interfaz. Este objeto que devuelve en realidad pertenece a una clase cuyo método de instancia es exactamente el mismo (delegando las llamadas al objeto con proxy) excepto aquellos que funcionan con colecciones que deben "adjuntarse" al repositorio. Esa proxyEntity contiene una referencia al objeto proxy (la entidad misma) necesaria para las operaciones CRUD en el repositorio.

  • Esto resuelve el problema a costa de forzar el uso de interfaces en lugar de clases de dominio simple. No está mal pensar en realidad ... pero también creo que no es ni estándar. Creo que todos queremos usar las clases de dominio. También para cada objeto de dominio tenemos que escribir una interfaz ... ¿qué pasa si el objeto entró en .JAR ... aha! touche! No podemos extraer una interfaz de tiempo de ejecución: S, y antes no podemos crear proxys.

A los efectos de explicar esto mejor anoto un ejemplo de hacer esto ...

en el nivel de dominio (donde reside la clase negocio principal)

@Entity 
public class Bill implements Serializable, BillInterface 
{ 
    private static final long serialVersionUID = 1L; 
    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO) 
    private Long id; 

    @OneToMany(fetch=FetchType.LAZY, cascade = {CascadeType.ALL}, mappedBy="bill") 
    private Collection<Item> items = new HashSet<Item>(); 

    @Temporal(javax.persistence.TemporalType.DATE) 
    private Date date; 

    private String descrip; 

    @Override 
    public Long getId() 
    { 
     return id; 
    } 

    public void setId(Long id) 
    { 
     this.id = id; 
    } 

    public void addItem (Item item) 
    { 
     item.setBill(this); 
     this.items.add(item); 
    } 

    public Collection<Item> getItems() 
    { 
     return items; 
    } 

    public void setItems(Collection<Item> items) 
    { 
     this.items = items; 
    } 

    public String getDescrip() 
    { 
     return descrip; 
    } 

    public void setDescrip(String descrip) 
    { 
     this.descrip = descrip; 
    } 

    public Date getDate() 
    { 
     return date; 
    } 

    public void setDate(Date date) 
    { 
     this.date = date; 
    } 

    @Override 
    public int hashCode() 
    { 
     int hash = 0; 
     hash += (id != null ? id.hashCode() : 0); 
     return hash; 
    } 

    @Override 
    public boolean equals(Object object) 
    { 
     // TODO: Warning - this method won't work in the case the id fields are not set 
     if (!(object instanceof Bill)) 
     { 
      return false; 
     } 
     Bill other = (Bill) object; 
     if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) 
     { 
      return false; 
     } 
     return true; 
    } 

    @Override 
    public String toString() 
    { 
     return "domain.model.Bill[ id=" + id + " ]"; 
    } 

    public BigDecimal getTotalAmount() { 
     BigDecimal total = new BigDecimal(0); 
     for (Item item : items) 
     { 
      total = total.add(item.getAmount()); 
     } 
     return total; 
    } 
} 

artículo es otro objeto de entidad que modela un artículo de una factura (una factura puede contener muchos artículos, un artículo pertenece solo a una y solo una factura).

El BillInterface es simplemente una interfaz que declara todos los métodos de Bill.

En la capa de persistencia coloco la BillProxy ...

El BillProxy tiene este aspecto:

class BillProxy implements BillInterface 
{ 
    Bill bill; // protected so it can be used inside the BillRepository (take a look at the next class) 

    public BillProxy(Bill bill) 
    { 
     this.bill = bill; 
     this.setId(bill.getId()); 
     this.setDate(bill.getDate()); 
     this.setDescrip(bill.getDescrip()); 
     this.setItems(bill.getItems()); 
    } 

    @Override 
    public void addItem(Item item) 
    { 
     EntityManager em = null; 
     try 
     { 
      em = PersistenceUtil.createEntityManager(); 
      this.bill = em.merge(this.bill); // attach the object 
      this.bill.addItem(item); 
     } 
     finally 
     { 
      if (em != null) 
      { 
       em.close(); 
      } 
     } 
    } 



    @Override 
    public Collection<Item> getItems() 
    { 
     EntityManager em = null; 
     try 
     { 
      em = PersistenceUtil.createEntityManager(); 
      this.bill = em.merge(this.bill); // attach the object 
      return this.bill.getItems(); 
     } 
     finally 
     { 
      if (em != null) 
      { 
       em.close(); 
      } 
     } 
    } 

    public Long getId() 
    { 
     return bill.getId(); // delegated 
    } 

    // More setters and getters are just delegated. 
} 

Ahora vamos a echar un vistazo a la BillRepository (vagamente basada en una plantilla dada por el IDE NetBeans)

public class DBBillRepository implementa BillRepository { private EntityManagerFactory emf = null;

public DBBillRepository(EntityManagerFactory emf) 
    { 
     this.emf = emf; 
    } 

    private EntityManager createEntityManager() 
    { 
     return emf.createEntityManager(); 
    } 

    @Override 
    public void create(BillInterface bill) 
    { 
     EntityManager em = null; 
     try 
     { 
      em = createEntityManager(); 
      em.getTransaction().begin(); 
      bill = ensureReference (bill); 
      em.persist(bill); 
      em.getTransaction().commit(); 
     } 
     finally 
     { 
      if (em != null) 
      { 
       em.close(); 
      } 
     } 
    } 

    @Override 
    public void update(BillInterface bill) throws NonexistentEntityException, Exception 
    { 
     EntityManager em = null; 
     try 
     { 
      em = createEntityManager(); 
      em.getTransaction().begin(); 
      bill = ensureReference (bill); 
      bill = em.merge(bill); 
      em.getTransaction().commit(); 
     } 
     catch (Exception ex) 
     { 
      String msg = ex.getLocalizedMessage(); 
      if (msg == null || msg.length() == 0) 
      { 
       Long id = bill.getId(); 
       if (find(id) == null) 
       { 
        throw new NonexistentEntityException("The bill with id " + id + " no longer exists."); 
       } 
      } 
      throw ex; 
     } 
     finally 
     { 
      if (em != null) 
      { 
       em.close(); 
      } 
     } 
    } 

    @Override 
    public void destroy(Long id) throws NonexistentEntityException 
    { 
     EntityManager em = null; 
     try 
     { 
      em = createEntityManager(); 
      em.getTransaction().begin(); 
      Bill bill; 
      try 
      { 
       bill = em.getReference(Bill.class, id); 
       bill.getId(); 
      } 
      catch (EntityNotFoundException enfe) 
      { 
       throw new NonexistentEntityException("The bill with id " + id + " no longer exists.", enfe); 
      } 
      em.remove(bill); 
      em.getTransaction().commit(); 
     } 
     finally 
     { 
      if (em != null) 
      { 
       em.close(); 
      } 
     } 
    } 

    @Override 
    public boolean createOrUpdate (BillInterface bill) 
    { 
     if (bill.getId() == null) 
     { 
      create(bill); 
      return true; 
     } 
     else 
     { 
      try 
      { 
       update(bill); 
       return false; 
      } 
      catch (Exception e) 
      { 
       throw new IllegalStateException(e.getMessage(), e); 
      } 
     } 
    } 

    @Override 
    public List<BillInterface> findEntities() 
    { 
     return findBillEntities(true, -1, -1); 
    } 

    @Override 
    public List<BillInterface> findEntities(int maxResults, int firstResult) 
    { 
     return findBillEntities(false, maxResults, firstResult); 
    } 

    private List<BillInterface> findBillEntities(boolean all, int maxResults, int firstResult) 
    { 
     EntityManager em = createEntityManager(); 
     try 
     { 
      Query q = em.createQuery("select object(o) from Bill as o"); 
      if (!all) 
      { 
       q.setMaxResults(maxResults); 
       q.setFirstResult(firstResult); 
      } 
      List<Bill> bills = q.getResultList(); 
      List<BillInterface> res = new ArrayList<BillInterface> (bills.size()); 
      for (Bill bill : bills) 
      { 
       res.add(new BillProxy(bill)); 
      } 
      return res; 
     } 
     finally 
     { 
      em.close(); 
     } 
    } 

    @Override 
    public BillInterface find(Long id) 
    { 
     EntityManager em = createEntityManager(); 
     try 
     { 
      return new BillProxy(em.find(Bill.class, id)); 
     } 
     finally 
     { 
      em.close(); 
     } 
    } 

    @Override 
    public int getCount() 
    { 
     EntityManager em = createEntityManager(); 
     try 
     { 
      Query q = em.createQuery("select count(o) from Bill as o"); 
      return ((Long) q.getSingleResult()).intValue(); 
     } 
     finally 
     { 
      em.close(); 
     } 
    } 

    private Bill ensureReference (BillInterface bill) { 
     if (bill instanceof BillProxy) { 
      return ((BillProxy)bill).bill; 
     } 
     else 
      return (Bill) bill; 
    } 

} 

Como habrá notado, la clase se llama en realidad DBBillRepository ... que es porque no puede haber varios repositorios (memoria, archivo, netos, ??) tipos de los demás un tiers no hay necesidad de conocer de qué tipo de repositorio estoy trabajando.

También existe un método interno ensureReference utilizado para obtener el objeto de la factura real, solo para el caso en que pasemos un objeto proxy de la capa de presentación. Y hablando de la capa de presentación, solo usamos BillInterfaces en lugar de Bill y todo funcionará bien.

En cierta clase controlador (o un método de devolución de llamada, en el caso de una aplicación Swing), podemos trabajar de la siguiente manera ...

BillInterface bill = RepositoryFactory.getBillRepository().find(1L); 
bill.addItem(new Item(...)); // this will call the method of the proxy 
Date date = bill.getDate(); // this will deleagte the call to the proxied object "hidden' behind the proxy. 
bill.setDate(new Date()); // idem before 
RepositoryFactory.getBillRepository().update(bill); 

Este es otro enfoque, a costa de forzar el uso interfaces.

4) Bueno en realidad hay una cosa más que podemos hacer para evitar trabajar con interfaces ... usando somekind del objeto proxy degenerado ...

Podríamos escribir un BillProxy esta manera:

class BillProxy extends Bill 
{ 
    Bill bill; 

    public BillProxy (Bill bill) 
    { 
     this.bill = bill; 
     this.setId(bill.getId()); 
     this.setDate(bill.getDate()); 
     this.setDescrip(bill.getDescrip()); 
     this.setItems(bill.getItems()); 
    } 

    @Override 
    public void addItem(Item item) 
    { 
     EntityManager em = null; 
     try 
     { 
      em = PersistenceUtil.createEntityManager(); 
      this.bill = em.merge(this.bill); 
      this.bill.addItem(item); 
     } 
     finally 
     { 
      if (em != null) 
      { 
       em.close(); 
      } 
     } 
    } 



    @Override 
    public Collection<Item> getItems() 
    { 
     EntityManager em = null; 
     try 
     { 
      em = PersistenceUtil.createEntityManager(); 
      this.bill = em.merge(this.bill); 
      return this.bill.getItems(); 
     } 
     finally 
     { 
      if (em != null) 
      { 
       em.close(); 
      } 
     } 
    } 

} 

Entonces en el nivel de presentación podríamos usar la clase Bill, también en DBBillRepository sin usar la interfaz, por lo que obtenemos una restricción menos :). No estoy seguro de si esto es bueno ... pero funciona, y también mantiene el código no contaminado al agregar llamadas adicionales a un tipo de repositorio específico.

Si quieres puedo enviarte mi aplicación completa y puedes verlo por ti mismo.

Además, hay varias publicaciones que explican lo mismo, que son muy interesantes de leer.

También voy a designar a esta referencias que todavía no leído completamente, pero parece prometedor.

http://javanotepad.blogspot.com/2007/08/managing-jpa-entitymanager-lifecycle.html http://docs.jboss.org/hibernate/orm/4.0/hem/en-US/html/transactions.html

Bueno llegamos al final de la respuesta aquí ... Sé que es tan largo y probablemente somekind del dolor a leer todo esto: D (hecho más complicado por mis errores gramaticales jeje) pero de todos modos espero que nos ayude ** a encontrar una solución más estable a un problema que simplemente no podemos borrar jeje.

Saludos.

Victor !!!

Cuestiones relacionadas