@Check wont help you in this case, since it's not meant to query the database as part of validation before persisting the entity. Like other hibernate validator annotations, it is meant for basic constraint checking.
Thus only way is to perform the DB check either before persisting or as part of a Custom annotation. However, since this will be expensive to check DB everytime, you can avoid it by having an in-memory cache (if there is only 1 instance of your application deployed) or a distributed cache (if multiple instances).
Example using custom annotation and in-memory cache:
(Spring Boot Version: 2.3.1.RELEASE, Hibernate validator: 5.2.4.Final)
(If you seek to use in-memory cache, remember to invalidate it, when record with role ROLE_ROOT is deleted)
- Entity class - (auto increment id)
@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 -
@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);
}
- Annotation-
@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 {};
}
- Constraint Validator Impl (Used In-memory Caffeine cache) -
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;
}
}
}
- Hibernate properties customizer -
@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);
}
}
}
- Validation in action -
@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
}
}