Hibernate可以使用MySQL的“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 

这比开始交易更有效,更有效,并处理新故事登上头版时发生的不可避免的exception。

我们如何使用Hibernate做同样的事情或实现相同的目标?

首先,Hibernate的HQL解析器将抛出exception,因为它不了解特定于数据库的关键字。 实际上,HQL不喜欢任何显式插入,除非它是“ INSERT ... SELECT .... ”。

其次,Hibernate将SQL限制为仅选择。 如果您尝试调用session.createSQLQuery("sql").executeUpdate() Hibernate将抛出exception。

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

我真的要颠覆Hibernate吗?

你看过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 

这是一个老问题,但我遇到了类似的问题,并认为我会添加到这个主题。 我需要将日志添加到现有的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映射,例如:

           

原始海报专门询问MySQL。 当我使用mysql实现insert-else-update行为时,当sql的’update path’被执行时,我得到了exception。 具体来说,mysql报告只更新了1行时更改了2行(表面上是因为现有行是删除而插入了新行)。 有关该特定function的更多详细信息,请参阅此问题 。

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

经过一番挖掘后,我发现这是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代码来找到这个 – 我无法通过解决方案在网上找到任何主题。

如果您使用的是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() { @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。

希望这可以帮助。