Java Hibernate@Check以确保列中只出现一个特定值

Java Hibernate@Check以确保列中只出现一个特定值,java,hibernate,orm,Java,Hibernate,Orm,我正在实现一个用于存储具有列user\u id和role的用户角色的表。业务需求是,应该有一个约束,即对于列ROLE,只能存在一条值为“ROLE\u ROOT”的记录。角色列中任何其他值的记录数没有限制 例如: 有效期: role |user_id| -------------|-------| ROLE_ROOT | 3| ROLE_CUSTOMER| 5| ROLE_CUSTOMER| 9| 无效: role |user_

我正在实现一个用于存储具有列
user\u id
role
的用户角色的表。业务需求是,应该有一个约束,即对于列
ROLE
,只能存在一条值为“ROLE\u ROOT”的记录。
角色
列中任何其他值的记录数没有限制

例如:

有效期:

role         |user_id|
-------------|-------|
ROLE_ROOT    |      3|
ROLE_CUSTOMER|      5|
ROLE_CUSTOMER|      9|
无效:

role         |user_id|
-------------|-------|
ROLE_ROOT    |      3|
ROLE_ROOT    |      4|
ROLE_CUSTOMER|      5|
ROLE_CUSTOMER|      9|
在持久化数据时,绝对不能出现以下情况

我最初考虑过在任何插入之前使用表上的触发器来检查此约束,但有人要求我不要实现触发器或任何特定于数据库的功能,而只使用Hibernate


这只剩下(据我所知)Hibernate中的@Check注释。但我无法确定要设置的约束,因为检查不能有聚合函数。有没有办法使用Hibernate@Check注释来实现这一点?唯一的另一种方法是手动实现,但考虑到我必须处理的约束条件,我想确保这可以在尽可能低的级别上实现。

@Check在这种情况下对您没有帮助,因为它并不意味着在持久化实体之前查询数据库作为验证的一部分。与其他hibernate验证器注释一样,它用于基本约束检查

因此,唯一的方法是在持久化之前或作为自定义注释的一部分执行DB检查。但是,由于每次检查DB的代价很高,因此可以通过使用内存缓存(如果只部署了应用程序的一个实例)或分布式缓存(如果有多个实例)来避免这种情况

使用自定义注释和内存缓存的示例:

