1

很抱歉在这里问,但我一生都无法理解发生了什么。几个小时以来一直在网上寻找答案,但没有运气。

我有一个在 JPA 中建模的简单测验,使用在使用 Hibernate 5.0.7.Final 的 WildFly 10.0.0.Final 服务器中运行的VRaptor (一个 MVC 框架)。测验有许多问题,每个问题有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}'}
]

[编辑] 如果我故意将某些内容留空,它会显示验证错误并且不会尝试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
    /* ... */
}
4

1 回答 1

2

知道了。一位朋友建议persist()merge().

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

在 Java 中,副本是所有生命的祸根。由于 Question 的所有实例都有一个未复制的 Alternatives 数组(的实例(据我所知,可能是因为List它不是实体或仅仅是因为副本很浅)。List

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

[编辑] 问题是因为复制 JPA 所做的。既然QuizList<Question>它会被复制,但出于某种原因,我不太确定(浅拷贝?)每个List<Alternative>Question没有被复制。这就是@NotEmpty验证alternativesQuestion.

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

通过这样做:

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));
    }
}
于 2016-06-02T17:21:16.090 回答