How to avoid blocking EDT with JPA lazy loading in

2019-03-25 03:34发布

问题:

I'm struggling with real-world use of JPA (Hibernate, EclipseLink, etc) in a Swing desktop application.

JPA seems like a great idea, but relies on lazy loading for efficiency. Lazy loading requires the entity manager exist for the lifetime of the entity beans, and offers no control over what thread is used for loading or any way to do the loading in the background while the EDT gets on with other things. Accessing a property that happens to be lazily loaded on the EDT will block your app's UI on database access, without even the opportunity to set a busy cursor. If the app is running on wifi/3G or slow Internet that can make it look like it has crashed.

To avoid lazy loading stalling the EDT I have to work with detached entities. Then, if I actually need the value of a lazy property all my components (even those that should supposedly be able to be unaware of the database) have to be prepared to handle lazy loading exceptions or use PersistenceUtil to test for property state. They have to dispatch entities back to the database worker thread to be merged and have properties loaded before being detached and returned again.

To make that efficient, my components need to know in advance what properties of a bean will be required.

So, you'll see all these shiny tutorials demonstrating how to whip up a simple CRUD app on the NetBeans Platform, Eclipse RCP, Swing App Framework, etc using JPA, but in reality the approaches demonstrated violate basic Swing practices (don't block the EDT) and are completely non-viable in the real world.

( More detail in write-up here: http://soapyfrogs.blogspot.com/2010/07/jpa-and-hibernateeclipselinkopenjpaetc.html )

There are some related questions with somewhat helpful responses, but none of them really cover the edt blocking / lazy loading / entity manager lifetime management issues together.

Lazy/Eager loading strategies in remoting cases (JPA)

How are others solving this? Am I barking the wrong tree by trying to use JPA in a desktop app? Or are there obvious solutions I'm missing? How are you avoiding blocking the EDT and keeping your app responsive while using JPA for transparent database access?

回答1:

I have encountered the same problem. My solution was to disable lazy loading and ensure that all entities are fully initialised before they are returned from the database layer. The implications of this is that you need to carefully design your entities so that they can be loaded in chunks. You have to limit the number of x-to-many associations, otherwise you end up retrieving half the database on every fetch.

I do not know if this is the best solution but it does work. JPA has been designed primarily for a request-response stateless app. It is still very useful in a stateful Swing app - it makes your program portable to multiple databases and saves a lot of boilerplate code. However, you have to be much more careful using it in that environment.



回答2:

I've only used JPA with an embedded database, where latency on the EDT wasn't a problem. In a JDBC context, I've used SwingWorker to handle background processing with GUI notification. I haven't tried it with JPA, but here's a trivial JDBC example.

Addendum: Thanks to @Ash for mentioning this SwingWorkerbug. A workaround is to build from source has been submitted.



回答3:

We wrap every significant operation into SwingWorkers which may trigger lazy-loading of single-objects or collections. This is annoying, but cannot be helped.



回答4:

Sorry being late!

As any other swing developer, i guess we all came to this kind of problem when JPA is incorporated hoping to deal with all persistence aspects, by encapsulating all of that logic in single isolated tier, also promoting a more clean separation of concerns, believing that it is totally free...but the truth is that it's definitely not.

As you stated before there are a problem with detached entities that makes us create workarounds to solve this problem. The problem is not only working with lazy collections, there is a problem working with the entity itself, first at all, any changes that we do to our entity must be reflected to repository (and with a detached this is not going to happen). I am not an expert on this.. but i will try to highlight my thoughts on this and expose several solutions (many of them had been previously announced by other folks).

From the presentation tier (that is, the code where resides all the user interface and interactions , this includes the controllers) we access the repository tier to do simple CRUD operations, despite the particular repository and the particular presentation, i think this is a standard fact accepted by the community. [I guess this a notion written down very well by Robert Martin in one of DDD books]

So, basically one can wander "if my entity is detached, why I not leave it attached" doing so, it will stay synchronized with my repository an all changes done to the entity will be reflected "immediately" to my repository. And yes.... that is where a first answer appears to this problem..

1) Use a single entity manager object and keep it open from the start of the app to the end.

  • At a glance it seems very simple (and it is, just open an EntityManager and store its reference globally and access the same instance everywhere in the application)
  • Not recommended by the community as it not safe to keep an entity manager open for too long. The repository connection (hence session/entityManager) may drop due to various reasons.

