Hibernate可以使用MySQL“ON DUPLICATE KEY UPDATE”语法吗?

问题描述 投票:25回答:3

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限制为仅选择。如果你试图调用session.createSQLQuery("sql").executeUpdate(),Hibernate将抛出异常。

第三,Hibernate的saveOrUpdate在这种情况下不符合要求。您的测试将通过,但如果您每秒有多个访问者,则会导致生产失败。

我真的要颠覆Hibernate吗?

java mysql hibernate orm
3个回答
24
投票

你看过Hibernate @SQLInsert Annotation吗?

@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,因为标准会话实现的缓存行为是不必要的开销,我们不希望我们的hibernate监听器触发审计日志写入。该实现是关于在没有交互的情况下实现尽可能高的写性能。

但是,新的日志类型需要使用insert-else-update类型的行为,我们打算使用事务时间更新现有日志条目作为“标记”行为类型。在StatelessSession中,不提供saveOrUpdate(),因此我们需要手动实现insert-else-update。

根据这些要求:

您可以通过自定义sql-insert为hibernate持久对象使用mysql“insert ... on duplicate key update”行为。您可以通过注释(如上面的答案)或通过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实现insert-else-update行为时,当sql的'update path'被执行时,我得到了异常。具体来说,mysql报告只更新了1行时更改了2行(表面上是因为现有行是删除而插入了新行)。有关该特定功能的更多详细信息,请参阅this issue

因此,当更新返回2x受hibernate影响的行数时,hibernate抛出了BatchedTooManyRowsAffectedException,将回滚事务,并传播异常。即使您要捕获异常并处理它,该事务也已经被回滚。

经过一番挖掘后,我发现这是hibernate使用的实体持久性问题。在我的例子中,hibernate使用的是SingleTableEntityPersister,它定义了一个Expectation,即更新的行数应该与批处理操作中定义的行数相匹配。

使此行为起作用所需的最后调整是定义自定义持久性(如上面的xml映射所示)。在这种情况下,我们所要做的就是扩展SingleTableEntityPersister并“覆盖”插入Expectation。例如。我只是将这个静态类添加到持久性对象中,并将其定义为hibernate映射中的自定义持久性:

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

}

花了很长时间挖掘hibernate代码来找到这个 - 我无法在网上找到任何解决这个问题的主题。


1
投票

如果您使用的是Grails,我发现这个解决方案不需要将您的Domain类移动到JAVA世界并使用@SQLInsert注释:

  1. 创建自定义Hibernate配置
  2. 覆盖PersistentClass映射
  3. 使用ON DUPLICATE KEY将自定义INSERT sql添加到所需的持久类。

例如,如果您有一个名为Person的Domain对象,并且您希望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);
            }
        };
    }

并将其添加为DataSource.groovy中的Hibernate配置:

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

请注意使用LAST_INSERT_ID,因为如果执行UPDATE而不是INSERT,则不会正确设置,除非您在语句中明确设置它,例如ID = LAST_INSERT_ID(ID)。我没有检查GORM从哪里获取ID,但我假设它正在使用LAST_INSERT_ID。

希望这可以帮助。

© www.soinside.com 2019 - 2024. All rights reserved.