JPA - создать, если не существует сущность? - PullRequest
22 голосов
/ 25 августа 2010

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

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

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

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

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

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

Ответы [ 3 ]

19 голосов
/ 25 августа 2010

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

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

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

5 голосов
/ 23 февраля 2016

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

Предварительные условия:

  • У объекта должно быть уникальное нарушение ограничения, котороепотерпит неудачу, если будут созданы два экземпляра
  • У вас есть какой-то видоискатель для поиска сущности (можно найти по первичному ключу с помощью EntityManager.find или по какому-либо запросу), мы будем называть это finder
  • У вас есть какой-то фабричный метод для создания нового объекта, если тот, который вы ищете, не существует, мы будем называть это 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();
}
0 голосов
/ 25 августа 2010
  1. Создайте экземпляр EntityManager (назовем его «em»), если у вас уже нет активного
  2. Создать новую транзакцию (назовем ее "tx")
  3. Вызов em.find (Object pk)
  4. Вызовите tx.begin ()
    • Если find () вернул ненулевую ссылку на сущность, вам нужно обновить. Примените свои изменения к возвращенному объекту и затем вызовите em.merge (объектный объект).
    • если find () вернул нулевую ссылку, то этот PK не существует в базе данных. Создайте новую сущность и затем вызовите em.persist (Object newEntity).
  5. Позвоните em.flush ()
  6. Позвоните tx.commit ()
  7. Возвращает ссылку на сущность согласно сигнатуре вашего метода.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...