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();
}