1

Sorry for asking here but I can't for the life of me understand what is going on. Been looking for answers all around the web for hours with no luck.

I have a simple Quiz modelled in JPA, using VRaptor (an MVC framework) running in a WildFly 10.0.0.Final server which uses Hibernate 5.0.7.Final. A Quiz have many Questions which have 2-10 Alternatives each.

I'm currently implementing a method for users to add/remove Questions on a Quiz. Before calling merge(quiz) I run the validations to make sure everything is valid. It passes. I get no errors.

Since there are no validation errors, I call merge(quiz) and finally I'm greeted with the following exception:

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] If I deliberately leave something blank it does show the validation error and doesn't try to merge(), so validations are being run as expected.

I've checked the whole thing manually and there really are no errors. Used this "alternative" method to check and print validation errors:

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("-------------");
    }
}

This is roughly what my add/remove Questions method do:

@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
    }
}

And finally these are the (what I think) relevant parts of my entities:

@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
    /* ... */
}
Rafael Lins
  • 458
  • 7
  • 12

1 Answers1

2

Got it. A friend suggested using persist() on the new Questions prior to the merge().

Although he said its because merge() won't create the new entities it will. According to the JPA spec if the entity doesn't exist a new instance of it will be created in the persistence context and the original will be copied into it.

Copies, in Java, are the bane of all life. Since all instances of Question have an array (instance of List) of Alternatives that wasn't copied (as far as I know probably because List isn't an entity or simply because the copies are shallow).

Well, thanks anyway to anyone who tried to help and very good luck to anyone that might bump into this on the future.

[Edit] Problem was because of the copying JPA does. Since Quiz have a List<Question> it'll be copied but for a reason I'm not quite totally sure (shallow copies?) the List<Alternative> of each Question wasn't being copied. That's why the @NotEmpty validation failed at the alternatives field of Question.

Calling persist() on each new Question before merging makes them part of the Persistence Context and a copy won't be necessary anymore.

Did it by doing this:

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));
    }
}
Rafael Lins
  • 458
  • 7
  • 12