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吗?
答案 0 :(得分: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
答案 1 :(得分: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代码来找到这个 - 我无法在网上找到任何解决方案的主题。
答案 2 :(得分:1)
如果您使用的是Grails,我发现这个解决方案不需要将您的Domain类移动到JAVA世界并使用@SQLInsert注释:
例如,如果您有一个名为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。
希望这有帮助。