简体   繁体   English

如何避免在Swing桌面应用程序中使用JPA延迟加载来阻止EDT

[英]How to avoid blocking EDT with JPA lazy loading in Swing desktop apps

I'm struggling with real-world use of JPA (Hibernate, EclipseLink, etc) in a Swing desktop application. 我正在努力在Swing桌面应用程序中实际使用JPA(Hibernate,EclipseLink等)。

JPA seems like a great idea, but relies on lazy loading for efficiency. JPA似乎是一个好主意,但依赖于延迟加载以提高效率。 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. 延迟加载需要实体管理器在实体bean的生命周期中存在,并且无法控制用于加载的线程或在EDT开始使用其他东西时在后台进行加载的任何方式。 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. 访问恰好在EDT上延迟加载的属性将阻止应用程序在数据库访问时使用UI,甚至无法设置忙碌光标。 If the app is running on wifi/3G or slow Internet that can make it look like it has crashed. 如果应用程序在wifi / 3G或慢速上网运行,可能会让它看起来像已经崩溃。

To avoid lazy loading stalling the EDT I have to work with detached entities. 为了避免延迟加载停止EDT,我必须使用分离的实体。 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. 然后,如果我真的需要一个惰性属性的值,我所有的组件(甚至应该是那些应该不知道数据库的组件)必须准备好处理延迟加载异常或使用PersistenceUtil来测试属性状态。 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. 为了提高效率,我的组件需要事先知道需要bean的哪些属性。

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. 因此,您将看到所有这些闪亮的教程演示如何使用JPA在NetBeans平台,Eclipse RCP,Swing App Framework等上创建一个简单的CRUD应用程序,但实际上这些方法违反了基本的Swing实践(不要阻止) EDT)并且在现实世界中完全不可行。

( More detail in write-up here: http://soapyfrogs.blogspot.com/2010/07/jpa-and-hibernateeclipselinkopenjpaetc.html ) (更多细节请写在这里: 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. 有一些相关的问题有一些有用的响应,但它们都没有真正涵盖edt阻塞/延迟加载/实体管理器生命周期管理问题。

Lazy/Eager loading strategies in remoting cases (JPA) 远程病例中的Lazy / Eager加载策略(JPA)

How are others solving this? 别人怎么解决这个问题? Am I barking the wrong tree by trying to use JPA in a desktop app? 我是否试图在桌面应用程序中使用JPA来吠叫错误的树? 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? 在使用JPA进行透明数据库访问时,您是如何避免阻止EDT并保持应用程序响应的?

I've only used JPA with an embedded database, where latency on the EDT wasn't a problem. 我只使用了带有嵌入式数据库的JPA,其中EDT上的延迟不是问题。 In a JDBC context, I've used SwingWorker to handle background processing with GUI notification. 在JDBC上下文中,我使用SwingWorker通过GUI通知处理后台处理。 I haven't tried it with JPA, but here's a trivial JDBC example . 我没有尝试使用JPA,但这是一个简单的JDBC 示例

Addendum: Thanks to @Ash for mentioning this SwingWorker bug . 附录:感谢@Ash提到这个SwingWorker 错误 A workaround is to build from source has been submitted . 解决方法是从源代码构建提交

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. 您必须限制x-to-many关联的数量,否则最终会在每次提取时检索一半数据库。

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. JPA主要用于请求 - 响应无状态应用程序。 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. 它在有状态的Swing应用程序中仍然非常有用 - 它使您的程序可移植到多个数据库并节省大量样板代码。 However, you have to be much more careful using it in that environment. 但是,在该环境中使用它必须更加小心。

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. 正如任何其他摇摆开发人员一样,我想我们都会在JPA被纳入时遇到这种问题,希望通过将所有逻辑封装在单个隔离层中来处理所有持久性方面,同时促进更清晰的关注点分离,相信它是完全免费的...但事实是,它绝对不是。

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. 从表示层(即,所有用户界面和交互的代码,包括控制器)我们访问存储库层来执行简单的CRUD操作,尽管特定的存储库和特定的表示,我认为这是一个标准社区接受的事实。 [I guess this a notion written down very well by Robert Martin in one of DDD books] [我想这是罗伯特·马丁在其中一本DDD书中写得很好的概念]

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. 1)使用单个实体管理器对象,并从应用程序的开头到结尾保持打开状态。

  • 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) 一目了然看起来很简单(只需打开一个EntityManager并全局存储它的引用并在应用程序的任何地方访问同一个实例)
  • 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. 所以鄙视它很简单,它不是最好的选择....所以让我们转到JPA API提供的另一个解决方案。

