验证通过,但调用merge()时会引发验证错误



很抱歉问这里,但我一辈子都不明白发生了什么。几个小时来,我一直在网上寻找答案,但运气不佳。

我有一个用JPA建模的简单测验,使用在WildFly 10.0.0.Final服务器上运行的VRaptor(一个MVC框架),该服务器使用Hibernate 5.0.7.Final。测验有很多问题,每个问题都有2-10备选方案。

我目前正在为用户实现一种在测验中添加/删除问题的方法。在调用merge(quiz)之前,我运行验证以确保所有内容都是有效的。它通过了。我没有任何错误。

由于没有验证错误,我调用了merge(quiz),最后出现以下异常:

javax.validation.ConstraintViolationException: Validation failed for classes [game.Question] during persist time for groups [javax.validation.groups.Default, ]
List of constraint violations:[
ConstraintViolationImpl{interpolatedMessage='Cannot be empty', propertyPath=alternatives, rootBeanClass=class game.Question, messageTemplate='{org.hibernate.validator.constraints.NotEmpty.message}'}
]

[Edit]如果我故意将某些内容留空,它确实显示了验证错误,并且没有尝试merge(),因此验证正在按预期运行。

我已经手动检查了整个过程,确实没有任何错误。使用这种"替代"方法检查和打印验证错误:

private void val(final Object obj, final String s) {
final ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
final javax.validation.Validator validator = factory.getValidator();
final Set<ConstraintViolation<Object>> constraintViolations = validator.validate(obj);
for (final ConstraintViolation cv : constraintViolations) {
log.info("-------------");
log.info(s + " ValidatationConstraint: " + cv.getConstraintDescriptor().getAnnotation());
log.info(s + " ValidatationConstraint: " + cv.getConstraintDescriptor());
log.info(s + " ValidatationConstraint: " + cv.getMessageTemplate());
log.info(s + " ValidatationConstraint: " + cv.getInvalidValue());
log.info(s + " ValidatationConstraint: " + cv.getLeafBean());
log.info(s + " ValidatationConstraint: " + cv.getRootBeanClass());
log.info(s + " ValidatationConstraint: " + cv.getPropertyPath().toString());
log.info(s + " ValidatationConstraint: " + cv.getMessage());
log.info("-------------");
}
}

这大致就是我的添加/删除问题方法所做的:

@Transactional
public void updateQuestions(final String quizId, final List<Question> questions) {
// Quizzes might have slugs (/quiz-name)
final Quiz quiz = findQuizByIdString(quizId);
if (quiz != null) {
for (final Question question : questions) {
question.setQuiz(quiz);
if (question.getAlternatives() != null) {
for (final Alternative alt : question.getAlternatives()) {
alt.setQuestion(question);
}
}
if (question.getId() != null) {
final Question old = (Question) ps.createQuery("FROM Question WHERE id = :id AND quiz = :quiz").setParameter("id", question.getId()).setParameter("quiz", quiz).getSingleResult();
// Making sure the Question do belong to the this Quiz
if (old == null) {
question.setId(null);
}
}
if (question.getId() == null) {
// Set the new question up (who created, timestamp, etc.)
}
}
quiz.setQuestions(questions);
if (!validator.validate(quiz).hasErrors()) {
try {
entityManager.merge(quiz);
} catch (final Exception e) {
if (log.isErrorEnabled()) { log.error("Error while updating Quiz Questions", e); }
}
}
}
else {
// Send an error to the user
}
}

最后,这些是我的实体的(我认为的)相关部分:

@Entity
public class Quiz {
/* ... */
@Valid // FYI: This just makes the validation cascade
@OneToMany(mappedBy = "quiz", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
private List<Question> questions;
/* ... */
}
@Entity
public class Question {
/* ... */
@Valid
@NotEmpty
@Size(min = 2, max = 10)
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, fetch = FetchType.LAZY, orphanRemoval = true)
private List<Alternative> alternatives;
/* ... */
}
@Entity
public class Alternative {
/* ... */
@NotBlank
@Size(max = 0xFF)
@Column(length = 0xFF, nullable = false)
private String text; // The only field that must be filled
/* ... */
}

明白了。一位朋友建议在merge()之前的新问题上使用persist()

尽管他说这是因为merge()不会创建新的实体。根据JPA规范,如果实体不存在,将在持久性上下文中创建一个新的实体实例,并将原始实例复制到其中。

在爪哇,复制品是所有生命的祸根。由于Question的所有实例都有一个Alternatives的数组(List的实例),没有被复制(据我所知,可能是因为List不是一个实体,或者只是因为副本很浅)。

好吧,无论如何,感谢任何试图提供帮助的人,并祝未来可能遇到这种情况的人好运。

[编辑]问题是因为JPA的复制。由于Quiz有一个List<Question>,它将被复制,但出于某种原因,我不完全确定(浅拷贝?)每个QuestionList<Alternative>没有被复制。这就是@NotEmpty验证在Questionalternatives字段失败的原因。

在合并之前对每个新的Question调用persist()会使它们成为持久性上下文的一部分,并且不再需要副本。

通过这样做做到了:

for (int i = 0, max = questions.size(); i < max; i++) {
Question question = questions.get(i);
/* all of that previous code */
if (question.getId() == null) {
entityManager.persist(question);
}
else {
/* merge() returns the newly merged and managed (as in it is now part of
the persistence context) instance, so replace the "old" one */
questions.set(i, entityManager.merge(question));
}
}

最新更新