【问题标题】:'Batch update returned unexpected row count from update' - after update to Micronaut 3.1+“批量更新从更新中返回了意外的行数” - 更新到 Micronaut 3.1+ 之后
【发布时间】:2022-01-06 05:26:13
【问题描述】:

我正在尝试升级到 Micronaut 3.2,但从 3.1 开始,数据库上的一些写入操作开始失败。我创建了一个示例项目来展示这一点:https://github.com/dpozinen/optimistic-lock

此外,我在https://github.com/micronaut-projects/micronaut-data/issues/1230创建了一个问题

简单地说,我的实体:

@MappedSuperclass
public abstract class BaseEntity {

  @Id
  @GeneratedValue(generator = "system-uuid")
  @GenericGenerator(name = "system-uuid", strategy = "uuid2")
  @Column(updatable = false, nullable = false, length = 36)
  @Type(type = "optimistic.lock.extra.UuidUserType")
  private UUID id;

  @Version
  @Column(nullable = false)
  private Integer version;
}
public class Game extends BaseEntity {
  @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
  @ToString.Exclude
  @OrderColumn(name = "sort_index")
  private List<Question> questions = new ArrayList<>();
}
@Inheritance(strategy = InheritanceType.JOINED)
@Table(name = "question")
public abstract class Question extends BaseEntity {
  @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
  @ToString.Exclude
  @OrderColumn(name = "sort_index")
  private List<AnswerOption> answerOptions = new ArrayList<>();
}
public class ImageSingleChoiceQuestion extends Question {
  @OneToOne(cascade = CascadeType.ALL)
  private AnswerOption validAnswer;
}
@Table(name = "answer_option")
public class AnswerOption extends BaseEntity {}

相当基本的设置。当我从游戏中删除问题时出现异常:

Game game = gameRepository.findById(gameId).orElseThrow()
Question question = questionRepository.findByGameAndId(gameId, questionId).orElseThrow()

game.getQuestions().remove(question)
gameRepository.saveAndFlush(game) // optional

预期结果:questiongame 分离并删除,将删除级联到 answerOptions。一直工作到Micronaut 3.0.3,您可以在示例项目中更改版本,测试会成功。

实际结果:

javax.persistence.OptimisticLockException: 
Batch update returned unexpected row count from update [0];
actual row count: 0; expected: 1; 
statement executed: delete from question where id=? and version=?

Here 是被执行的 SQL,注意版本号被弄乱了。任何帮助将不胜感激。

编辑 1:

仅在将 ImageSingleChoiceQuestion#setValidAnswer 与同样用于 Question#setAnswerOptions 的实例应用时才会出现问题

为什么会这样,因为这在 Micronaut 3.0.3 中有效?

编辑 2:

编辑 3:

  • 确认为错误并在PR中修复

【问题讨论】:

  • 嗨。使用 Micronaut Data 3.2.1,我的测试与您的原始代码一起通过。感谢您的合作。 BR

标签: hibernate jpa micronaut micronaut-data


【解决方案1】:

更新 正如@saw303 正确指出的那样,这是 Micronaut Data 版本

https://github.com/micronaut-projects/micronaut-data/releases

我用这个更改更新了我的 PR。


我能够使用您存储库中的代码重现该问题。 Micronaut 数据变更日志显示 v3.1.0 有相关变更。 https://github.com/micronaut-projects/micronaut-data/releases

只有在申请时才​​会出现问题 ImageSingleChoiceQuestion#setValidAnswer 带有一个也在 Question#setAnswerOptions 中使用的实例。

因此,这样做时问题就消失了: question.setValidAnswer(new AnswerOption());。解释:当在一对多和一对一中使用相同的 AnswerOption 实例时,Hibernate 会尝试在两个地方删除实例。

原因似乎是 CascadeType.ALL。从问题类中删除它时,测试通过了。

以下解决方案通过了测试,但目前我无法解释为什么它基于生成的 SQL 工作。这种方法需要进一步调查。

@Getter
@Setter
@ToString
@Entity(name = "ImageSingleChoiceQuestion")
@Table(name = "image_single_choice_question")
public class ImageSingleChoiceQuestion extends Question {

    @OneToOne
    @PrimaryKeyJoinColumn
    private AnswerOption validAnswer;
}

到目前为止,通过测试的最佳可解释解决方案是将@OneToOne 替换为@OneToMany,如下所示。 Hibernate 将使用这种方法创建连接表image_single_choice_question_valid_answers。这当然不是最优的。

