Подтвердить что ты не робот

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

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

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

Добавление: Спасибо @Ash за упоминание этого SwingWorker bug. Обходным путем является build from source, был отправлен.

Ответ 3

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

Ответ 4

Простите, что опоздал!

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

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

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

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

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

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

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

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

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

Снова.. это не решит проблему.

3) Используя шаблон проектирования прокси-сервера, вы можете извлечь интерфейс Entity (позвольте ему назвать 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 - это просто интерфейс, объявляющий все Билл-методы.

На уровне сохранения я поставлю 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) Ну, на самом деле есть еще одна вещь, которую мы можем сделать, чтобы не работать с интерфейсами... используя somekind вырожденного прокси-объекта...

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

Привет.

Виктор!!!