hibernate unique key validation

28,766

Solution 1

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

Solution 2

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.

Solution 3

One possibility is to annotate the field as @NaturalId

Solution 4

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

Solution 5

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
        }
        else {
            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.

Share:
28,766
nidhin
Author by

nidhin

A bug

Updated on July 09, 2022

Comments

  • nidhin
    nidhin almost 2 years

    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?

  • nidhin
    nidhin over 13 years
    Thanks, in case @Column(unique=true) how can i show error in view
  • Ralph
    Ralph over 13 years
    @Column(unique=true) will create a data base constraint to the according column. -- This means all you get is an exception if you try to store the entity with the not unique value. -- So there is no (easy) way to combine it with the Bean validation.
  • Janning
    Janning over 11 years
    Of course, the database needs a unique constraint to handle concurrently inserts. But I don't think you should let the database handle it alone. Validating the uniqueness before makes it much easier to show error messages to the user. So in my opinion it is a perfectly valid approach to do the validation beforehand and test for uniqueness. Usually you want to have "username not available" as soon as the user left the username field in his web browser. So doing an ajax request and trying to validate with jsr-303 is the way to go. That's why I down voted your answer (sorry for this)
  • Ralph
    Ralph over 11 years
    One upvote from me, because you clearly stated that this is "one possible" solution.
  • Ralph
    Ralph over 11 years
    @Jannig: You change the scope of the question, there was no one ever talking about the Ajax scenario.
  • Janning
    Janning over 11 years
    Ajax was just an example. If you have a business rule you should validate it. Imagine the same example without ajax. You give a username and email address. The email address is not valid, so you get an error page. But as you not have tried to update the database you can't show the user an appropriate message according its choosen username. the user corrects the email adresse, submits and is getting the next error, talking about uniqueness of the username.
  • Janning
    Janning over 11 years
    From an architectural point of view, check the uniqueness at the same place where you validate all the other stuff.
  • FuSsA
    FuSsA about 8 years
    how can i use the same code with Sessionfactory instead of EntityManager ?
  • Benjamin Lowry
    Benjamin Lowry over 7 years
    Please consider adding an explanation as to how your code is the best method.
  • Tanmoy Mandal
    Tanmoy Mandal over 7 years
    The code is based on the previous one using EnintyManager. One person asked for the Hibernate Session. Sorry for not mentioning. And thank you.
  • infiniteRefactor
    infiniteRefactor over 7 years
    Note that this implementation does not support testing the validator in a unit test setting. In unit test settings validators are not created with Spring bean creation mechanism, thus SessionFactory should be manually injected into the validator, possibly using a custom ConstraintValidatorFactory as accepted answer does.
  • James
    James over 6 years
    Thanks. Changed criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()])); to criteriaQuery.select(root).where(predicates.toArray(new Predicate[predicates.size()]));. Now it works for me. With Spring use, I got rid of EntityManagerAwareValidator & ConstraintValidatorFactoryImpl and added @PersistenceContext private EntityManager entityManager directly in the UniqueKeyValidator.
  • Xegara
    Xegara about 5 years
    How will this code work for update use cases? Let's say when you submit an update request with modified fields other than the field with the unique key constraint? Wouldn't that throw a false negative?
  • mneri
    mneri over 4 years
    Why the @Repository annotation?