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();
        }
    }