Hibernate - 批量更新返回的行数与预期不符:实际行数为0,预期行数为1

220

我收到了以下Hibernate错误。我能够确定导致问题的函数。不幸的是,该函数中有几个数据库调用。由于Hibernate在事务结束时刷新会话,因此我无法找到导致问题的行。下面提到的Hibernate错误看起来像一般性错误。它甚至没有提到哪个Bean导致了问题。有人熟悉这个Hibernate错误吗?

org.hibernate.StaleStateException: Batch update returned unexpected row count from update: 0 actual row count: 0 expected: 1
        at org.hibernate.jdbc.BatchingBatcher.checkRowCount(BatchingBatcher.java:93)
        at org.hibernate.jdbc.BatchingBatcher.checkRowCounts(BatchingBatcher.java:79)
        at org.hibernate.jdbc.BatchingBatcher.doExecuteBatch(BatchingBatcher.java:58)
        at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:195)
        at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:235)
        at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:142)
        at org.hibernate.event.def.AbstractFlushingEventListener.performExecutions(AbstractFlushingEventListener.java:297)
        at org.hibernate.event.def.DefaultFlushEventListener.onFlush(DefaultFlushEventListener.java:27)
        at org.hibernate.impl.SessionImpl.flush(SessionImpl.java:985)
        at org.hibernate.impl.SessionImpl.managedFlush(SessionImpl.java:333)
        at org.hibernate.transaction.JDBCTransaction.commit(JDBCTransaction.java:106)
        at org.springframework.orm.hibernate3.HibernateTransactionManager.doCommit(HibernateTransactionManager.java:584)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransacti
onManager.java:500)
        at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManag
er.java:473)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.doCommitTransactionAfterReturning(Transaction
AspectSupport.java:267)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:170)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:176)

谢谢@Peter Mortensen。我已经更新了我的电子邮件。 - Sujee
我有同样的问题。这并不是一个大问题,因为它很少发生。使用 show_sql 并不实际,因为要复现这种行为需要数百万次的事务。然而,由于这在我运行的系统测试中反复发生(该测试包含无数次的事务),我怀疑有一个特定的原因。 - daniel_or_else
我在尝试更新没有任何更改的行时遇到了这个问题。如果没有差异,请不要更新任何内容。 - fifman
44个回答

100

当我尝试删除一个完全不存在的记录时,我遇到了相同的异常。因此,请确保您要更新/删除的记录实际存在于数据库中。


18
当我将一个子元素从其父元素中移除,保存该父元素(从而删除了该子元素),然后试图手动删除该子元素时,我遇到了这个问题。 - Dave Thieben
这解决了我的问题。记录不存在,我的服务在调用updateAll()方法时实际上需要调用createOrUpdateAll()方法。谢谢。 - Mital Pritmani
4
那么如何解决这个问题?如果我获取了一条记录,然后将其删除;但是如果系统在我删除之前已经把它删除了,我的应用程序将会抛出异常。 - Stony
@davethieben,这正是我所需要的信息。我没有意识到在删除时父子关系会持续存在。 - snowe
解决方案是在获取记录时使用 select for update 来锁定行,这样在你执行删除操作之前,没有其他内容可以删除它。 - Matthew Read

83

如果没有您的事务代码和映射,要调查问题就几乎不可能。

但是,为了更好地掌握问题的原因,请尝试以下操作:

  • 在您的 Hibernate 配置中,将 hibernate.show_sql 设置为 true。这将显示执行并引起问题的 SQL 语句。
  • 将 Spring 和 Hibernate 的日志级别设置为 DEBUG,这将让您更清楚哪一行代码引起了问题。
  • 创建一个单元测试,在 Spring 中不配置事务管理器的情况下复制该问题。这应该可以让您更好地了解有问题的代码行。

