Hibernate @Check, чтобы гарантировать единственное вхождение определенного значения в столбец - PullRequest
4 голосов
/ 14 июля 2020

Я реализую таблицу для хранения ролей пользователей со столбцами user_id и role. Бизнес-требование состоит в том, что должно быть ограничение, согласно которому должна существовать только одна запись со значением «ROLE_ ROOT» для столбца role. Нет ограничений на количество записей для любого другого значения в столбце role.

Например:

Допустимо:

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|

Приведенный ниже сценарий вообще не должен происходить при сохранении данных.

Сначала я подумал об использовании триггера в таблице для проверки этого ограничения перед любой вставкой, но меня спросили не реализовывать триггеры или какие-либо особенности базы данных c и использовать только Hibernate.

Это оставляет только (насколько мне известно) аннотацию @Check в Hibernate. Но я не могу определить устанавливаемое ограничение, поскольку проверки не могут иметь агрегатных функций. Есть ли способ использовать аннотацию Hibernate @Check для этого? Единственный другой способ - реализовать это вручную, но я хотел убедиться, что это может быть достигнуто на как можно более низком уровне, учитывая ограничения, с которыми мне приходится работать.

Ответы [ 2 ]

0 голосов
/ 14 июля 2020

@Check - это только способ установить SQL ограничение проверки для объекта:

Произвольные SQL ограничения CHECK, которые могут быть определены в классе , свойство или уровень коллекции.

То есть :

тип ограничения целостности в SQL, который определяет требование, которое должно быть выполнено каждой строкой в ​​таблице базы данных. Ограничение должно быть предикатом. Он может относиться к одному столбцу или нескольким столбцам таблицы.

И соответствующая часть:

Общие ограничения

Большинство системы управления базами данных ограничивают проверочные ограничения одной строкой с доступом к константам и детерминированным c функциям, но не к данным в других таблицах или к данным, невидимым для текущей транзакции из-за изоляции транзакции.

Таким образом, это не может быть применимо к вашему случаю, потому что ваша проверка строки зависит от содержимого других строк.

Я думаю, что вы попадаете в случаи бизнес-правил, где вы должны убедиться, что в пределах в ваших рабочих процессах это ограничение не нарушается. У вас есть 2 способа:

  • для обработки определенных c случаев, когда столбец role может быть обновлен
  • , чтобы иметь более комплексный подход, реализовав предварительное обновление JPA и PreInsert listeners.

Наконец, ничто не может помешать вашему коду приложения выполнить собственный запрос, даже с JPA. Но что ж, обзор кода, модульные тесты и интеграционные тесты также должны максимально заполнить пробел.

0 голосов
/ 14 июля 2020

@ Check не поможет вам в этом случае, так как он не предназначен для запроса базы данных как части проверки перед сохранением объекта. Как и другие аннотации валидатора спящего режима, он предназначен для базовой c проверки ограничений.

Таким образом, единственный способ - выполнить проверку БД либо перед сохранением, либо как часть настраиваемой аннотации . Однако, поскольку это будет дорого проверять БД каждый раз, вы можете избежать этого, имея кеш в памяти (если развернут только 1 экземпляр вашего приложения) или распределенный кеш (если несколько экземпляров) .

Пример использования пользовательских аннотаций и кеша в памяти:

(Версия Spring Boot: 2.3.1.RELEASE, валидатор Hibernate: 5.2.4.Final) ( Если вы хотите использовать кеш в памяти, не забудьте сделать его недействительным, когда будет удалена запись с ролью ROLE_ ROOT )

  1. Класс сущности - (идентификатор автоинкремента)
@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 {};
}
Валидатор ограничений Impl (Используемый кэш кофеина в памяти) -
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
  }
}

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...