Hibernate 可以使用 MySQL 的“ON DUPLICATE KEY UPDATE”语法吗?
Posted
技术标签:
【中文标题】Hibernate 可以使用 MySQL 的“ON DUPLICATE KEY UPDATE”语法吗?【英文标题】:Can Hibernate work with MySQL's "ON DUPLICATE KEY UPDATE" syntax? 【发布时间】:2009-05-26 23:49:45 【问题描述】: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 吗?
【问题讨论】:
另请参阅此调查:***.com/questions/56453690/… 【参考方案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
【讨论】:
如果你想使用自动递增的 id 字段(比如在 mysql 中)怎么办?我收到关于数据库未返回 ID 的奇怪错误on duplicate key update some_value = ?
会是什么样子,在哪里? some_value
的实际新值是否应该更新?
这个解决方案是否经过实际测试?我尝试了它并遇到了困难,因为id
字段在引用对象中不同步,导致保存另一个对象时出现外键约束错误。我想这可以在特定情况下工作。
请注意,Hibernate 使用的列顺序与它们在 Entity 类中定义的顺序不同。 Here 是如何解决这个问题的。
如果我不想将 view_count 增加 1,而是增加作为参数传入的另一个数字,该怎么办?【参考方案2】:
这是一个老问题,但我遇到了类似的问题,我想我会添加到这个主题中。我需要将日志添加到现有的 StatelessSession 审计日志编写器。现有实现使用 StatelessSession,因为标准会话实现的缓存行为是不必要的开销并且我们不希望我们的休眠侦听器触发审计日志写入。此实现旨在在不进行交互的情况下实现尽可能高的写入性能。
但是,新的日志类型需要使用 insert-else-update 类型的行为,我们打算使用事务时间更新现有日志条目作为“标记”行为类型。在 StatelessSession 中,不提供 saveOrUpdate(),因此我们需要手动实现 insert-else-update。
根据这些要求:
您可以通过针对休眠持久对象的自定义 sql-insert 来使用 mysql“插入 ... 重复键更新”行为。您可以通过注释(如上面的答案)或通过 sql-insert 实体定义自定义 sql-insert 子句和休眠 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 的“更新路径”时遇到异常。具体来说,mysql 报告在仅更新 1 行时更改了 2 行(表面上是因为删除了现有行并插入了新行)。有关该特定功能的更多详细信息,请参阅this issue。
因此,当更新返回 2 倍受休眠影响的行数时,休眠将抛出 BatchedTooManyRowsAffectedException,将回滚事务并传播异常。即使您要捕获异常并处理它,到那时事务已经回滚了。
经过一番挖掘,我发现这是休眠使用的实体持久性的问题。在我的例子中,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;
花了很长时间挖掘休眠代码来找到这个 - 我无法在网上找到任何主题与解决这个问题。
【讨论】:
你不能通过指定“check”属性来完成同样的事情吗?请参阅docs.jboss.org/hibernate/orm/3.6/reference/en-US/html/… 例如:<sql-insert check="none">...
或 @SQLInsert(sql="...", check="none")
以上更正,注解应如下所示:@SQLInsert(sql="...", check=ResultCheckStyle.NONE)
【参考方案3】:
如果您使用的是 Grails,我发现此解决方案不需要将您的 Domain 类移动到 JAVA 世界并使用 @SQLInsert 注释:
-
创建自定义休眠配置
覆盖 PersistentClass 映射
使用 ON DUPLICATE KEY 将您的自定义 INSERT sql 添加到您想要的持久类中。
例如,如果您有一个名为 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);
;
并将其添加为 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。
希望这会有所帮助。
【讨论】:
以上是关于Hibernate 可以使用 MySQL 的“ON DUPLICATE KEY UPDATE”语法吗?的主要内容,如果未能解决你的问题,请参考以下文章
JPA + Hibernate:如何定义具有 ON DELETE CASCADE 的约束
在 Mysql 上使用 ddl 模式生成时未生成 ON DELETE CASCADE 选项
tomcat 7.0.42 pooling, hibernate 4.2, mysql 坚如磐石的自动重新连接解决方案
maven 项目 hibernate 处理延时加载 on session的问题