27
我建议将日志类别org.hibernate.SQL的级别设置为DEBUG,而不是使用hibernate.show_sql。这样做可以避免仅为记录日志而修改Hibernate配置的情况。请注意,此举并不会改变原始意思。 - Thierry
“show_sql” 的使用可能会非常冗长,而且在生产环境中并不实用。因此,我已经修改了 BatchingBatcher 类第 74 行的 catch 子句,通过 ps.toString() 打印语句,只显示出存在问题的语句。 - Orden

65

解决方案: 在Hibernate映射文件中,如果您使用任何生成器类来定义id属性,则不应该使用setter方法显式设置该属性的值。

如果您显式地设置了Id属性的值,将会导致上述错误。请检查以避免此错误。 或者 当您在映射文件中提到字段generator="native"或"incremental"并且在您的数据库中,映射的表没有自动递增时,它就会显示错误。 解决方案:转到您的数据库并更新表以设置自动递增。


4
完全正确。这一定是正确的答案。谢谢 @Rēda Biramanē - Kuldeep Verma
1
然而,您可以将ID值设置为“0”,然后Hibernate将随意覆盖它。 - forresthopkinsa
3
谢谢!不确定为什么这个答案不是排名最高的。 - Stuart McIntyre

30

在我的案例中,我遇到了两个相似的情况而出现了这个异常:

  • 在一个被注释为@Transactional的方法中,我调用了另一个服务(需要长时间响应)。这个方法更新了实体的一些属性(在方法之后,实体仍然存在于数据库中)。如果用户请求该方法两次(因为他认为第一次没有生效),当退出事务性方法时,第二次Hibernate尝试更新一个已经在事务开始时改变了状态的实体。由于Hibernate正在查找处于一种状态的实体,并发现了同样的实体,但已经被第一个请求更改,它会抛出异常,因为它无法更新实体。这就像在GIT中的冲突一样。
  • 我有自动请求(用于监控平台)来更新一个实体(并在几秒钟后手动回滚)。但是这个平台已经被测试团队使用了。当测试人员在与自动请求相同的实体上执行测试(在同一毫秒内),我就会得到这个异常。就像在前面的情况下,当退出第二个事务时,先前获取的实体已经发生了变化。

结论:在我的情况下,这不是可以在代码中找到的问题。 当Hibernate发现从数据库检索的实体在当前事务期间被更改时,就会抛出此异常,因此它无法将其刷新到数据库中,因为Hibernate不知道实体的正确版本:是当前事务开始时提取的版本,还是已经存储在数据库中的版本。

解决方案:为了解决这个问题,您需要使用HibernateLockMode来找到最适合您要求的那一个。


你好,感谢你的有用回答。请问你最终选择了哪种锁定模式来解决错误?我遇到了类似的问题,当API在毫秒级别内被连续点击时,由于用户不小心点击两次而导致出现错误。悲观锁定会影响性能,所以很想知道你最终选择了什么? - Madhur Bhaiya
1
我已经很久没有遇到这个问题了,也不太记得我当时采用的确切解决方案,但是可能是LockMode.READ或者LockMode.OPTIMISTIC - Sergio Lema

20

有一次,当我在为某些对象分配特定的ID(测试)并尝试将它们保存到数据库中时,发生了这种情况。问题在于,在数据库中设置了一个特定的策略来设置对象的ID。如果您在Hibernate级别拥有策略,请不要分配ID。


17

我刚遇到了这个问题,并发现我在一个Hibernate事务中删除了一条记录,并尝试在之后更新它。


17

Hibernate 5.4.1和HHH-12878问题

在Hibernate 5.4.1之前,乐观锁定失败异常(例如StaleStateExceptionOptimisticLockException)并不包括失败的语句。

HHH-12878问题被创建以改进Hibernate,以便在抛出乐观锁定异常时,JDBC PreparedStatement实现也被记录:

if ( expectedRowCount > rowCount ) {
    throw new StaleStateException(
            "Batch update returned unexpected row count from update ["
                    + batchPosition + "]; actual row count: " + rowCount
                    + "; expected: " + expectedRowCount + "; statement executed: "
                    + statement
    );
}

测试时间

