Проверка уникального ключа гибернации
У меня есть поле, скажем, user_name
, который должен быть уникальным в таблице.
Каков наилучший способ его проверки с помощью валидации Spring/Hibernate?
7 ответов
Одним из возможных решений является создание пользовательских @UniqueKey
ограничение (и соответствующий валидатор); и искать существующие записи в базе данных, предоставить экземпляр EntityManager
(или Hibernate Session
) в 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;
}
}
использование
@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;
}
....
}
Тестовое задание
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());
}
Я думаю, что не стоит использовать Hibernate Validator (JSR 303) для этой цели. Или лучше это не цель Hibernate Validator.
JSR 303 о проверке компонентов. Это значит проверить, правильно ли установлено поле. Но то, что вы хотите, находится в гораздо более широкой области, чем один компонент. Это как-то в глобальном масштабе (относительно всех Бобов этого типа). - Я думаю, вы должны позволить базе данных справиться с этой проблемой. Установите уникальное ограничение для столбца в вашей базе данных (например, аннотируйте поле с помощью @Column(unique=true)
) и база данных убедится, что поле уникально.
В любом случае, если вы действительно хотите использовать для этого JSR303, вам нужно создать собственную аннотацию и собственный валидатор. Валидатор должен получить доступ к базе данных и проверить, нет ли другого объекта с указанным значением. - Но я полагаю, что возникнут некоторые проблемы с доступом к базе данных из валидатора на нужном сеансе.
Вы могли бы использовать @Column
атрибут, который может быть установлен как unique
,
Я нашел какое-то хитрое решение.
Во-первых, я реализовал уникальное противопоставление моей базе данных MySql:
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)
) ;
Вы видите, что я управляю тегами XML, которые определены уникальной меткой и текстовым полем с именем "XPath".
В любом случае, второй шаг - просто перехватить ошибку, возникшую, когда пользователь пытается выполнить плохое обновление. Плохое обновление - при попытке заменить текущую метку существующей меткой. Если вы оставите этикетку нетронутой, нет проблем. Итак, в моем контроллере:
@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
}
}
}
Это может конфликтовать с шаблоном @Unique, но вы также можете использовать этот грязный метод для проверки добавления.
Примечание: есть еще одна проблема: если перед исключением возникнут другие ошибки проверки, сообщение об уникальности не будет отображаться.
Этот код основан на предыдущем, реализованном с использованием EntityManager
, В случае, если кому-то нужно использовать Hibernate Session
, Пользовательские аннотации с использованием 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;
}
}
использование
@UniqueKey(columnNames="userName", className = UserEntity.class)
// @UniqueKey(columnNames="userName") // unique key
С моей точки зрения, в представленных здесь решениях отсутствует очень важный случай — обновление. Мы должны учитывать первичный ключ при запросе наших API JPA для persist или megre, поэтому вы ДОЛЖНЫ исключить текущий объект из проверки уникальности (с помощью первичного ключа).
В приведенной ниже демонстрации используется Spring Framework.
Аннотация:
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();
}
}
Реализация валидатора аннотаций:
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;
}
}
если вы хотите использовать @Unique более одного раза для одного и того же объекта:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Uniques {
Unique[] value();
}
Объект JPA:
@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;
}