Written in the beginning

How often do you do curDS where you need to check if the field already exists in the database? When creating/renewing a member, check your name, phone number, email… Whether the database already exists. It was a simple requirement that took three minutes to write down.

Complete the needs of you complacently, see the two yuan touch fish, the leader threw a new demand to you, asked to check the members of the nickname, the second nickname, the third nickname… The 100th nickname is unique. You avenue grievance, “a hundred fields ah, have to write when”! Next to the glasses gently pushed the glasses, throw a lifeline to you –@Uni

Train of thought

No nonsense, let’s talk about ideas first. Implemented using custom annotation @UNI, Hibernate-Validator and dynamic Mosaic SQL based on Mybatis – Plus. Just annotate the @UNI annotation on the class to indicate the field being tested. Here is the implementation code.

Custom annotations

@Documented @Constraint( validatedBy = UniqueValidator.class ) @Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(Unique.List.class) public @interface Unique { // The name of the validation class String name(); String[] value(); String[] desc() default {}; Mapper Class<? Mapper Class<? extends BaseMapper> mapper() default BaseMapper.class; // the entity Class that reads the table attribute Class<? extends AbstractBasePo> clazz() default AbstractBasePo.class; String message() default "{desc}-- >{values} already exists "; Class<? >[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Documented @Target({ElementType.TYPE, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface List { Unique[] value(); }}Copy the code

Based on Hibernate Validator ConstraintValidator

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
    <version>${spring-boot-starter-validation}</version>
</dependency>
Copy the code

ConstraintValidator is a Hibernate field validator that inherits the initialize() and isValid() implementations to control the validation rules.

public abstract class AbstractConstraintValidator<A extends Annotation, T> implements ConstraintValidator<A, T>, ValidatorHandler<A, T> {/** ** protected A annotation; @Override public void initialize(A annotation) { this.annotation = annotation; this.validateParameters(); } @Override public boolean isValid(T t, ConstraintValidatorContext context) { if(Objects.isNull(t)) { return doValidNull(context); } doInitialize(annotation, t); return doValid(t, context); } /** * null validation * @author liuhr * @date 2021/6/513:44 * @param context * @return */ protected Boolean doValidNull(ConstraintValidatorContext context) { return true; @author liuhr * @date 2021/6/513:44 * @return */ protected abstract void validateParameters(); }Copy the code

Create a ValidatorHandler interface that allows the handler to detect fields

public interface ValidatorHandler<A extends Annotation, T> {

    boolean doInitialize(A annotation, T obj);

    boolean doValid(T obj, ConstraintValidatorContext context);
}
Copy the code

Create UniqueValidator AbstractConstraintValidator implementation class

@Slf4j public class UniqueValidator extends AbstractConstraintValidator<Unique, Object> { private ValidatorHandler validatorHandler; @override protected void validateParameters() {assert.notempty (annotation.value(), "validate field cannot be empty "); Assert.isTrue(ArrayUtils.isEmpty(annotation.desc()) || annotation.desc().length == annotation.value().length, "Desc and value length inconsistent "); } // ApplicationUtil is a utility class for obtaining beans. @override public Boolean doInitialize(Unique annotation, Object obj) { If (null == (validatorHandler = ApplicationUtil.getBean(Cacheservice.class). Get (CacheName.VALIDATOR, annotation.name(), ValidatorHandler.class))) { synchronized (annotation.clazz()) { if (null == (validatorHandler = ApplicationUtil.getBean(CacheService.class).get(CacheName.VALIDATOR, annotation.name(), Validatorhandler.class)) {log.info(" Initialize validator :[annotation: {}]", annotation.clazz()); / / UniqueValidatorHandlerFactory access to specific point handler, After the implementation, provide validatorHandler = UniqueValidatorHandlerFactory. GetHandler (obj. GetClass ()); validatorHandler.doInitialize(annotation, obj); ApplicationUtil.getBean(CacheService.class).put(CacheName.VALIDATOR, annotation.name(), validatorHandler); } } } return true; } @Override public boolean doValid(Object obj, ConstraintValidatorContext context) { return validatorHandler.doValid(obj, context); }}Copy the code

Actual execution EntityUniqueValidatorHandler detection logic

@Slf4j public class EntityUniqueValidatorHandler implements ValidatorHandler<Unique, Object> {// table primary key ID field private field keyField; // Table primary key ID columnName private String keyColumn; ColumnName --> field private Map<String, field > validColumnFieldMap; // mapper private BaseMapper mapper; @Override public boolean doInitialize(Unique unique, Object obj) { log.info("entity unique validator init......" ); Class<? > clazz = unique.clazz(); TableInfo tableInfo = SqlHelper.table(clazz); keyField = ReflectUtil.getField(clazz, tableInfo.getKeyProperty()); keyColumn = tableInfo.getKeyColumn(); mapper = MyBatisPlusUtil.getMapper(tableInfo.getConfiguration().getMapperRegistry(), unique.mapper(), clazz); Map<String, Field> fieldMap = ReflectUtil.getFieldMap(clazz); List<Field> fieldList = Arrays.stream(unique.value()).map(fieldMap::get).collect(Collectors.toList()); validColumnFieldMap = MyBatisPlusUtil.getColumnFieldMap(fieldList, tableInfo.getFieldList()); return true; } @Override public boolean doValid(Object obj, ConstraintValidatorContext context) { Object id = ReflectUtil.getFieldValue(obj, keyField.getName()); boolean isNotEmptyKey = ! StringUtils.isEmpty(id); for (Map.Entry<String, Field> entry : validColumnFieldMap.entrySet()) { Object value = ReflectUtil.getFieldValue(obj, entry.getValue().getName()); boolean isEmpty = StringUtils.isEmpty(value); // select * from 'SQL'; Return false QueryWrapper<Object> wrapper = Wrappers.query().ne(isNotEmptyKey, keyColumn, id) .isNotNull(isEmpty, entry.getKey()) .eq(! isEmpty, entry.getKey(), String.valueOf(value)); if (mapper.selectCount(wrapper) > 0) { context.unwrap(HibernateConstraintValidatorContext.class) .addMessageParameter(UniqueConstant.DESC, entry.getValue().getAnnotation(ApiModelProperty.class).value()) .addMessageParameter(UniqueConstant.VALUES, ObjectWrapper.getInstance(obj, entry.getValue())); return false; } } return true; }}Copy the code

The idea of doInitialize is to encapsulate the fields that need to be checked for faster performance in subsequent checks. For stateless beans, the initialization method should be executed only once, even if the same class is initialized only once on the first detection. ConstraintValidator is created every time a new ConstraintValidator object is created, resulting in the need to doInitialize every time a DCL handler is added to the cache. Every time a request comes in, the corresponding handler is retrieved, so you don’t need to enter the doInitialize method.

use

In the entity class, name is the key for cache, value is the field that needs to be checked, clazz is the entity, because I’m using DDD, I need this property. If you add annotations to the entity class, you don’t need this attribute. This way, in the future, even if you check the uniqueness of a hundred fields, you just need to add annotations.

@Unique(name = "account", value = {"username", "phoneNum"}, clazz = AccountPo.class)
Copy the code

Trigger check

ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); javax.validation.Validator validator = factory.getValidator(); Set<ConstraintViolation<Object>> ConstraintViolation = validator.validate(entity); if (! constraintViolations.isEmpty()) { ValidatorUtil.isTrue(true, constraintViolations.stream().findFirst().get().getMessage());Copy the code

Tool class supplement

@Component public class ApplicationUtil implements ApplicationContextAware { private static ApplicationContext applicationContext; public static ListableBeanFactory getApplicationContext() { return applicationContext; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public static <T> T getBean(String name) { return (T) applicationContext.getBean(name); } public static <T> T getBean(Class<T> clazz) { return applicationContext.getBean(clazz); } public static boolean containsBean(String name) { return applicationContext.containsBean(name); }}Copy the code
public class UniqueValidatorHandlerFactory { public static void assertValidClass(Class<? > clazz) {assert.isfalse (clazz.isenum (), "enum types are not supported "); Assert. IsFalse (CharSequence. Class. IsAssignableFrom (clazz), "do not support the character type"); Assert. IsFalse (ClassUtils isPrimitiveOrWrapper (clazz), "does not support basic types and packing type"); Assert. IsFalse (Map) class) isAssignableFrom (clazz), "temporarily does not support the Map type"); } public static ValidatorHandler<Unique, ? > getHandler(Class<? > clazz) { assertValidClass(clazz); return new EntityUniqueValidatorHandler(); }}Copy the code

Use effect

{" code ": 0," MSG ":" username - > [1001] exists ", "data" : null}Copy the code

Write in the back

Science is not good words, only wish you a happy New Year’s day.