Accessing the Hibernate Session within a ConstraintValidator

Here are all the pieces you need to write a ConstraintValidator which has access to the Hibernate Session respectively Hibernate EntityManager. This example will implement a @Unique constraint as discussed in HV-230. The reason @Unique is not part of the built-in constraints is the fact that accessing the Session/EntityManager during a valiation is opening yourself up for potenital phantom reads. Think twice before you go for the following approach.

 

First you will need a custom ConstraintValidatorFactory. This factory has a handle to the Hibernate SessionFactory in order to inject it when a new ConstraintValidator is created.

 

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorFactory;

import org.hibernate.SessionFactory;
import org.hibernate.validator.constraints.SessionAwareConstraintValidator;

public class SessionAwareConstraintValidatorFactory extends ConstraintValidatorFactoryImpl
        implements ConstraintValidatorFactory {

    private SessionFactory sessionFactory;

    public SessionAwareConstraintValidatorFactory() {
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        T constraintValidator = super.getInstance( key );
        if ( constraintValidator instanceof SessionAwareConstraintValidator ) {
            ( ( SessionAwareConstraintValidator ) constraintValidator ).setSessionFactory( sessionFactory );
        }
        return constraintValidator;
    }
}

 

Next we introduce a base class for session aware ConstraintValidators:

 

import javax.validation.ConstraintValidatorContext;
import javax.validation.ValidationException;

import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.SessionFactory;

public abstract class SessionAwareConstraintValidator<T> {
    private SessionFactory sessionFactory;
    boolean openedNewTransaction;
    private Session tmpSession;

    public SessionAwareConstraintValidator() {
    }

    public boolean isValid(T value, ConstraintValidatorContext context) {
        openTmpSession();
        boolean result = isValidInSession( value, context );
        closeTmpSession();
        return result;
    }

    public abstract boolean isValidInSession(T value, ConstraintValidatorContext context);

    public SessionFactory getSessionFactory() {
        return sessionFactory;
    }

    public void setSessionFactory(SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

    public Session getTmpSession() {
        return tmpSession;
    }

    private void openTmpSession() {
        Session currentSession;
        try {
            currentSession = getSessionFactory().getCurrentSession();
        }
        catch ( HibernateException e ) {
            throw new ValidationException( "Unable to determine current Hibernate session", e );
        }
        if ( !currentSession.getTransaction().isActive() ) {
            currentSession.beginTransaction();
            openedNewTransaction = true;
        }
        try {
            tmpSession = getSessionFactory().openSession( currentSession.connection() );
        }
        catch ( HibernateException e ) {
            throw new ValidationException( "Unable to open temporary session", e );
        }
    }

    private void closeTmpSession() {
        if ( openedNewTransaction ) {
            try {
                getSessionFactory().getCurrentSession().getTransaction().commit();
            }
            catch ( HibernateException e ) {
                throw new ValidationException( "Unable to commit transaction for temporary session", e );
            }
        }
        try {
            tmpSession.close();
        }
        catch ( HibernateException e ) {
            throw new ValidationException( "Unable to close temporary Hibernate session", e );
        }
    }
}

 

Let's now first look at the constraint we want to create:

 

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

import org.hibernate.validator.constraints.impl.UniqueValidator;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Constraint(validatedBy = { UniqueValidator.class })
@Target({ TYPE, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
public @interface Unique {
    String message() default "{org.hibernate.validator.constraints.Unique.message}";

    String[] properties();

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

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

 

The actual validator implementation looks now like this:

 

import java.io.Serializable;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.hibernate.EntityMode;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.validator.constraints.SessionAwareConstraintValidator;
import org.hibernate.validator.constraints.Unique;

public class UniqueValidator extends SessionAwareConstraintValidator<Object>
        implements ConstraintValidator<Unique, Object> {

    private String[] fields;

    public void initialize(Unique annotation) {
        this.fields = annotation.properties();
    }

    public boolean isValidInSession(Object value, ConstraintValidatorContext context) {
        if ( value == null ) {
            return true;
        }
        return countRows( value ) == 0;
    }

    private int countRows(Object value) {
        ClassMetadata meta = getSessionFactory().getClassMetadata( value.getClass() );
        String idName = meta.getIdentifierPropertyName();
        Serializable id = meta.getIdentifier( value, ( SessionImplementor ) getTmpSession() );

        DetachedCriteria criteria = DetachedCriteria.forClass( value.getClass() );
        for ( String field : fields ) {
            criteria.add( Restrictions.eq( field, meta.getPropertyValue( value, field, EntityMode.POJO ) ) );
        }
        criteria.add( Restrictions.ne( idName, id ) ).setProjection( Projections.rowCount() );

        List results = criteria.getExecutableCriteria( getTmpSession() ).list();
        Number count = ( Number ) results.iterator().next();
        return count.intValue();
    }
}