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

Найти или вставить на основе уникального ключа с Hibernate

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

ОБНОВЛЕНИЕ: Позвольте мне пояснить, что приложение, для которого я пишу это, - это в основном пакетный процессор входных файлов. Системе необходимо прочитать файл по строкам и вставить записи в db. Формат файла в основном является денормализованным представлением нескольких таблиц в нашей схеме, поэтому мне нужно проанализировать родительскую запись или вставить ее в db, чтобы я мог получить новый синтетический ключ, или если он уже существует, выберите его. Затем я могу добавить дополнительные связанные записи в другие таблицы, которые имеют внешние ключи обратно к этой записи.

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

Отображаемый класс для родительских записей выглядит следующим образом:

@Entity
public class Foo {
    @Id
    @GeneratedValue(strategy = IDENTITY)
    private int id;
    @Column(unique = true)
    private String name;
    ...
}

Моя первоначальная попытка написать этот метод выглядит следующим образом:

public Foo findOrCreate(String name) {
    Foo foo = new Foo();
    foo.setName(name);
    try {
        session.save(foo)
    } catch(ConstraintViolationException e) {
        foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
    }
    return foo;
}

Проблема заключается в том, когда имя, которое я ищу, существует, исключение org.hibernate.AssertionFailure вызывается вызовом uniqueResult(). Полная трассировка стека ниже:

org.hibernate.AssertionFailure: null id in com.searchdex.linktracer.domain.LinkingPage entry (don't flush the Session after an exception occurs)
    at org.hibernate.event.def.DefaultFlushEntityEventListener.checkId(DefaultFlushEntityEventListener.java:82) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.DefaultFlushEntityEventListener.getValues(DefaultFlushEntityEventListener.java:190) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.DefaultFlushEntityEventListener.onFlushEntity(DefaultFlushEntityEventListener.java:147) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.AbstractFlushingEventListener.flushEntities(AbstractFlushingEventListener.java:219) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:99) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.event.def.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:58) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.impl.SessionImpl.autoFlushIfRequired(SessionImpl.java:1185) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.impl.SessionImpl.list(SessionImpl.java:1709) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.impl.CriteriaImpl.list(CriteriaImpl.java:347) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]
    at org.hibernate.impl.CriteriaImpl.uniqueResult(CriteriaImpl.java:369) [hibernate-core-3.6.0.Final.jar:3.6.0.Final]

Кто-нибудь знает, что вызывает это исключение? Поддерживает ли hibernate лучший способ выполнить это?

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

4b9b3361

Ответ 1

У меня было подобное требование пакетной обработки, с процессами, запущенными на нескольких JVM. Подход, который я принял для этого, был следующим. Это очень похоже на предложение jtahlborn. Однако, как указывало vbence, если вы используете транзакцию NESTED, когда вы получаете исключение нарушения ограничения, ваш сеанс считается недействительным. Вместо этого я использую REQUIRES_NEW, который приостанавливает текущую транзакцию и создает новую независимую транзакцию. Если новая транзакция откатится, она не повлияет на исходную транзакцию.

Я использую Spring TransactionTemplate, но я уверен, что вы можете легко перевести его, если вы не хотите зависимости от Spring.

public T findOrCreate(final T t) throws InvalidRecordException {
   // 1) look for the record
   T found = findUnique(t);
   if (found != null)
     return found;
   // 2) if not found, start a new, independent transaction
   TransactionTemplate tt = new TransactionTemplate((PlatformTransactionManager)
                                            transactionManager);
   tt.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
   try {
     found = (T)tt.execute(new TransactionCallback<T>() {
        try {
            // 3) store the record in this new transaction
            return store(t);
        } catch (ConstraintViolationException e) {
            // another thread or process created this already, possibly
            // between 1) and 2)
            status.setRollbackOnly();
            return null;
        }
     });
     // 4) if we failed to create the record in the second transaction, found will
     // still be null; however, this would happy only if another process
     // created the record. let see what they made for us!
     if (found == null)
        found = findUnique(t);
   } catch (...) {
     // handle exceptions
   }
   return found;
}

Ответ 2

Два решения приходят на ум:

То, что ТАБЛИЧНЫЕ ЗАМКИ для

Hibernate не поддерживает блокировки таблиц, но это та ситуация, когда они удобны. К счастью, вы можете использовать собственный SQL thru Session.createSQLQuery(). Например (в MySQL):

// no access to the table for any other clients
session.createSQLQuery("LOCK TABLES foo WRITE").executeUpdate();

// safe zone
Foo foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
if (foo == null) {
    foo = new Foo();
    foo.setName(name)
    session.save(foo);
}

// releasing locks
session.createSQLQuery("UNLOCK TABLES").executeUpdate();

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

Как насчет блокировок Hibernate?

Hibernate использует блокировку уровня строки. Мы не можем использовать его напрямую, потому что мы не можем заблокировать несуществующие строки. Но мы можем создать фиктивную таблицу с одной записью, сопоставить ее с ORM, а затем использовать блокировки стилей SELECT ... FOR UPDATE на этом объекте для синхронизации наших клиентов. В основном нам нужно только быть уверенным, что никакие другие клиенты (работающие с одним и тем же программным обеспечением с теми же соглашениями) не будут выполнять какие-либо конфликтующие операции, пока мы работаем.

// begin transaction
Transaction transaction = session.beginTransaction();

// blocks until any other client holds the lock
session.load("dummy", 1, LockOptions.UPGRADE);

// virtual safe zone
Foo foo = session.createCriteria(Foo.class).add(eq("name", name)).uniqueResult();
if (foo == null) {
    foo = new Foo();
    foo.setName(name)
    session.save(foo);
}