(Spring启动版本:2.3.1.RELEASE,Hibernate验证程序:5.2.4.Final) (如果您试图使用内存缓存,请记住在删除具有role_ROOT的记录时使其无效)

  • 实体类-(自动增量id)
  • 存储库-
  • 是对实体设置SQL检查约束的唯一方法:

    可以在类中定义的任意SQL检查约束, 属性或集合级别

    :

    SQL中的一种完整性约束,用于指定要求 数据库表中的每一行都必须满足这一要求 约束必须是谓词。它可以指单个列, 或表的多个列

    及有关部分:

    共同限制

    大多数数据库管理系统将检查约束限制为 单行,可访问常量和确定性函数,但 不是其他表中的数据,也不是当前用户不可见的数据 事务隔离导致的事务

    因此,它很可能不适用于您的案例,因为您对某一行的检查依赖于其他行的内容

    我认为您属于业务规则情形,您应该确保自己在工作流中不违反该约束。
    你有两种方法:

    • 处理可能更新
      角色
      列的特定情况
    • 通过实现JPA预更新和预插入监听器,实现更跨领域的方法

    最后,没有什么可以阻止应用程序代码执行本机查询,即使使用JPA也是如此。但是,代码审查、单元测试和集成测试也尽可能地填补了这一空白。

    您是否可以在角色列中使用
    @Column(unique=true)
    ?或等效的
    @UniqueConstraint
    ?另一个选项是使用
    @Id
    注释将角色设置为表的主键。@IasonFilippopoulos他的角色不是自然Id。它可以有多个值作为role\u CUSTOMER,但只能有一个role\u ROOT可选解决方案:检查表中是否存在role\u ROOT,如果不保存记录。@Desmond27请检查答案。希望它能帮助你。仅仅为一张记录添加一张支票似乎要复杂得多。但让我看看是否值得。@Desmond27同意你的看法。正如我所提到的,另一种方法也是最简单的方法是在将任何数据持久化到角色表之前执行DB检查。好吧,我还没有尝试过,因为我必须快速交付。我决定添加一个条件,在插入之前显式检查代码中的角色名称。稍后我会在自己的时间尝试这个。但是谢谢你的解决办法。@Desmond27好的。如果您接受答案并结束此问题,我将不胜感激。好的,我理解。在这种情况下,在插入之前实现条件似乎是最简单的方法。
    @Entity
    @Table(name = "my_table")
    @CheckRole
    public class MyEntity {
    
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Column(name = "id")
      private int id;
    
      @Column(name = "role")
      private String role;
    
      @Column(name = "user_id")
      private int userId;
    
      public int getId() {
        return id;
      }
    
      public String getRole() {
        return role;
      }
    
      public void setRole(String role) {
        this.role = role;
      }
    
      public int getUserId() {
        return userId;
      }
    
      public void setUserId(int userId) {
        this.userId = userId;
      }
    
    }
    
    @Repository
    public interface MyTableRepository extends JpaRepository<MyEntity, Integer> {
    
      @Transactional(propagation = Propagation.NOT_SUPPORTED)
      @Query(value = "SELECT CASE WHEN COUNT(e) > 0 THEN true ELSE false END FROM MyEntity e WHERE e.role = :roleName")
      Boolean checkIfRoleExists(@Param("roleName") String roleName);
    }
    
    
    @Target(TYPE)
    @Retention(RUNTIME)
    @Constraint(validatedBy = RoleValidator.class)
    public @interface CheckRole {
    
      String message() default "Cannot have duplicate entry for Role: ROLE_ROOT";
    
      Class<?>[] groups() default {};
    
      Class<? extends Payload>[] payload() default {};
    }
    
    public class RoleValidator implements ConstraintValidator<CheckRole, MyEntity > {
    
      private static final String ROLE_TO_VALIDATE = "ROLE_ROOT";
      private LoadingCache<String, Boolean> myCache;
    
      public RoleValidator(MyTableRepository repository) {
    
        myCache = Caffeine.newBuilder()
            .maximumSize(1)
            .expireAfterWrite(5, TimeUnit.MINUTES)
            .refreshAfterWrite(1, TimeUnit.MINUTES)
            .build(repository::checkIfRoleExists);
    
      }
    
      @Override
      public void initialize(CheckRole constraintAnnotation) {
      }
    
      @Override
      public boolean isValid(MyEntity entity, ConstraintValidatorContext context) {
        String roleValue = entity.getRole();
        if (roleValue.equals(ROLE_TO_VALIDATE)) {
          boolean isValid = !myCache.get(ROLE_TO_VALIDATE);
          if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate("Cannot have duplicate entry for Role: " + ROLE_TO_VALIDATE)
                .addConstraintViolation();
          }
          return isValid;
        } else {
          return true;
        }
      }
    
    }
    
    @Component
    public class ValidatorAddingCustomizer implements HibernatePropertiesCustomizer {
    
      private final ObjectProvider<Validator> provider;
    
      @Autowired
      public ValidatorAddingCustomizer(ObjectProvider<Validator> provider) {
        this.provider = provider;
      }
    
      @Override
      public void customize(Map<String, Object> hibernateProperties) {
        Validator validator = provider.getIfUnique();
        if (validator != null) {
          hibernateProperties.put("javax.persistence.validation.factory", validator);
        }
      }
    }
    
    @RestController
    public class MyController {
    
      @Autowired
      private MyTableRepository repository;
    
      @GetMapping("/hello")
      public void hello() {
        MyEntity myEntity = new MyEntity();
        myEntity.setRole("ROLE_ROOT");
        myEntity.setUserId(3);
    
        repository.save(myEntity); //saves successfully
    
        MyEntity myEntity2 = new MyEntity();
        myEntity2.setRole("ROLE_ROOT");
        myEntity2.setUserId(4);
    
        repository.save(myEntity2); //Throws Constraint Violation Exception
      }
    }