JPA - объект create-if-not-exist?

У меня есть несколько отображаемых объектов в приложении JPA/Hibernate. В сети я получаю пакеты, которые представляют обновления для этих объектов, или могут фактически представлять новые объекты целиком.

Я бы хотел написать такой метод, как

<T> T getOrCreate(Class<T> klass, Object primaryKey)

который возвращает объект предоставленного класса, если он существует в базе данных с pk primaryKey и в противном случае создает новый объект этого класса, сохраняет его и возвращает.

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

Есть ли идиоматический способ сделать это в JPA, или есть лучший способ решить мою проблему?

Ответы

Ответ 1

  • Создайте экземпляр EntityManager (позвоните ему "em" ), если у вас уже есть активный
  • Создайте новую транзакцию (назовите ее "tx" )
  • Вызов em.find(Object pk)
  • Вызов tx.begin()
    • Если find() возвратил ненулевую ссылку на объект, вам необходимо сделать обновление. Примените свои изменения к возвращенному объекту, а затем вызовите em.merge(объект объекта).
    • если find() возвратил нулевую ссылку, тогда PK не существует в базе данных. Создайте новый объект, а затем вызовите em.persist(Object newEntity).
  • Вызов em.flush()
  • Вызов tx.commit()
  • Верните ссылку на сущность в соответствии с вашей сигнатурой метода.

Ответ 2

Я хотел бы написать метод типа <T> T getOrCreate(Class<T> klass, Object primaryKey)

Это будет непросто.

Наивным подходом было бы сделать что-то вроде этого (предполагая, что метод запущен внутри транзакции):

public <T> T findOrCreate(Class<T> entityClass, Object primaryKey) {
    T entity = em.find(entityClass, primaryKey);
    if ( entity != null ) {
        return entity;
    } else {
        try {
            entity = entityClass.newInstance();
            /* use more reflection to set the pk (probably need a base entity) */
            return entity;
        } catch ( Exception e ) {
            throw new RuntimeException(e);
        }
    }
}

Но в параллельной среде этот код может выйти из строя из-за некоторого состояния гонки:

T1: BEGIN TX;
T2: BEGIN TX;

T1: SELECT w/ id = 123; //returns null
T2: SELECT w/ id = 123; //returns null

T1: INSERT w/ id = 123;
T1: COMMIT; //row inserted

T2: INSERT w/ name = 123;
T2: COMMIT; //constraint violation

И если вы используете несколько JVM, синхронизация не поможет. И, не приобретая блокировку таблицы (что довольно ужасно), я действительно не вижу, как вы могли бы решить эту проблему.

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

Вероятно, вам следует добавить некоторые сведения об упомянутых ограничениях (многопоточная распределенная среда?).

Ответ 3

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

Предпосылки:

  • Сущность должна иметь уникальное нарушение ограничений, которое потерпит неудачу, если будут созданы два экземпляра
  • У вас есть какой-то искатель для поиска сущности (можно найти по первичному ключу с EntityManager.find или по какому-то запросу), мы будем называть это как finder
  • У вас есть какой-то метод factory для создания нового объекта, если тот, который вы ищете, не существует, мы будем называть это как factory.
  • Я предполагаю, что данный метод findOrCreate будет существовать на каком-то объекте репозитория и вызывается в контексте существующего менеджера сущностей и существующей транзакции.
  • Если уровень изоляции транзакции сериализуется или моментальный снимок, это не сработает. Если транзакция повторяется, то вы не должны пытаться читать объект в текущей транзакции.
  • Я бы рекомендовал разбить логику ниже на несколько методов поддержки.

код:

public <T> T findOrCreate(Supplier<T> finder, Supplier<T> factory) {
    EntityManager innerEntityManager = entityManagerFactory.createEntityManager();
    innerEntityManager.getTransaction().begin();
    try {
        //Try the naive find-or-create in our inner entity manager
        if(finder.get() == null) {
            T newInstance = factory.get();
            innerEntityManager.persist(newInstance);
        }
        innerEntityManager.getTransaction().commit();
    } catch (PersistenceException ex) {
        //This may be a unique constraint violation or it could be some
        //other issue.  We will attempt to determine which it is by trying
        //to find the entity.  Either way, our attempt failed and we
        //roll back the tx.
        innerEntityManager.getTransaction().rollback();
        T entity = finder.get();
        if(entity == null) {
            //Must have been some other issue
            throw ex;
        } else {
            //Either it was a unique constraint violation or we don't
            //care because someone else has succeeded
            return entity;
        }
    } catch (Throwable t) {
        innerEntityManager.getTransaction().rollback();
        throw t;
    } finally {
        innerEntityManager.close();
    }
    //If we didn't hit an exception then we successfully created it
    //in the inner transaction.  We now need to find the entity in
    //our outer transaction.
    return finder.get();
}