Как избежать блокировки EDT с помощью отложенной загрузки JPA в настольных приложениях Swing

Я борюсь с реальным использованием JPA (Hibernate, EclipseLink и т. Д.) В настольном приложении Swing.

JPA кажется отличной идеей, но для эффективности полагается на ленивую загрузку. Ленивая загрузка требует, чтобы диспетчер сущностей существовал в течение всего времени существования объектных компонентов, и не дает никакого контроля над тем, какой поток используется для загрузки, или каким-либо способом выполнять загрузку в фоновом режиме, в то время как EDT занимается другими делами. Доступ к свойству, которое лениво загружается в EDT, заблокирует пользовательский интерфейс вашего приложения при доступе к базе данных, даже без возможности установить курсор занятости. Если приложение работает через Wi-Fi / 3G или медленный Интернет, это может выглядеть как сбой.

Чтобы избежать остановки EDT из-за ленивой загрузки, я должен работать с отдельными объектами. Затем, если мне действительно нужно значение ленивого свойства, все мои компоненты (даже те, которые предположительно должны не знать о базе данных) должны быть готовы обрабатывать исключения отложенной загрузки или использовать PersistenceUtil для проверки состояния свойства. Они должны отправлять объекты обратно в рабочий поток базы данных для объединения и иметь загруженные свойства, прежде чем они будут отсоединены и возвращены снова.

Чтобы сделать это эффективным, мои компоненты должны знать заранее, какие свойства компонента потребуются.

Итак, вы увидите все эти блестящие руководства, демонстрирующие, как создать простое приложение CRUD на платформе NetBeans, Eclipse RCP, Swing App Framework и т. Д. С помощью JPA, но на самом деле продемонстрированные подходы нарушают базовые методы Swing (не блокируйте EDT) и совершенно нежизнеспособны в реальном мире.

