News from Jun 22, 2008

  2008/06/22
Spring MVC and Hibernate
Last changed: Jul 30, 2008 11:03 by Auris Aume
Labels: spring, mvc, hibernate, validation

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

Posted at 22 Jun @ 12:33 PM by Timur Strekalov | 2 Comments