Может ли Hibernate работать с синтаксисом «ON DUPLICATE KEY UPDATE» в MySQL? - PullRequest
25 голосов
/ 27 мая 2009

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?

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

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

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

Должен ли я действительно разрушать Hibernate?

Ответы [ 3 ]

24 голосов
/ 27 мая 2009

Вы смотрели на 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 голосов
/ 17 мая 2013

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

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

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

Вы можете использовать mysql "вставка ... при обновлении дублированного ключа" через пользовательский sql-вставку для персистентного объекта гибернации. Вы можете определить пользовательское предложение 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. Когда я реализовал поведение вставки-обновления-обновления с помощью mysql, я получал исключения, когда «путь обновления» sql был представлен. В частности, mysql сообщал, что 2 строки были изменены, когда была обновлена ​​только 1 строка (якобы потому, что существующая строка удаляется, а новая строка вставляется). См. этот выпуск для получения более подробной информации об этой конкретной функции.

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

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

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

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

}

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

1 голос
/ 21 сентября 2014

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

  1. Создание пользовательской конфигурации Hibernate
  2. Переопределить карту PersistentClass
  3. Добавьте свой пользовательский 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, так как он НЕ будет установлен правильно, если вместо INSERT выполняется UPDATE, если вы не установили это явно в операторе, например, ID = LAST_INSERT_ID (ID). Я не проверял, откуда GORM получает идентификатор, но я предполагаю, что где-то он использует LAST_INSERT_ID.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...