Search code examples
javahibernatejpaseamconcurrentmodification

Custom Validation Annotation introduces ConcurrentModificationException


I was tasked with creating an Annotation for Custom Validation. This was due to some problems with handling database constraint violations nicely. What I did in response to this was relatively simple. I created a class-level CustomConstraint specifically for the one domain-class that required it. What I got as my current result is the following:

@UniqueLocation Annotation:

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = UniqueLocationValidator.class)
@Documented
public @interface UniqueLocation {

    String message() default "must be unique!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

This is not spectacular, in fact it's copied almost verbatim from the hibernate documentation.

I proceeded to create my UniqueLocationValidator and ran into a problem with using the persistence context in there. I wanted to run a defensive select, and thusly tried to Inject my application wide @Produces @PersistenceContext EntityManager.

Therefor I included JBoss Seam to use it's InjectingConstraintValidatorFactory configuring my validation.xml as follows:

<?xml version="1.0" encoding="UTF-8"?>
<validation-config 
   xmlns="http://jboss.org/xml/ns/javax/validation/configuration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration validation-configuration-1.0.xsd">

   <constraint-validator-factory>
      org.jboss.seam.validation.InjectingConstraintValidatorFactory
   </constraint-validator-factory>

</validation-config>

After running into some issues with Creating Constraint Violations this is how my Validator actually looks:

@ManagedBean
public class UniqueLocationValidator implements
        ConstraintValidator<UniqueLocation, Location> {
    // must not return a result for name-equality on the same Id
    private final String QUERY_STRING = "SELECT * FROM Location WHERE locationName = :value AND id <> :id";

    @Inject
    EntityManager entityManager;

    private String constraintViolationMessage;

    @Override
    public void initialize(final UniqueLocation annotation) {
        constraintViolationMessage = annotation.message();
    }

    @Override
    public boolean isValid(final Location instance,
            final ConstraintValidatorContext context) {
        if (instance == null) {
            // Recommended, instead use explicit @NotNull Annotation for
            // validating non-nullable instances
            return true;
        }

        if (duplicateLocationExists(instance)) {
            createConstraintViolations(context);
            return false;
        } else {
            return true;
        }
    }

    private void createConstraintViolations(
            final ConstraintValidatorContext context) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(constraintViolationMessage)
                .addNode("locationName").addConstraintViolation();
    }

    private boolean duplicateLocationExists(final Location location) {
        final String checkedValue = location.getLocationName();
        final long id = location.getId();

        Query defensiveSelect = entityManager.createNativeQuery(QUERY_STRING)
                .setParameter("value", checkedValue).setParameter("id", id);

        return !defensiveSelect.getResultList().isEmpty();
    }
}

So much for my current configuration, now to the real beef, the problem:

When I run following code after recieving an action from a user, the thing works wonderfully and correctly marks a duplicate location name as invalid.. Also persisting works just fine when the locationName is not duplicated.

public long add(@Valid final Location location) {
    entityManager.persist(location);
    return location.getId();
}

Mind that the entityManager here and the entityManager in the UniqueLocationValidator are both injected via Weld CDI from the aforementioned @PersistenceContext EntityManager.

What does not work is the following:

public long update(@Valid final Location location){
    entityManager.merge(location);
    return location.getId();
}

When calling this code, I get a relatively short stacktrace, that has a ConcurrentModificationException as root-cause.

I neither understand why that's the case, nor how I would go about fixing this. I have nowhere attempted to explicitly multithread my application, so this should have been managed by the JBoss 7.1.1-Final I am using as application server..


Solution

  • What you're trying to do is not possible via the EntityManager. Well, not normally.

    Your validator is called during the processing of the updates. Queries sent via the EntityManager affect the internal storage, the ActionQueue of the EntityManager. This is what causes the ConcurrentModificationException: the results from your query alter the list that the EntityManager is iterating through when flushing changes.

    A workaround for this would be to bypass the EntityManager.

    How can we do this?

    Well, ... it's a bit dirty, since you're effectively adding a dependency on the hibernate implementation, but you can get the connection from the Session or EntityManager in various ways. And once you have a java.sql.Connection object, well, you can use something like a PreparedStatement to execute your query anyway.


    Example fix:

    Session session = entityManager.unwrap(Session.class);
    SessionFactoryImplementor sessionFactoryImplementation = (SessionFactoryImplementor) session.getSessionFactory();
    ConnectionProvider connectionProvider = sessionFactoryImplementation.getConnectionProvider();
    try {
           connection = connectionProvider.getConnection();
           PreparedStatement ps = connection.prepareStatement("SELECT 1 FROM Location WHERE id <> ? AND locationName = ?");
           ps.setLong(1, id);
           ps.setString(2, checkedValue);
           ResultSet rs = ps.executeQuery();
           boolean result = rs.next();//found any results? if we can retrieve a row: yes!
           rs.close();
           return result;
    }//catch SQLException etc... 
    //finally, close resources (only the resultset!)