// ends transaction (releasing locks)
transaction.commit();

Ваша база данных должна знать синтаксис SELECT ... FOR UPDATE (Hibernate is goig для его использования), и, конечно же, это работает только в том случае, если все ваши клиенты имеют одно и то же соглашение (им нужно заблокировать один и тот же фиктивный объект).

Ответ 3

Это отличный вопрос, поэтому я решил написать статью, чтобы объяснить это более подробно.

Как я объяснил в этой бесплатной главе моей книги, "Высокопроизводительная стойкость Java", вам нужно использовать UPSERT или MERGE для достижения этой цели.

Однако Hibernate не поддерживает эту конструкцию, поэтому вам нужно использовать jOOQ.

private PostDetailsRecord upsertPostDetails(
        DSLContext sql, Long id, String owner, Timestamp timestamp) {
    sql
    .insertInto(POST_DETAILS)
    .columns(POST_DETAILS.ID, POST_DETAILS.CREATED_BY, POST_DETAILS.CREATED_ON)
    .values(id, owner, timestamp)
    .onDuplicateKeyIgnore()
    .execute();

    return sql.selectFrom(POST_DETAILS)
    .where(field(POST_DETAILS.ID).eq(id))
    .fetchOne();
}

Вызов этого метода в PostgreSQL:

PostDetailsRecord postDetailsRecord = upsertPostDetails(
    sql, 
    1L, 
    "Alice",
    Timestamp.from(LocalDateTime.now().toInstant(ZoneOffset.UTC))
);

Выводит следующие операторы SQL:

INSERT INTO "post_details" ("id", "created_by", "created_on") 
VALUES (1, 'Alice',  CAST('2016-08-11 12:56:01.831' AS timestamp))
ON CONFLICT  DO NOTHING;

SELECT "public"."post_details"."id",
       "public"."post_details"."created_by",
       "public"."post_details"."created_on",
       "public"."post_details"."updated_by",
       "public"."post_details"."updated_on"
FROM "public"."post_details"
WHERE "public"."post_details"."id" = 1

В Oracle и SQL Server jOOQ будет использовать MERGE, а в MySQL - ON DUPLICATE KEY.

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

enter image description here

Код доступен на GitHub.

Ответ 4

Hibernate документация по транзакциям и исключениям утверждает, что все HibernateExceptions являются неустранимыми и что текущую транзакцию нужно отменить, как только встретится, Это объясняет, почему приведенный выше код не работает. В конечном итоге вы никогда не должны улавливать исключение HibernateException, не выходя из транзакции и закрывая сеанс.

Единственный реальный способ сделать это, похоже, состоял бы в том, чтобы управлять закрытием старого сеанса и повторным открытием нового в самом методе. Реализация метода findOrCreate, который может участвовать в существующей транзакции и безопасен в распределенной среде, кажется невозможным использовать Hibernate на основе того, что я нашел.

Ответ 5

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

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

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

Ответ 6

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

Здесь вы можете найти объяснение стратегий оптимистической и пессимистической блокировки с спящим режимом: http://docs.jboss.org/hibernate/core/3.3/reference/en/html/transactions.html

Ответ 7

Ну, вот один из способов сделать это - но это не подходит для всех ситуаций.

  • В Foo удалите атрибут "unique = true" на name. Добавьте временную метку, которая обновляется на каждой вставке.
  • В findOrCreate() не мешайте проверять, существует ли сущность с указанным именем - просто вставляйте новый каждый раз.
  • При поиске экземпляров Foo с помощью name может быть 0 или более с заданным именем, поэтому вы просто выберите самый новый.

Хорошая вещь об этом методе заключается в том, что он не требует блокировки, поэтому все должно работать довольно быстро. Недостатком является то, что ваша база данных будет усеяна устаревшими записями, поэтому вам, возможно, придется что-то делать в другом месте, чтобы справиться с ними. Кроме того, если другие таблицы ссылаются на Foo на id, то это заведет эти отношения.

Ответ 8

Возможно, вам стоит изменить свою стратегию: Сначала найдите пользователя с именем и только если пользователь не существует, создайте его.

Ответ 9

Я бы попробовал следующую стратегию:

. Начать основную транзакцию (в момент 1)
<Ь > В. Запустите суб-транзакцию (во время 2)

Теперь любой объект, созданный после времени 1, не будет отображаться в основной транзакции. Поэтому, когда вы делаете

С. Создать новый объект состояния гонки, совершить суб-транзакцию
D. Обработайте конфликт, запустив новую суб-транзакцию (в момент 3) и получив объект из запроса (суб-транзакция из точки B теперь выходит за пределы области видимости).

возвращает только первичный ключ объекта, а затем используйте EntityManager.getReference(..) для получения объекта, который вы будете использовать в основной транзакции. В качестве альтернативы, начните основную транзакцию после D; мне не совсем ясно, сколько условий гонки у вас будет в рамках вашей основной транзакции, но приведенное выше должно позволять n раз B-C-D в "большой" транзакции.

Обратите внимание, что вам может потребоваться многопоточность (по одному потоку на один процессор), и тогда вы, вероятно, можете значительно уменьшить эту проблему, используя общий статический кеш для этих конфликтов - и точка 2 может сохраняться "оптимистично", т.е. не делать сначала .find(..).

Изменить. Для новой транзакции вам нужен метод метода интерфейса EJB, аннотированный с типом транзакции REQUIRES_NEW.

Изменить: дважды проверьте, что getReference (..) работает так, как я думаю.