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

Может ли Hibernate работать с синтаксисом MySQL ON ON DUPLICATE KEY UPDATE?

MySQL поддерживает синтаксис "INSERT ... ON DUPLICATE KEY UPDATE ...", который позволяет "вслепую" вставлять в базу данных и возвращаться к обновлению существующей записи, если таковая существует.

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

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

 INSERT INTO story_count (id, view_count) VALUES (12345, 1)
 ON DUPLICATE KEY UPDATE set view_count = view_count + 1

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

Как мы можем сделать то же самое или выполнить ту же цель с Hibernate?

Во-первых, парсер Hibernate HQL генерирует исключение, потому что он не понимает ключевые слова для конкретной базы данных. На самом деле, HQL не любит каких-либо явных вложений, если это не "INSERT ... SELECT ....".

Во-вторых, Hibernate ограничивает SQL только выбором. Hibernate выдаст исключение, если вы попытаетесь вызвать session.createSQLQuery("sql").executeUpdate().

В-третьих, Hibernate saveOrUpdate не соответствует счету в этом случае. Ваши тесты пройдут, но тогда вы получите сбои в работе, если у вас более одного посетителя в секунду.

Неужели мне нужно подорвать Hibernate?

4b9b3361

Ответ 1

Вы посмотрели на Hibernate @SQLInsert Аннотации?

@Entity
@Table(name="story_count")
@SQLInsert(sql="INSERT INTO story_count(id, view_count) VALUES (?, ?)
 ON DUPLICATE KEY UPDATE view_count = view_count + 1" )
public class StoryCount

Ответ 2

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

Однако для нового типа журнала необходимо использовать тип поведения insert-else-update, в котором мы намерены обновлять существующие записи журнала с использованием времени транзакции как типа поведения "помечать". В StatelessSession saveOrUpdate() не предлагается, поэтому нам нужно было выполнить обновление insert-else вручную.

В свете этих требований:

Вы можете использовать поведение mysql "insert... on duplicate key update" через пользовательскую sql-insert для постоянного объекта hibernate. Вы можете определить настраиваемое предложение sql-insert либо посредством аннотации (как в приведенном выше ответе), либо через объект sql-insert для сопоставления hibernate xml, например:

<class name="SearchAuditLog" table="search_audit_log" persister="com.marin.msdb.vo.SearchAuditLog$UpsertEntityPersister">

    <composite-id name="LogKey" class="SearchAuditLog$LogKey">
        <key-property
            name="clientId"
            column="client_id"
            type="long"
        />
        <key-property
            name="objectType"
            column="object_type"
            type="int"
        />
        <key-property
            name="objectId"
            column="object_id"
        />
    </composite-id>
    <property
        name="transactionTime"
        column="transaction_time"
        type="timestamp"
        not-null="true"
    />
    <!--  the ordering of the properties is intentional and explicit in the upsert sql below  -->
    <sql-insert><![CDATA[
        insert into search_audit_log (transaction_time, client_id, object_type, object_id)
        values (?,?,?,?) ON DUPLICATE KEY UPDATE transaction_time=now()
        ]]>
    </sql-insert>

Оригинальный плакат спрашивает о MySQL специально. Когда я реализовал поведение insert-else-update с помощью mysql, я получал исключения, когда "путь обновления" sql был выведен. В частности, mysql сообщал, что 2 строки были изменены, когда обновлялась только одна строка (якобы из-за того, что существующая строка является удаленной, а новая строка вставлена). См. эту проблему для более подробной информации об этой конкретной функции.

Поэтому, когда обновление возвратило 2x количество строк, затронутых в спящий режим, hibernate выбрасывал исключение BatchedTooManyRowsAffectedException, откатил бы транзакцию и распространил исключение. Даже если вы должны поймать исключение и обработать его, транзакция уже была отклонена этим пунктом.

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

Последняя настройка, необходимая для работы этого поведения, заключалась в определении пользовательского persister (как показано в приведенном выше сопоставлении xml). В этом случае все, что нам нужно было сделать, это расширить SingleTableEntityPersister и "переопределить" вставку Expectation. Например. Я просто применил этот статический класс к объекту persistence и определил его как пользовательский persister в сопоставлении спящего режима:

public static class UpsertEntityPersister extends SingleTableEntityPersister {

    public UpsertEntityPersister(PersistentClass arg0, EntityRegionAccessStrategy arg1, SessionFactoryImplementor arg2, Mapping arg3) throws HibernateException {
        super(arg0, arg1, arg2, arg3);
        this.insertResultCheckStyles[0] = ExecuteUpdateResultCheckStyle.NONE;
    }

}

Чтобы найти это, потребовалось довольно много времени, чтобы найти код спящего режима - я не смог найти какие-либо темы в сети с решением этого.

Ответ 3

Если вы используете Grails, я нашел это решение, которое не требовало переноса вашего класса Domain в мир JAVA и использования аннотаций @SQLInsert:

  • Создание настраиваемой конфигурации Hibernate
  • Переопределить карту PersistentClass
  • Добавьте свой собственный INSERT sql в постоянные классы, которые вы хотите использовать с помощью ключа ON DUPLICATE.

Например, если у вас есть объект Domain, называемый Person, и вы хотите, чтобы INSERTS были INSERT ON DUPLICATE KEY UPDATE, вы создали бы такую ​​конфигурацию:

public class MyCustomConfiguration extends GrailsAnnotationConfiguration {

    public MyCustomConfiguration() {
        super();

        classes = new HashMap<String, PersistentClass>() {
            @Override
            public PersistentClass put(String key, PersistentClass value) {
                if (Person.class.getName().equalsIgnoreCase(key)) {
                    value.setCustomSQLInsert("insert into person (version, created_by_id, date_created, last_updated, name) values (?, ?, ?, ?, ?) on duplicate key update id=LAST_INSERT_ID(id)", true, ExecuteUpdateResultCheckStyle.COUNT);
                }
                return super.put(key, value);
            }
        };
    }

и добавьте это как конфигурацию Hibernate в DataSource.groovy:

dataSource {
    pooled = true
    driverClassName = "com.mysql.jdbc.Driver"
    configClass = 'MyCustomConfiguration'
}

Просто обратите внимание на использование LAST_INSERT_ID, так как это НЕ будет установлено правильно, если UPDATE выполняется вместо INSERT, если вы не установите его явно в инструкции, например. ID = LAST_INSERT_ID (ID). Я не проверял, где GORM получает идентификатор, но я предполагаю, что он использует LAST_INSERT_ID.

Надеюсь, что это поможет.