我在我的 High-Performance Java Persistence GitHub 存储库中创建了 BatchingOptimisticLockingTest,以演示新行为是如何工作的。

首先,我们将定义一个 Post 实体,该实体定义了一个 @Version 属性,因此启用了 隐式乐观锁定机制

@Entity(name = "Post")
@Table(name = "post")
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;

    private String title;

    @Version
    private short version;

    public Long getId() {
        return id;
    }

    public Post setId(Long id) {
        this.id = id;
        return this;
    }

    public String getTitle() {
        return title;
    }

    public Post setTitle(String title) {
        this.title = title;
        return this;
    }

    public short getVersion() {
        return version;
    }
}

我们将使用以下3个配置属性启用JDBC批处理:

properties.put("hibernate.jdbc.batch_size", "5");
properties.put("hibernate.order_inserts", "true");
properties.put("hibernate.order_updates", "true");

我们将创建3个帖子实体:

doInJPA(entityManager -> {
    for (int i = 1; i <= 3; i++) {
        entityManager.persist(
            new Post()
                .setTitle(String.format("Post no. %d", i))
        );
    }
});

然后Hibernate将执行JDBC批量插入:

SELECT nextval ('hibernate_sequence')
SELECT nextval ('hibernate_sequence')
SELECT nextval ('hibernate_sequence')

Query: [
    INSERT INTO post (title, version, id) 
    VALUES (?, ?, ?)
], 
Params:[
    (Post no. 1, 0, 1), 
    (Post no. 2, 0, 2), 
    (Post no. 3, 0, 3)
]

所以,我们知道JDBC批处理完全正常。

现在,让我们复制乐观锁定问题:

doInJPA(entityManager -> {
    List<Post> posts = entityManager.createQuery("""
        select p 
        from Post p
        """, Post.class)
    .getResultList();

    posts.forEach(
        post -> post.setTitle(
            post.getTitle() + " - 2nd edition"
        )
    );

    executeSync(
        () -> doInJPA(_entityManager -> {
            Post post = _entityManager.createQuery("""
                select p 
                from Post p
                order by p.id
                """, Post.class)
            .setMaxResults(1)
            .getSingleResult();

            post.setTitle(post.getTitle() + " - corrected");
        })
    );
});

第一次事务选择所有的Post实体并修改title属性。
然而,在第一个EntityManager被刷新之前,我们将使用executeSync方法执行第二个事务。
第二个事务修改了第一个Post,因此它的version将会增加:
Query:[
    UPDATE 
        post 
    SET 
        title = ?, 
        version = ? 
    WHERE 
        id = ? AND 
        version = ?
], 
Params:[
    ('Post no. 1 - corrected', 1, 1, 0)
]

现在,当第一次事务尝试刷新EntityManager时,我们将会收到OptimisticLockException异常:

Query:[
    UPDATE 
        post 
    SET 
        title = ?, 
        version = ? 
    WHERE 
        id = ? AND 
        version = ?
], 
Params:[
    ('Post no. 1 - 2nd edition', 1, 1, 0), 
    ('Post no. 2 - 2nd edition', 1, 2, 0), 
    ('Post no. 3 - 2nd edition', 1, 3, 0)
]

o.h.e.j.b.i.AbstractBatchImpl - HHH000010: On release of batch it still contained JDBC statements

o.h.e.j.b.i.BatchingBatch - HHH000315: Exception executing batch [
    org.hibernate.StaleStateException: 
    Batch update returned unexpected row count from update [0]; 
    actual row count: 0; 
    expected: 1; 
    statement executed: 
        PgPreparedStatement [
            update post set title='Post no. 3 - 2nd edition', version=1 where id=3 and version=0
        ]
], 
SQL: update post set title=?, version=? where id=? and version=?

因此,您需要升级到Hibernate 5.4.1或更高版本才能从此改进中受益。