2) Use eager loading of fields, so there is no need to be attached to the repository. 2)使用热切的字段加载,因此不需要附加到存储库。

  • 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) 因此,如果您正在使用多层应用程序,那么从表示层开始,您必须包含对存储库层的额外调用,这会污染表示层的代码以附加到与JPA一起使用的具体存储库(存储库会发生什么)只是内存中实体的集合?...内存存储库是否需要额外调用来“更新”对象的集合...答案是否定的,所以这是一个很好的做法,但它是为了让事情“终于”起作用)
  • 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). 3)使用代理设计模式,您可以提取实体的接口(让我们称之为EntityInterface)并使用这些接口在您的表示层中工作(假设您实际上可以强制您的代码的客户端)。 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. 您可以很酷并使用动态代理或静态代理(实际上并不关心)在存储库层中创建ProxyEntity以返回实现该接口的对象。 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. 该proxyEntity包含对存储库上的CRUD操作所必需的代理对象(实体本身)的引用。

  • This resolves the problem at the cost of forcing use Interfaces instead of plain domain classes. 这解决了这个问题,代价是强制使用Interfaces而不是普通的域类。 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! 此外,对于每个域对象,我们必须编写一个接口......如果对象进来,会发生什么... .JAR ...啊哈! touche! 德勤! We cannon't extract an interface an runtime :S, and therebefore we cannot create proxys. 我们无法在运行时提取接口:S,因此我们无法创建代理。

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). Item是另一个对Bill的项目进行建模的实体对象(Bill可以包含许多Items,Item只属于一个且只有一个Bill)。

The BillInterface is simply an interface declaring all Bill Methods. BillInterface只是一个声明所有Bill方法的接口。

On the persistence tier i place the BillProxy... 在持久层上我放置BillProxy ...

The BillProxy has this look : BillProxy有这样的外观:

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) 现在让我们来看看BillRepository(松散地基于NetBeans IDE提供的模板)

public class DBBillRepository implements BillRepository { private EntityManagerFactory emf = null; 公共类DBBillRepository实现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. 正如你所注意到的那样,这个类实际上被称为DBBillRepository ......这是因为可以有多个存储库(内存,文件,网络,??)类型来自其他层,不需要知道我正在使用哪种存储库。

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. 还有一个ensureReference内部方法用于获取真实的bill对象,仅用于我们从表示层传递代理对象的情况。 And talking about presentation layer we just use BillInterfaces instead of Bill an all will work well. 谈论表示层我们只使用BillInterfaces而不是Bill,一切都会运行良好。

In some controller class (or a callback method, in case of a SWING app), we can work the following way... 在某些控制器类(或回调方法,如果是SWING应用程序),我们可以按以下方式工作......

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... 4)实际上还有一件事我们可以做以避免使用接口...使用一些退化的代理对象......

We could write a BillProxy this way : 我们可以这样写一个BillProxy:

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 :). 所以在表示层中我们可以使用Bill类,也可以在DBBillRepository中使用而不使用接口,因此我们得到一个约束:)。 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. 此外,还有几篇文章解释了相同的内容,这些内容非常有趣。

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 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. 好吧,我们到达答案的最后......我知道它是如此之长,可能是一些痛苦的阅读所有这些:D(由于我的语法错误使得更复杂)但无论如何希望它有助于我们找到一个更稳定的解决方案来解决我们无法抹去的问题。

Greetings. 问候。

Victor!!! 胜利者!!!

We wrap every significant operation into SwingWorkers which may trigger lazy-loading of single-objects or collections. 我们将每个重要的操作包装到SwingWorkers中,这可能会触发单个对象或集合的延迟加载。 This is annoying, but cannot be helped. 这很烦人,但无法帮助。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM