nidhin
nidhin

Reputation: 6920

hibernate unique key validation

I have a field, say, user_name, that should be unique in a table.

What is the best way for validating it using Spring/Hibernate validation?

Upvotes: 18

Views: 34180

Answers (7)

Ahmed
Ahmed

Reputation: 81

From my point of view the solutions provided here are missing very important case, which is update. We have to consider the primary key while asking our JPA APIs for persist or megre, So you MUST exclude the current entity from the uniqueness check (by using the primary key).

The below demonstration uses Spring Framework.

The annotation:

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = UniqueValidator.class)
public @interface Unique {
    String[] fields();
    String primaryKey();

    String message() default "Email address must be unique!";

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

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

    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @interface List {
        Unique[] value();
    }
}

Annotation validator implementation:

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;

@Slf4j
@Component
public class UniqueValidator implements ConstraintValidator<Unique, Serializable> {
    @Autowied
    private EntityManager entityManager;

    private String[] fields;
    private String primaryKey;

    @Override
    public void initialize(Unique constraintAnnotation) {
        this.fields = constraintAnnotation.fields();
        this.primaryKey = constraintAnnotation.primaryKey();
    }

    @Override
    public boolean isValid(Serializable target, ConstraintValidatorContext context) {
        log.info("start validation...");
        if(entityManager != null) {
            Class entityClass = target.getClass();
            CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
            CriteriaQuery<?> criteriaQuery = criteriaBuilder.createQuery(entityClass);
            Root<?> root = criteriaQuery.from(entityClass);
            List<Predicate> predicates = new ArrayList(fields.length + 1);

            try {
                PropertyDescriptor desc = new PropertyDescriptor(primaryKey, entityClass);
                Method readMethod = desc.getReadMethod();
                Object propertyValue = readMethod.invoke(target);
                Predicate predicate = criteriaBuilder.notEqual(root.get(primaryKey), propertyValue);
                predicates.add(predicate);

                for (int i = 0; i < fields.length; i++) {
                    String propertyName = fields[i];
                    desc = new PropertyDescriptor(propertyName, entityClass);
                    readMethod = desc.getReadMethod();
                    propertyValue = readMethod.invoke(target);
                    predicate = criteriaBuilder.equal(root.get(propertyName), propertyValue);
                    predicates.add(predicate);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }

            criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()]));
            Query typedQuery = entityManager.createQuery(criteriaQuery);
            List<Object> resultSet = typedQuery.getResultList();
            log.info("found {}", resultSet);
            return resultSet.size() == 0;
        }
        return true;
    }
}

in case of you want use @Unique more than once for the same entity:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Uniques {
    Unique[] value();
}

The JPA entity:

@Unique(fields ={"name", "email"}, primaryKey = "id")
@Unique(fields ={"phoneNumber"}, primaryKey = "id")
public class User implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Integer id;

    @Column(length = 60, nullable = false)
    private String name;

    @Column(length = 128, nullable = false, unique = true)
    private String email;

    @Column(length = 30, nullable = false)
    private String phoneNumber;
}

Upvotes: 0

Julien
Julien

Reputation: 2756

I've found kind of a tricky solution.

First, I've implemented the unique contraint to my MySql database :

CREATE TABLE XMLTAG
(
    ID INTEGER NOT NULL AUTO_INCREMENT,
    LABEL VARCHAR(64) NOT NULL,
    XPATH VARCHAR(128),
    PRIMARY KEY (ID),
    UNIQUE UQ_XMLTAG_LABEL(LABEL)
) ;

You see that I manage XML Tags that are defined by a unique label and a text field named "XPath".

Anyway, the second step is to simply catch the error raised when the user tries to do a bad update. A bad update is when trying to replace the current label by an existing label. If you leave the label untouched, no problemo. So, in my controller :

    @RequestMapping(value = "/updatetag", method = RequestMethod.POST)
    public String updateTag(
            @ModelAttribute("tag") Tag tag, 
            @Valid Tag validTag,
            BindingResult result,
            ModelMap map) {
        
        if(result.hasErrors()) {        // you don't care : validation of other
            return "editTag";       // constraints like @NotEmpty
        }
        

        try {
            tagService.updateTag(tag);    // try to update
            return "redirect:/tags";   // <- if it works        
        }
        catch (DataIntegrityViolationException ex) { // if it doesn't work
            result.rejectValue("label", "Unique.tag.label"); // pass an error message to the view 
            return "editTag"; // same treatment as other validation errors
        }
    }

This may conflict with the @Unique pattern but you can use this dirty method to valid the adding too.

Note : there is still one problem : if other validation errors are catched before the exception, the message about unicity will not be displayed.

Upvotes: 3

Tanmoy Mandal
Tanmoy Mandal

Reputation: 496

This code is based on the previous one implemented using EntityManager. In case anyone need to use Hibernate Session. Custom Annotation using Hibernate Session.
@UniqueKey.java

import java.lang.annotation.*;
import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueKeyValidator.class)
@Documented
public @interface UniqueKey {
    String columnName();
    Class<?> className();
    String message() default "{UniqueKey.message}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

UnqieKeyValidator.java

import ch.qos.logback.classic.gaffer.PropertyUtil;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
@Transactional
@Repository
public class UniqueKeyValidator implements ConstraintValidator<UniqueKey, String> {

    @Autowired
    private SessionFactory sessionFactory;

    public Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    private String columnName;
    private Class<?> entityClass;

    @Override
    public void initialize(UniqueKey constraintAnnotation) {
        this.columnNames = constraintAnnotation.columnNames();
        this.entityClass = constraintAnnotation.className();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        Class<?> entityClass = this.entityClass;
        System.out.println("class: " + entityClass.toString());
        Criteria criteria = getSession().createCriteria(entityClass);
        try {
                criteria.add(Restrictions.eq(this.columnName, value));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return criteria.list().size()==0;
    }
}

Usage

@UniqueKey(columnNames="userName", className = UserEntity.class)
// @UniqueKey(columnNames="userName") // unique key

Upvotes: 2

Raghuram
Raghuram

Reputation: 52645

One possibility is to annotate the field as @NaturalId

Upvotes: 5

dira
dira

Reputation: 30594

One of the possible solutions is to create custom @UniqueKey constraint (and corresponding validator); and to look-up the existing records in database, provide an instance of EntityManager (or Hibernate Session)to UniqueKeyValidator.

EntityManagerAwareValidator

public interface EntityManagerAwareValidator {  
     void setEntityManager(EntityManager entityManager); 
} 

ConstraintValidatorFactoryImpl

public class ConstraintValidatorFactoryImpl implements ConstraintValidatorFactory {

    private EntityManagerFactory entityManagerFactory;

    public ConstraintValidatorFactoryImpl(EntityManagerFactory entityManagerFactory) {
        this.entityManagerFactory = entityManagerFactory;
    }

    @Override
    public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
        T instance = null;

        try {
            instance = key.newInstance();
        } catch (Exception e) { 
            // could not instantiate class
            e.printStackTrace();
        }

        if(EntityManagerAwareValidator.class.isAssignableFrom(key)) {
            EntityManagerAwareValidator validator = (EntityManagerAwareValidator) instance;
            validator.setEntityManager(entityManagerFactory.createEntityManager());
        }

        return instance;
    }
}

UniqueKey

@Constraint(validatedBy={UniqueKeyValidator.class})
@Target({ElementType.TYPE})
@Retention(RUNTIME)
public @interface UniqueKey {

    String[] columnNames();

    String message() default "{UniqueKey.message}";

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

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

    @Target({ ElementType.TYPE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        UniqueKey[] value();
    }
}

UniqueKeyValidator

public class UniqueKeyValidator implements ConstraintValidator<UniqueKey, Serializable>, EntityManagerAwareValidator {

    private EntityManager entityManager;

    @Override
    public void setEntityManager(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    private String[] columnNames;

    @Override
    public void initialize(UniqueKey constraintAnnotation) {
        this.columnNames = constraintAnnotation.columnNames();

    }

    @Override
    public boolean isValid(Serializable target, ConstraintValidatorContext context) {
        Class<?> entityClass = target.getClass();

        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

        CriteriaQuery<Object> criteriaQuery = criteriaBuilder.createQuery();

        Root<?> root = criteriaQuery.from(entityClass);

        List<Predicate> predicates = new ArrayList<Predicate> (columnNames.length);

        try {
            for(int i=0; i<columnNames.length; i++) {
                String propertyName = columnNames[i];
                PropertyDescriptor desc = new PropertyDescriptor(propertyName, entityClass);
                Method readMethod = desc.getReadMethod();
                Object propertyValue = readMethod.invoke(target);
                Predicate predicate = criteriaBuilder.equal(root.get(propertyName), propertyValue);
                predicates.add(predicate);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()]));

        TypedQuery<Object> typedQuery = entityManager.createQuery(criteriaQuery);

        List<Object> resultSet = typedQuery.getResultList(); 

        return resultSet.size() == 0;
    }

}

Usage

@UniqueKey(columnNames={"userName"})
// @UniqueKey(columnNames={"userName", "emailId"}) // composite unique key
//@UniqueKey.List(value = {@UniqueKey(columnNames = { "userName" }), @UniqueKey(columnNames = { "emailId" })}) // more than one unique keys
public class User implements Serializable {

    private String userName;
    private String password;
    private String emailId;

    protected User() {
        super();
    }

    public User(String userName) {
        this.userName = userName;
    }
        ....
}

Test

public void uniqueKey() {
    EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("default");

    ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
    ValidatorContext validatorContext = validatorFactory.usingContext();
    validatorContext.constraintValidatorFactory(new ConstraintValidatorFactoryImpl(entityManagerFactory));
    Validator validator = validatorContext.getValidator();

    EntityManager em = entityManagerFactory.createEntityManager();

    User se = new User("abc", poizon);

       Set<ConstraintViolation<User>> violations = validator.validate(se);
    System.out.println("Size:- " + violations.size());

    em.getTransaction().begin();
    em.persist(se);
    em.getTransaction().commit();

        User se1 = new User("abc");

    violations = validator.validate(se1);

    System.out.println("Size:- " + violations.size());
}

Upvotes: 27

Ralph
Ralph

Reputation: 120811

I think it is not wise to use Hibernate Validator (JSR 303) for this purpose. Or better it is not the goal of Hibernate Validator.

The JSR 303 is about bean validation. This means to check if a field is set correct. But what you want is in a much wider scope than a single bean. It is somehow in a global scope (regarding all Beans of this type). -- I think you should let the database handle this problem. Set a unique constraint to the column in your database (for example by annotate the field with @Column(unique=true)) and the database will make sure that the field is unique.

Anyway, if you really want to use JSR303 for this, than you need to create your own Annotation and own Validator. The Validator have to access the Database and check if there is no other entity with the specified value. - But I believe there would be some problems to access the database from the Validator in the right session.

Upvotes: 8

Qwerky
Qwerky

Reputation: 18445

You could use the @Column attribute which can be set as unique.

Upvotes: 3

Related Questions