¡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 !!!
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. –
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. –