我试图升级到Micronaut3.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
预期结果:将question
从game
中分离并删除,将删除级联到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=?
这里是执行的SQL,注意版本号被搞砸了。任何帮助都将不胜感激。
编辑1:
仅在将ImageSingleChoiceQuestion#setValidAnswer应用于也在Question#setAnswerOptions中使用的实例时才会出现问题。
为什么是这样,因为这在Micronaut 3.0.3中有效?
编辑2:
编辑3:
发布于 2021-12-05 18:45:53
正如@saw303正确指出的那样,更新是Micronaut数据版本<= 3.2.0中的一个错误。这个错误是在io.micronaut.data:micronaut-data-hibernate-jpa:3.2.1中修复的,它是在2021-12-07发布的(今天写这篇文章时)。
https://github.com/micronaut-projects/micronaut-data/releases
我用这个变化更新了我的公关。
我可以用你回购的代码来重现这个问题。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);
}
}
PR在这里: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())
}
}
https://stackoverflow.com/questions/70153405
复制相似问题