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

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

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

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

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

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

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

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

Стратегии загрузки Lazy / Eager в удаленных случаях (JPA)

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

Ответы [ 4 ]

6 голосов
/ 22 июля 2010

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

Добавление: Спасибо @Ash за упоминание этой SwingWorker ошибки Временное решение: сборка из источника отправка .

3 голосов
/ 22 июля 2010

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

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

1 голос
/ 12 января 2013

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

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

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

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

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

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

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

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

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

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

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

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

  • Это решает проблему за счет принудительного использования интерфейсов вместо простых классов домена. Неплохо подумать на самом деле ... но я также думаю, что ни то, ни другое. Я думаю, что мы все хотим использовать доменные классы. Также для каждого объекта домена мы должны написать интерфейс ... что произойдет, если объект вошел в .JAR ... ага! Touche! Мы не можем извлечь интерфейс во время выполнения: 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;
    }
}

Предмет - это еще один объект-сущность, моделирующий предмет Билла (Билл может содержать много Предметов, Предмет принадлежит только одному и только одному Биллу).

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 ... потому что может быть несколько типов репозиториев (memory, file, net, ??) и других уровней, поэтому нет необходимости знать, из каких Я работаю с хранилищем.

Существует также внутренний метод 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.

Привет.

Victor !!!

1 голос
/ 20 декабря 2010

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

...