Validating entities for unique constraints violations
Once again, apologies for the long title
I think it's the last blog rant of mine before the joining the military, so I must make it worth your while. I'll try, promise ![]()
I for one have always been bothered when writing/seeing chunks of code like this:
try { // save/update operation here } catch (final DataIntegrityViolationException e) { // tell the user than something happened }
This approach has serious drawbacks. Firstly, you cannot tell which constraint has been violated, so you don't know actually what to tell the user. Secondly, it's just plain ugly and spawns code scattering, which is bad.
After thinking and playing for a while, I've come up with a different solution to the problem, which doesn't have the aforementioned drawbacks. This particular approach relies on the Hibernate Criteria API, but I guess other ORM providers have some kind of similar API to facilitate the problem.
First, we need some kind of metadata bearer, for the javax.persistence.Table's uniqueConstraints() parameter doesn't help too much: it allows you to specify database column names, but not entity property names... Moreover, it doesn't allow you to tell which I18N keys to use on a constraint failure. So it just won't do here, thus let's create our own:
@Documented @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface UniqueConstraints { UniqueConstraint[] value(); }
@Documented @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) public @interface UniqueConstraint { /** * A set of class fields representing a unique constraint (to allow * multi-field constraints) */ String[] value(); /** * I18N message key on constraint failure (a default one will be constructed * on the fly if not specified here) */ String key() default ""; }
Okay, that's it for our metadata carriers. What we need now is some kind of class to actually analyze this metadata and make use of it.
public interface UniquenessValidationService { void validate(Object target) throws UniquenessViolationException; }
Warning: here be a big chunk of code ![]()
public final class UniquenessValidationServiceImpl implements UniquenessValidationService { private static final char I18N_KEY_SEPARATOR = '.'; private final SessionFactory sessionFactory; public UniquenessValidationServiceImpl(final SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; } @Override public void validate(final Object target) throws UniquenessViolationException { final Class<?> targetClass = target.getClass(); final UniqueConstraints constraints = targetClass .getAnnotation(UniqueConstraints.class); if (constraints == null) { return; } final List<String> messageKeys = new ArrayList<String>(); final List<String> fallbackMessages = new ArrayList<String>(); for (final UniqueConstraint constraint : constraints.value()) { final Criteria c = createCriteria(targetClass); final String[] fields = constraint.value(); for (final String field : fields) { // There's no need to show the ReflectionUtils.getValue(Object, String)'s // implementation, it's easy and you can figure it out yourself, I know :) final Object value = ReflectionUtils.getValue(target, field); c.add(Restrictions.eq(field, value)); } final Object uniqueResult = c.uniqueResult(); if (uniqueResult != null && uniqueResult != target) { messageKeys.add(constructI18nKey(targetClass, constraint)); fallbackMessages.add(constructFallbackMessage(constraint)); } } if (messageKeys.size() > 0) { final String[] keysArray = messageKeys.toArray(new String[0]); final String[] fallbackMessagesArray = fallbackMessages .toArray(new String[0]); throw new UniquenessViolationException(keysArray, fallbackMessagesArray); } } private static String constructI18nKey(final Class<?> targetClass, final UniqueConstraint constraint) { if (StringUtils.isNotBlank(constraint.key())) { return constraint.key(); } final String[] fields = constraint.value(); final StringBuilder buf = new StringBuilder(fields.length * 7); buf.append("validator").append(I18N_KEY_SEPARATOR).append( targetClass.getSimpleName()); for (final String field : fields) { buf.append(I18N_KEY_SEPARATOR).append(field); } return buf.toString(); } private static String constructFallbackMessage( final UniqueConstraint constraint) { final String[] fields = constraint.value(); if (fields.length == 1) { return fields[0] + " must be a unique value"; } return StringUtils.join(fields, ", ") + " must be a unique tuple"; } private Session getSession() { return sessionFactory.getCurrentSession(); } private Criteria createCriteria(final Class<?> targetClass) { return getSession().createCriteria(targetClass); } }
As for the exception...
public final class UniquenessViolationException extends CheckedSystemException { private static final String LENGTH_ASSERTION_ERROR = "must supply an equal number of constraint violation message keys and fallback messages"; private static final long serialVersionUID = 1L; private final String[] messageKeys; private final String[] fallbackMessages; public UniquenessViolationException(final String[] messageKeys, final String[] fallbackMessages) { final boolean equalLength = messageKeys.length == fallbackMessages.length; Assert.isTrue(equalLength, LENGTH_ASSERTION_ERROR); this.messageKeys = messageKeys; this.fallbackMessages = fallbackMessages; } public String[] getMessageKeys() { return messageKeys; } public String[] getFallbackMessages() { return fallbackMessages; } }
... and the Spring validator
public final class UniquenessValidator implements Validator { private final UniquenessValidationService validationService; public UniquenessValidator( final UniquenessValidationService validationService) { this.validationService = validationService; } @Override @SuppressWarnings("unchecked") public boolean supports(final Class clazz) { // calling to Object.class.isAssignableFrom(clazz) would be fun :) return true; } @Override public void validate(final Object target, final Errors errors) { try { validationService.validate(target); } catch (final UniquenessViolationException e) { final String[] constraintViolationMessages = e.getMessageKeys(); final String[] fallbackMessages = e.getFallbackMessages(); for (int i = 0; i < constraintViolationMessages.length; i++) { errors.reject(constraintViolationMessages[i], fallbackMessages[i]); } } } }
And now plug this validator anywhere you want. I just injected it into my AbstractValidator and now I'm a happy, well-adjusted developer ![]()
Now let's just use our brand new annotation on some domain object...
@Entity
@UniqueConstraints({ @UniqueConstraint({ "group", "name" }) })
public final class ExtraSpecialDomainObject {
// dunno what this group is about, but bear with me
private Group group;
private String name;
public Group getGroup() {
return group;
}
public void setGroup(final Group group) {
this.group = group;
}
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
}
Phew, lots of code and not so many words, but I think everything here should be pretty much self-explanatory. It seems that this works for me, but if anyone notices even the slightest bit of a problem here, let me know ASAP so I can fix it ![]()
Thanks for reading this, feedback is very welcome ![]()