(Подробнее см. Здесь: http://soapyfrogs.blogspot.com/2010/07/jpa-and-hibernateeclipselinkopenjpaetc.html).

Есть несколько связанных вопросов с несколько полезными ответами, но ни один из них не охватывает вместе проблемы блокировки edt / ленивой загрузки / управления временем жизни менеджера сущностей.

Стратегии отложенной / нетерпеливой загрузки в случаях удаленного взаимодействия (JPA)

Как другие решают это? Я лаю не на то дерево, пытаясь использовать JPA в настольном приложении? Или есть очевидные решения, которые мне не хватает? Как вы избегаете блокировки EDT и сохраняете отзывчивость вашего приложения при использовании JPA для прозрачного доступа к базе данных?


person Craig Ringer    schedule 22.07.2010    source источник


Ответы (4)


Я столкнулся с той же проблемой. Мое решение заключалось в том, чтобы отключить отложенную загрузку и убедиться, что все сущности полностью инициализированы, прежде чем они будут возвращены из уровня базы данных. Следствием этого является то, что вам необходимо тщательно спроектировать свои сущности, чтобы их можно было загружать по частям. Вы должны ограничить количество ассоциаций x-to-many, иначе вы получите половину базы данных при каждой выборке.

Не знаю, лучшее ли это решение, но оно работает. JPA был разработан в первую очередь для приложений без отслеживания состояния типа запрос-ответ. Он по-прежнему очень полезен в приложении Swing с отслеживанием состояния - он делает вашу программу переносимой в несколько баз данных и сохраняет много шаблонного кода. Однако вы должны быть намного осторожнее, используя его в этой среде.

person Russ Hayward    schedule 22.07.2010
comment
К сожалению, в этой среде не использовать какую-либо форму отложенной загрузки. У меня 10 000 клиентов с сотнями тысяч связанных сущностей в базе данных. С некоторыми объектами обязательно связаны большие или сложные данные, которые требуются для некоторых частей приложения, но не для большинства. В этой статье рассматриваются некоторые проблемы проектирования с использованием JPA / Hibernate / и т. Д. В двухуровневом настольном приложении: blog.schauderhaft.de/2008/09/28/ ... и приходит к выводу, что на самом деле нет ничего хорошего решения. Я склонен согласиться. - person Craig Ringer; 27.07.2010
comment
В этом случае единственное, что я могу предложить, - это создать слой поверх своих сущностей, который использует асинхронный механизм для получения доступа к ленивым загружаемым свойствам. Таким образом, у вас будет что-то вроде метода get, но вы передадите ему интерфейс, который будет получать уведомление об ответе, когда он будет готов. Если бы вы использовали только этот верхний уровень из своего графического интерфейса, это предотвратило бы блокировку EDT и избавило бы ваш графический интерфейс от необходимости иметь дело с беспорядочным процессом проверки состояния свойств и слияния. Однако это, вероятно, потребует значительного переписывания графического интерфейса. - person Russ Hayward; 27.07.2010

Я использовал только JPA со встроенной базой данных, где задержка на EDT не была проблемой. В контексте JDBC я использовал SwingWorker для обработки фоновой обработки с уведомлением графического интерфейса. Я не пробовал это с JPA, но вот простой пример JDBC.

Приложение: Спасибо @Ash за упоминание об этой SwingWorker ошибке. Обходной путь - сборка из исходного кода была отправлено.

person trashgod    schedule 22.07.2010
comment
Для справки: версия SwingWorker в JVM Sun / Oracle из JDK6u17, по-видимому, имеет довольно серьезную ошибку: bugs.sun.com/bugdatabase/view_bug.do?bug_id=6880336. В конце комментариев есть обходные пути. - person Ash; 22.07.2010
comment
На самом деле выполнение фоновой обработки - это не проблема. Проблема возникает с JPA, потому что вы не знаете, когда эта фоновая обработка требуется, поскольку все это прозрачно с отложенной загрузкой через прокси cglib. Когда вы работаете с кодом, поддерживающим базу данных, и явной фоновой загрузкой, легко узнать, когда вызывать фонового работника базы данных с помощью обратного вызова Runnable, использовать SwingWorker или что-то еще. Однако с JPA вы не знаете, что вам нужно что-то загружать, пока пользовательский интерфейс не будет заблокирован, ожидая его загрузки. - person Craig Ringer; 22.07.2010
comment
Повторите ошибку SwingWorker: я обычно в любом случае использую систему Executor напрямую, а фоновый работник базы данных обрабатывает исполняемые файлы с обратными вызовами по завершении. Но спасибо за подсказку. - person Craig Ringer; 22.07.2010
comment
@trashgod Спасибо за пример, человек ... Я реализую дизайн с использованием Spring Data для обеспечения устойчивости, а архитектура контроллера немного сложна с SwingWorkers ... - person Edward J Beckett; 03.02.2015

Мы оборачиваем каждую важную операцию в SwingWorkers, которая может вызвать отложенную загрузку отдельных объектов или коллекций. Это раздражает, но ничего не поделаешь.

person sola    schedule 20.12.2010

Извините за опоздание!

Как и любой другой разработчик Swing, я полагаю, что все мы пришли к подобной проблеме, когда был включен JPA, надеясь справиться со всеми аспектами персистентности, инкапсулируя всю эту логику на одном изолированном уровне, а также способствуя более четкому разделению проблем, полагая, что это совершенно бесплатно ... но правда в том, что это определенно не так.

Как вы заявили ранее, существует проблема с отдельными объектами, которая заставляет нас создавать обходные пути для решения этой проблемы. Проблема не только в работе с ленивыми коллекциями, есть проблема в работе с самой сущностью, во-первых, любые изменения, которые мы делаем с нашей сущностью, должны отражаться в репозитории (а с отсоединением этого не произойдет). Я не эксперт в этом вопросе ... но я постараюсь выделить свои мысли по этому поводу и представить несколько решений (многие из них были ранее анонсированы другими людьми).

С уровня представления (то есть кода, в котором находится весь пользовательский интерфейс и взаимодействия, включая контроллеры) мы получаем доступ к уровню репозитория для выполнения простых операций CRUD, несмотря на конкретный репозиторий и конкретную презентацию, я думаю, что это стандарт факт, принятый сообществом. [Я думаю, это понятие очень хорошо записано Робертом Мартином в одной из книг DDD]

Таким образом, в основном можно блуждать «если моя сущность отсоединена, почему я не оставляю ее прикрепленной», делая это, она останется синхронизированной с моим репозиторием, и все изменения, внесенные в сущность, будут «немедленно» отражены в моем репозитории. И да .... вот где появляется первый ответ на эту проблему ..

1) Используйте единый объект диспетчера сущностей и держите его открытым от начала приложения до конца.

  • На первый взгляд это кажется очень простым (и это так, просто откройте EntityManager и сохраните его ссылку глобально и получите доступ к одному и тому же экземпляру повсюду в приложении)
  • Не рекомендуется сообществом, так как держать менеджера объекта открытым слишком долго небезопасно. Соединение с репозиторием (следовательно, session / entityManager) может разорваться по разным причинам.

Так что презирайте это просто, это не самые лучшие варианты ... так что давайте перейдем к другому решению, предоставляемому JPA API.