@Getter
@Setter
@ToString
@Entity(name = "ImageSingleChoiceQuestion")
@Table(name = "image_single_choice_question")
public class ImageSingleChoiceQuestion extends Question {

    @OneToMany
    private Set<AnswerOption> validAnswers = new HashSet<>();

    public AnswerOption getValidAnswer() {
        return validAnswers.stream().findFirst().orElse(null);
    }

    public void setValidAnswer(final AnswerOption answerOption) {
        validAnswers.clear();
        validAnswers.add(answerOption);
    }
}

公关在这里: https://github.com/dpozinen/optimistic-lock/pull/1

我在你的项目中添加了测试容器,测试类现在看起来像这样:

package optimistic.lock

import optimistic.lock.answeroption.AnswerOption
import optimistic.lock.image.ImageSingleChoiceQuestion
import optimistic.lock.testframework.ApplicationContextSpecification
import optimistic.lock.testframework.testcontainers.MariaDbFixture

import javax.persistence.EntityManager
import javax.persistence.EntityManagerFactory

class NewOptimisticLockSpec extends ApplicationContextSpecification
        implements MariaDbFixture {

    @Override
    Map<String, Object> getCustomConfiguration() {
        [
                "datasources.default.url"     : "jdbc:mariadb://localhost:${getMariaDbConfiguration().get("port")}/opti",
                "datasources.default.username": getMariaDbConfiguration().get("username"),
                "datasources.default.password": getMariaDbConfiguration().get("password"),
        ]
    }

    GameRepository gameRepository
    QuestionRepository questionRepository

    TestDataProvider testDataProvider
    GameTestSvc gameTestSvc

    EntityManagerFactory entityManagerFactory
    EntityManager entityManager

    @SuppressWarnings('unused')
    void setup() {
        gameRepository = applicationContext.getBean(GameRepository)
        questionRepository = applicationContext.getBean(QuestionRepository)

        testDataProvider = applicationContext.getBean(TestDataProvider)
        gameTestSvc = applicationContext.getBean(GameTestSvc)

        entityManagerFactory = applicationContext.getBean(EntityManagerFactory)
        entityManager = entityManagerFactory.createEntityManager()
    }

    void "when removing question from game, game has no longer any questions"() {
        given:
        def testGame = testDataProvider.saveTestGame()
        and:
        Game game = gameRepository.findById(testGame.getId()).orElseThrow()
        and:
        Question question = testGame.questions.first()
        assert question != null
        and:
        def validAnswer = ((ImageSingleChoiceQuestion)question).getValidAnswer()
        assert validAnswer != null

        when:
        gameTestSvc.removeQuestionFromGame(game.getId(), question.getId())

        then:
        noExceptionThrown()
        and:
        0 == entityManager.find(Game.class, game.getId()).getQuestions().size()
        and:
        null == entityManager.find(AnswerOption.class, validAnswer.getId())
    }
}

【讨论】:

  • 感谢您的回答,能否请您详细说明几点:“仅在将 ImageSingleChoiceQuestion#setValidAnswer 与 Question#setAnswerOptions 中使用的实例一起应用时才会出现问题”而且:“最佳解释到目前为止的解决方案,就是像这样用OneToMany替换OneToOne“什么解释?
  • @dpozinen:为我的回答添加了更多解释。
  • “当在一对多和一对一中使用相同的 AnswerOption 实例时,Hibernate 会尝试在两个地方删除实例”正确,但是引用同一实体两次?这在升级之前有效。这是 micronaut 方面的回归还是相反 - 这是他们修复的错误,允许这种行为
  • @dpozinen 我猜你已经阅读了 Micronaut Data 3.1.0 的更新日志;有一些相关的变化:github.com/micronaut-projects/micronaut-data/releases
【解决方案2】:

您遇到了 Micronaut Data 3.1 中引入的错误。在您的情况下,版本属性不正确地递增。 Micronaut Data 团队已确认此错误,并且有一个待处理的拉取请求(请参阅PR)将解决此问题。

这是关于 io.micronaut.data.runtime.event.listeners.VersionGeneratingEntityEventListener 错误地增加了您的 JP A 实体的版本属性(请参阅 code pointer)。

我认为您可以期待即将发布的 Micronaut 版本 3.2.2 中的修复

【讨论】:

  • 感谢您指出这一点。该错误实际上已在 3.2.1 中修复。 BR
猜你喜欢
  • 2017-12-09
  • 2016-07-17
  • 2019-01-18
  • 2012-01-11
  • 1970-01-01
  • 2014-03-04
  • 2011-04-20
相关资源
最近更新 更多