Spring MVC and Hibernate

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:

class UniqueConstraints
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueConstraints {

  UniqueConstraint[] value();

}
class UniqueConstraint
@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.

interface UniquenessValidationService
public interface UniquenessValidationService {

  void validate(Object target) throws UniquenessViolationException;

}

Warning: here be a big chunk of code

class UniquenessValidationServiceImpl
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...

class UniquenessViolationException
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

class UniquenessValidator
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

Labels

spring spring Delete
mvc mvc Delete
hibernate hibernate Delete
validation validation Delete
Enter labels to add to this page:
Please wait 
Looking for a label? Just start typing.
  1. Jun 25, 2008

    Ürgo Ringo says:

    Very interesting post! I think we could also introduce field level uniqueness c...

    Very interesting post!

    I think we could also introduce field level uniqueness constraints.

    Also we could try to integrate this validation with Hibernate validator. Added benefit is that then we don't have to write any code for executing this rule check for each use case.

    Timur, you already know this maybe it is interesting to know for some that Hibernate validator can be used independently of any other Hibernate features. You can check this page [AQRIS:Implement single field validation using Validators] about integrating Hibernate validator and Spring validation.

    1. Jun 25, 2008

      Timur Strekalov says:

      It should actually be quite simple to add field-level constraints, but it would ...

      It should actually be quite simple to add field-level constraints, but it would require more time to go through all the methods and catch these constraints. I guess it then would be best to make this into a bigger validator with its own cache of constraints for entities which it would load on application startup, for example.

      And as for the Hibernate Validator, you're absolutely right. It should also be trivial to add this constraint to be picked by a ClassValidator automagically and register a handler for it. I'll look into it, shouldn't be too hard I just used an abstract validator for all of mine, which launched the Hibernate Validator automatically, so I didn't worry about that. But it's a good idea to try, thanks for the feedback