2) Используйте активную загрузку полей, чтобы не было необходимости прикрепляться к репозиторию.

  • Это работает хорошо, но если вы хотите добавить или удалить в коллекцию объекта, или изменить какое-либо значение поля напрямую, это не будет отражено в репозитории .. вам придется вручную объединить или обновить объект, используя какой-либо метод . Следовательно, если вы работаете с многоуровневым приложением, где из уровня представления вы должны включить дополнительный вызов уровня репозитория, вы загрязняете код уровня представления, который нужно прикрепить к конкретному репозиторию, который работает с JPA (происходит то, что репозиторий это просто набор объектов в памяти? ... требуется ли репозиторию памяти дополнительный вызов для "обновления" коллекции объекта ... ответ отрицательный, так что это хорошая практика, но это делается ради заставить вещь "наконец-то" заработать)
  • Также вы должны учитывать, что происходит, если полученный граф объектов слишком велик для одновременного сохранения в памяти, поэтому он, вероятно, не удастся. (Точно так, как прокомментировал Крейг)

Опять же .. это не решает проблему.

3) Используя шаблон проектирования прокси, вы можете извлечь интерфейс объекта (назовем его EntityInterface) и работать на уровне представления с этими интерфейсами (предполагая, что вы действительно можете заставить клиента вашего кода сделать это). Вы можете быть крутым и использовать динамический прокси или статические (на самом деле все равно), чтобы создать ProxyEntity на уровне репозитория, чтобы вернуть объект, реализующий этот интерфейс. Этот возвращаемый объект фактически принадлежит классу, метод экземпляра которого точно такой же (делегирование вызовов проксируемому объекту), за исключением тех, которые работают с коллекциями, которые необходимо «прикрепить» к репостори. Этот proxyEntity содержит ссылку на проксируемый объект (сам объект), необходимый для операций CRUD в репозитории.

  • Это решает проблему за счет принудительного использования интерфейсов вместо простых классов домена. На самом деле неплохая мысль ... но я думаю, что это ни то, ни другое стандартное. Я думаю, что все мы хотим использовать классы предметной области. Также для каждого объекта домена мы должны написать интерфейс ... что произойдет, если объект попал в .JAR ... ага! прикоснуться! Мы не можем извлечь интерфейс во время выполнения: S, и поэтому мы не можем создавать прокси.

Чтобы лучше объяснить это, я запишу пример того, как это сделать ...

На уровне домена (где проживает основной бизнес-класс)

@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 - это еще один объект сущности, моделирующий элемент Bill (Bill может содержать много элементов, Item принадлежит только одному и только одному Bill).

BillInterface - это просто интерфейс, объявляющий все методы Bill.

На уровне постоянства я помещаю BillProxy ...

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

Теперь давайте взглянем на BillRepository (основанный на шаблоне, предоставленном IDE NetBeans)

открытый класс 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;
    }

}

как вы заметили, класс на самом деле называется DBBillRepository ... это потому, что может быть несколько типов репозиториев (память, файл, сеть, ??) и из других уровней, нет необходимости знать, из какого репозитория я работаю.

Существует также ensureReference внутренний метод, используемый для получения реального объекта счета, на тот случай, если мы передаем прокси-объект из уровня представления. Говоря о слое представления, мы просто используем BillInterfaces вместо Bill, и все будет хорошо.

В некотором классе контроллера (или методе обратного вызова, в случае приложения 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);

Это еще один подход за счет принудительного использования интерфейсов.

4) Ну, на самом деле есть еще одна вещь, которую мы можем сделать, чтобы избежать работы с интерфейсами ... используя какой-то выродившийся прокси-объект ...

Мы могли бы написать 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();
            }
        }
    }

}

Таким образом, на уровне представления мы могли бы использовать класс Bill, также в DBBillRepository, без использования интерфейса, поэтому мы получаем на одно ограничение меньше :). Я не уверен, что это хорошо ... но он работает, а также поддерживает код, не загрязненный добавлением дополнительных вызовов к определенному типу репозитория.

Если вы хотите, я могу отправить вам все свое приложение, и вы сможете убедиться в этом сами.

Также есть несколько постов, объясняющих одно и то же, которые очень интересно читать.

Также я назначу эти ссылки, которые я еще не прочитал полностью, но выглядит многообещающим.

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

Что ж, мы подошли к концу ответа здесь ... я знаю, что читать все это так долго и, вероятно, немного больно: D (усложнено моими грамматическими ошибками, jeje), но в любом случае надеюсь, что это поможет ** нам найти более стабильное решение проблемы, которую мы просто не можем стереть jeje.

Привет.

Виктор!!!

person Victor    schedule 11.01.2013