【问题标题】:hibernate unique key validation休眠唯一密钥验证
【发布时间】:2011-06-04 12:36:28
【问题描述】:

我有一个字段,例如 user_name,它在表中应该是唯一的。

使用 Spring/Hibernate 验证对其进行验证的最佳方式是什么?

【问题讨论】:

    标签: java hibernate spring bean-validation hibernate-validator


    【解决方案1】:

    一种可能的解决方案是创建自定义@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;
        }
    }
    

    唯一键

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

    【讨论】:

    • 我怎样才能将相同的代码与 Sessionfactory 而不是 EntityManager 一起使用?
    • 谢谢。将criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()])); 更改为criteriaQuery.select(root).where(predicates.toArray(new Predicate[predicates.size()]));。现在它对我有用。使用Spring,我去掉了EntityManagerAwareValidator & ConstraintValidatorFactoryImpl,直接在UniqueKeyValidator中添加了@PersistenceContext private EntityManager entityManager
    • 此代码如何用于更新用例?假设您提交的更新请求包含修改过的字段而不是具有唯一键约束的字段?这不会引发假阴性吗?
    【解决方案2】:

    我认为为此目的使用 Hibernate Validator (JSR 303) 是不明智的。 或者更好的是,这不是 Hibernate Validator 的目标。

    JSR 303 是关于 bean 验证的。这意味着检查字段是否设置正确。但是你想要的是比单个 bean 更广泛的范围。它以某种方式在全局范围内(关于这种类型的所有 Bean)。 -- 我认为你应该让数据库来处理这个问题。为数据库中的列设置唯一约束(例如通过使用@Column(unique=true) 注释字段),数据库将确保该字段是唯一的。

    无论如何,如果您真的想为此使用 JSR303,那么您需要创建自己的 Annotation 和自己的 Validator。验证器必须访问数据库并检查是否没有具有指定值的其他实体。 - 但我相信在正确的会话中从验证器访问数据库会有一些问题。

    【讨论】:

    • 谢谢,如果@Column(unique=true) 我如何在视图中显示错误
    • @Column(unique=true) 将为相应的列创建一个数据库约束。 -- 这意味着如果您尝试存储具有非唯一值的实体,您得到的只是一个例外。 -- 所以没有(简单的)方法可以将它与 Bean 验证结合起来。
    • 当然,数据库需要一个唯一的约束来处理并发插入。但我认为你不应该让数据库单独处理它。之前验证唯一性可以更容易地向用户显示错误消息。因此,在我看来,预先进行验证并测试唯一性是一种完全有效的方法。通常,一旦用户在其 Web 浏览器中离开用户名字段,您就会希望“用户名不可用”。因此,执行 ajax 请求并尝试使用 jsr-303 进行验证是可行的方法。这就是为什么我对你的答案投了反对票(对此感到抱歉)
    • @Jannig:你改变了问题的范围,从来没有人谈论过 Ajax 场景。
    • Ajax 只是一个例子。如果您有业务规则,则应对其进行验证。想象一下没有 ajax 的相同示例。您提供用户名和电子邮件地址。电子邮件地址无效,因此您会收到错误页面。但是由于您没有尝试更新数据库,因此您无法根据用户选择的用户名向用户显示适当的消息。用户更正电子邮件地址,提交并收到下一个错误,谈论用户名的唯一性。
    【解决方案3】:

    一种可能性是将字段注释为@NaturalId

    【讨论】:

    • 我投了赞成票,因为您明确表示这是“一种可能的”解决方案。
    【解决方案4】:

    您可以使用@Column 属性,该属性可以设置为unique

    【讨论】:

      【解决方案5】:

      我找到了一种棘手的解决方案。

      首先,我实现了 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)
      ) ;
      

      您会看到我管理由唯一标签和名为“XPath”的文本字段定义的 XML 标记。

      无论如何,第二步是简单地捕获当用户尝试进行错误更新时引发的错误。错误更新是尝试用现有标签替换当前标签。如果你不碰标签,没问题。所以,在我的控制器中:

          @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 模式冲突,但您也可以使用这种脏方法来验证添加。

      注意:还有一个问题:如果在异常之前捕获了其他验证错误,则不会显示关于 unicity 的消息。

      【讨论】:

        【解决方案6】:

        此代码基于之前使用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
        

        【讨论】:

        • 请考虑添加说明,说明您的代码如何是最佳方法。
        • 代码是基于上一个使用EnintyManager的。一个人要求 Hibernate Session。抱歉没提。谢谢。
        • 请注意,此实现不支持在单元测试设置中测试验证器。在单元测试设置中,验证器不是使用 Spring bean 创建机制创建的,因此应该手动将 SessionFactory 注入验证器,可能使用自定义的 ConstraintValidatorFactory 作为接受的答案。
        • 为什么是 @Repository 注释?
        猜你喜欢
        • 2015-05-19
        • 2013-11-23
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-09-18
        • 1970-01-01
        • 2012-11-19
        相关资源
        最近更新 更多