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

Java 验证通过,但调用merge()时会引发验证错误,java,jpa,bean-validation,wildfly-10,vraptor,Java,Jpa,Bean Validation,Wildfly 10,Vraptor,很抱歉在这里问你,但我一辈子都不明白发生了什么事。几个小时以来,我一直在网上寻找答案,但运气不佳 我有一个用JPA建模的简单测试,使用的是运行在WildFly 10.0.0.Final服务器上的MVC框架,该服务器使用Hibernate 5.0.7.Final。测验有许多问题,每个问题都有2-10个备选答案 我目前正在为用户实现一种在测验中添加/删除问题的方法。在调用merge(quick)之前,我运行验证以确保所有内容都有效。它过去了。我没有错误 由于没有验证错误,我调用merge(quick

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

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

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

由于没有验证错误,我调用
merge(quick)
,最后遇到以下异常:

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("-------------");
    }
}
private void val(最终对象obj,最终字符串s){
最终验证器工厂=Validation.buildDefaultValidatorFactory();
final javax.validation.Validator Validator=factory.getValidator();
最终设置constraintViolations=validator.validate(obj);
对于(最终约束约束约束cv:constraintViolations){
log.info(“--------------”;
log.info(s+“ValidationConstraint:+cv.getConstraintDescriptor().getAnnotation());
log.info(s+“ValidationConstraint:+cv.getConstraintDescriptor());
log.info(s+“ValidationConstraint:+cv.getMessageTemplate());
log.info(s+“ValidationConstraint:+cv.getInvalidValue());
log.info(s+“ValidationConstraint:+cv.getLeafBean());
log.info(s+“ValidationConstraint:+cv.getRootBeanClass());
log.info(s+“ValidationConstraint:+cv.getPropertyPath().toString());
log.info(s+“ValidationConstraint:+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
    }
}
@Transactional
公共void updateQuestions(最终字符串quizId,最终问题列表){
//测验可能有段塞(/测验名称)
最终测验=FindQuizbydstring(quizId);
if(测验!=null){
(最后一个问题:问题){
问题.小测验(小测验);
if(question.getAlternations()!=null){
for(最终备选方案alt:question.getAlternations()){
备选问题(问题);
}
}
if(question.getId()!=null){
最后一道题old=(Question)ps.createQuery(“来自问题,其中id=:id和quick=:quick”).setParameter(“id”,Question.getId()).setParameter(“quick”,quick.getSingleResult();
//确保问题确实属于本次测验的主题
if(old==null){
问题.setId(空);
}
}
if(question.getId()==null){
//设置新问题(谁创建的、时间戳等)
}
}
小测验.设置问题(问题);
if(!validator.validate(quick.hasErrors()){
试一试{
entityManager.merge(测验);
}捕获(最终异常e){
if(log.isErrorEnabled()){log.error(“更新测验问题时出错”,e);}
}
}
}
否则{
//向用户发送一个错误
}
}
最后,这些是(我认为)我的实体的相关部分:

@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
    /* ... */
}
@实体
公开课测验{
/* ... */
@Valid//FYI:这只是进行验证级联
@OneToMany(mappedBy=“quick”,cascade=CascadeType.ALL,fetch=FetchType.EAGER,orphan=true)
私人名单问题;
/* ... */
}
@实体
公开课问题{
/* ... */
@有效的
@空空如也
@尺寸(最小值=2,最大值=10)
@OneToMany(mappedBy=“question”,cascade=CascadeType.ALL,fetch=FetchType.LAZY,orphan=true)
私人名单备选方案;
/* ... */
}
@实体
公共类替代方案{
/* ... */
@不空白
@大小(最大值=0xFF)
@列(长度=0xFF,可空=false)
私有字符串文本;//必须填写的唯一字段
/* ... */
}

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

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

在Java中,拷贝是所有生命的祸根。因为问题的所有实例都有一个未复制的备选方案数组(例如
List
)(据我所知,可能是因为
List
不是一个实体,或者只是因为副本很浅)

好吧,不管怎样,感谢所有试图帮助你的人,祝所有将来可能遇到这种情况的人好运

[编辑] 问题在于JPA所做的复制。由于
测验
有一个
列表
它会被复制,但由于某种原因我不能完全确定(浅拷贝?)每个
问题的
列表
没有被复制。这就是为什么
@NotEmpty
验证在
问题
备选方案
字段失败的原因

在合并之前对每个新的
问题调用
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));
    }
}
for(int i=0,max=questions.size();i