So despise it's simple, it's not the best options.... so let's move to another solution provided by the JPA API.

2) Use eager loading of fields, so there is no need to be attached to the repository.

  • This works well, but if you want to add or remove to a collection of the entity, or modify some field value directly, this will not be reflected in the repository.. you will have to manually merge or update the entity by using some method. Therebefore, if you are working with multi tier app where from the presentation tier you must include an extra call to repository tier you are contaminating the code of the presentation tier to be attach to a concrete repository that works with JPA (what happens is the repository is just a collection of entities in memory? ... does a memory repository need an extra call to "update" a collection of an object... the answer is no, so this is good practice but it is done for the sake of make thing "finally" works)
  • Also you have to consider to what happens is the object graph retrieved is too big to be stored at the same time in memory, so it would probably fail. (Exactly as Craig commented)

Again.. this not resolve the problem.

3) Using the proxy design pattern, you could extract the Interface of the Entity (let's call it EntityInterface) and work in your presentation layer with those interfaces (supposing that you actually can force the client of your code to this). You can be cool and use dynamic proxy or static ones (really don't care) to create a ProxyEntity in the repository tier to return object that implement that interface. This object that return actually belongs to a class whose instance method are exactly the same (delegating the calls to the proxied object) except for those that works with collections that need to be "attached" to the repostory. That proxyEntity contains a reference to the proxied object (the entity itself) necessary to the CRUD operations on the repository.

  • This resolves the problem at the cost of forcing use Interfaces instead of plain domain classes. Not a bad think actually... but also i guess is neither and standard. I think we all want to use the domain classes. Also for every domain object we have to write an interface... what happens if the object came in .JAR... aha! touche! We cannon't extract an interface an runtime :S, and therebefore we cannot create proxys.

For the purposes of explain this better i write down an example of doing this...

On the domain tier (where the core business class resides)

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

Item is another entity object modelling an item of a Bill (a Bill can contains many Items, an Item belongs only to one and only one Bill).

The BillInterface is simply an interface declaring all Bill Methods.

On the persistence tier i place the BillProxy...

The BillProxy has this look :

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

Now let's take a look to the BillRepository (loosely based on a template given by NetBeans IDE)

public class DBBillRepository implements 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;
    }

}

as you noticed, the class is actually called DBBillRepository... that is because there can be several repositories (memory, file, net, ??) types an from others tiers there is no need to know from what kind of repository i am working.

There is also a ensureReference internal method used by to get the real bill object, just for the case we pass a proxy object from the presentation layer. And talking about presentation layer we just use BillInterfaces instead of Bill an all will work well.

In some controller class (or a callback method, in case of a SWING app), we can work the following way...

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

This is one more approach, at the cost of forcing using interfaces.

4) Well there is actually one more thing that we can do to avoid working with interfaces... using somekind of degenerated proxy object...

We could write a BillProxy this way :

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

}

So in the presentation tier we could use the Bill class, also in the DBBillRepository without using the interface, so we get one constraint less :). I am not sure if this is good... but it works, and also maintains the code not polluted by adding additional calling to a specific repository type.

If you want i can send you my entire app and you can see for yourself.

Also, there are several post explaining the same thing, that are very interesting to read.

  • Swing and lazy loading components
  • jpa on a Desktop SWING Application
  • Should JPA Entity Manager be closed?
  • Equals and hashcode in conjunction with collections

Also i will appoint this references that i still don't read completely, but looks promising.

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

Well we reach the end of the answer here... i know that it is so long and probably somekind of pain to read all of this :D (made more complicated by my grammatical errors jeje) but anyway hope it helps **us to find a more stable solution to a problem that we just cannot erase jeje.

Greetings.

Victor!!!