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..
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!)