很遗憾,可能是因为这里的一个 bug: https://hibernate.atlassian.net/browse/HHH-13741 我只能看到 Hikari 准备语句的一些哈希值。使用 Spring Boot 和 Hikari 连接池。 - booFar
对我的前一个评论:我的问题在于Hikari准备的语句包装了一个SQLServerPreparedStatement,其中的内容不会显示在日志中。这可能是解决方案:https://hibernate.atlassian.net/browse/HHH-13888 - booFar

13

当触发器执行影响行数的其他DML (数据修改)查询时,可能会发生这种情况。我的解决方案是在触发器顶部添加以下内容:

SET NOCOUNT ON;

2
这个答案让我朝着正确的方向前进。我正在处理一个遗留数据库,它上面有一个触发器 - 幸运的是,我正在用代码替换触发器的功能,所以我可以直接删除它。 - S. Baggy
3
+1 这也是我的问题。我正在使用postgresql,因此需要使用 @SQLInsert 注解来关闭行数检查:http://www.technology-ebay.de/the-teams/mobile-de/blog/postgresql-table-partitioning-hibernate.html - s1mm0t
SET NOCOUNT OFF* - G. Ciardini
你把这个注解放在哪里了?在代码库里吗? - Romain Dereux
@RomainDereux 将其添加为触发问题的第一行。 - Mr. TA

10

我遇到了同样的问题。 这段代码在测试环境中是可行的,但在演示环境中却不起作用。

org.hibernate.jdbc.BatchedTooManyRowsAffectedException: Batch update returned unexpected row count from update [0]; actual row count: 3; expected: 1

问题在于测试数据库表中每个主键只有一个条目。但是在暂存数据库中,同一主键有多个条目。(问题在于暂存数据库中的表没有任何主键约束,并且存在多个条目。)
因此,每次更新操作都会失败。它尝试更新单个记录并期望更新计数为1。但由于同一主键在表中有3个记录,结果更新计数为3。由于期望的更新计数和实际结果更新计数不匹配,它会抛出异常并回滚。
之后,我删除了所有具有重复主键的记录并添加了主键约束。现在它可以正常工作了。
Hibernate - Batch update returned unexpected row count from update: 0 actual row count: 0 expected: 1

实际行数:0 // 表示没有记录可更新
更新:0 // 表示没有找到需要更新的记录
期望:1 // 表示在数据库表中至少期望找到一个带有此关键字的记录。

这里的问题是查询尝试使用某个关键字更新记录,但是Hibernate没有找到与该关键字匹配的任何记录。


嗨,@ParagFlume,当我尝试从我的数据库中选择一个对象时,我遇到了同样的错误“批量更新返回了意外的行数:实际行数为0,预期为1”。这个问题只存在于生产环境中,你有任何解决这个问题的想法吗?我现在处于一个紧急情况。谢谢! - James
我认为查询在数据库中没有找到任何记录,它期望在数据库中找到一条记录,然后尝试更新它。您可以手动检查/使用查询浏览器来确定记录是否实际存在于数据库中。 - ParagFlume
是的,记录存在,但我的问题是为什么Hibernate尝试更新,我只是使用选择服务从数据库获取对象! - James
也许您正在尝试更改对象的状态,而会话仍然处于打开状态。 - ParagFlume
我该如何检查这个行为,因为我不是开发该服务的人... - James
仅检查对象的任何状态是否已更改,特别是对象的id(dto的id/主键)。 - ParagFlume

10

我的一些想法。

问题: Spring Boot 2.7.1中,h2数据库版本已更改为v2.1.214,这可能会导致在使用生成的UUID作为Id列时抛出OptimisticLockException异常,详情请参见https://hibernate.atlassian.net/browse/HHH-15373

解决方案:@Column注释中添加columnDefinition="UUID"

例如,对于像这样的实体的主键定义:

@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
@Column(name = COLUMN_UUID, updatable = false, nullable = false)
UUID uUID;

将列的注释更改为:

@Column(name = COLUMN_UUID, updatable = false, nullable = false, columnDefinition="UUID")

你说它“可能”会导致这样的异常。不知道你是否知道确切是什么原因或在哪些条件下可能会发生? - Manu

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接