Custom verification-database field uniqueness verification

Scenario: It is often necessary to verify whether a field in a table is unique. For example, when naming in Honor of Kings, it always prompts that the name already exists. This is a uniqueness check.

condition:

Springboot has provided a lot of checks, such as @NotNull, @NotEmpty, etc. These are actually JSR303 specifications.

So, what if custom annotations meet my needs?

The content of this blog post relies on mybatis-plus and reflection tool classes.

1. Already have resources

1. Table act_cx_category

CREATE TABLE `act_cx_category` (
  `category_id` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci NOT NULL COMMENT 'category id',
  `category_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'category name',
  `parent_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'parent id',
  `icon` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8_general_ci DEFAULT NULL COMMENT 'icon',
  `sort` double DEFAULT NULL COMMENT 'sort',
  `app_id` varchar(255) DEFAULT NULL COMMENT 'application id',
  PRIMARY KEY (`category_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC COMMENT='Classification Table';

2. Corresponding entity class

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;

/**
 * Classification table
 * @TableName act_cx_category
 */
@TableName(value ="act_cx_category")
@Data
public class ActCxCategoryEntity implements Serializable {

    /**
     * Category id
     */
    @TableId(type= IdType.ASSIGN_ID)
    private String categoryId;

    /**
     * Category Name
     */
    private String categoryName;

    /**
     * parent id
     */
    private String parentId;

    /**
     * icon
     */
    private String icon;

    /**
     * Sort
     */
    private Double sort;

    /**
     * application id
     */
    private String appId;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

3. Corresponding mapper

import com.jxctjt.workflow.domain.entity.ActCxCategoryEntity;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
* @description Database operation Mapper for table [act_cx_category (category table)]
* @Entity com.jxctjt.workflow.domain.entity.ActCxCategoryEntity
*/
public interface ActCxCategoryMapper extends BaseMapper<ActCxCategoryEntity> {

}

4. Controller layer

 @PostMapping("/test")
  public Result test(@Validated(AddGroup.class) @RequestBody CategoryBo bo){
    log.info(bo.toString());
    // logic
    return Result.ok("Test successful!");
  }

5. Bo objects that need to be verified

import com.jxctjt.workflow.common.validate.group.AddGroup;
import com.jxctjt.workflow.common.validate.group.EditGroup;
import com.jxctjt.workflow.common.validate.group.anno.UniqueExtendAppIdImpl;
import com.jxctjt.workflow.common.validate.group.anno.UniqueField;
import com.jxctjt.workflow.common.validate.group.anno.UniqueType;
import com.jxctjt.workflow.domain.entity.ActCxCategoryEntity;
import com.jxctjt.workflow.mapper.ActCxCategoryMapper;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import javax.validation.constraints.NotNull;
import lombok.Data;

/**
 * @author xgz
 */
@Data
@ApiModel("Classification parameter object")
public class CategoryBo implements Serializable {

  private static final long serialVersionUID = 1L;

  @ApiModelProperty("category id")
  @NotNull(message = "Category id cannot be empty", groups = {EditGroup.class})
  private String categoryId;

  /**
   * Category Name
   */
  @ApiModelProperty("category name")
  @NotNull(message = "Category name cannot be empty", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

  /**
   * icon
   */
  @ApiModelProperty("icon")
  private String icon;

  /**
   * Sort
   */
  @ApiModelProperty("sort")
  @NotNull(message = "Sort cannot be empty", groups = {AddGroup.class, EditGroup.class})
  private Double sort;
}

2. Implementation

1. UniqueType annotation

Specify this annotation to use UniqueValidator for verification through @Constraint(validatedBy = {UniqueValidator.class}). 

This annotation can only be annotated on the class. When the @Validated annotation is annotated on the request entity, it will enter

Inside UniqueValidator.

import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;

/**
 * Verification parameters are unique
 * Need to be used in conjunction with UniqueField annotation
 * Scenario: The user name is required to be unique, but the database user name does not have a unique key set.
 * @author xgz
 */
@Documented
@Constraint(validatedBy = {UniqueValidator.class})
@Target({TYPE})
@Retention(RUNTIME)
public @interface UniqueType {
  Class<?> entity();
  //Services that access the database
  Class<? extends BaseMapper> mapper();
  /**
   * Whether to enable verification
   *
   * @return boolean value of whether to force verification
   */
  boolean required() default true;
  String message() default "{database already exists}";

  Class<?>[] groups() default {};

  Class<? extends Payload>[] payload() default {};
  /** The default value must not be changed **/
  Class<? extends UniqueExtend> extend() default UniqueExtend.class;
}

2. UniqueField annotation

This annotation can only be marked on fields.

The UniqueType annotation must be paired with the UniqueField annotation to take effect. The reason is UniqueValidator’s isValid

It is determined by the implementation logic of the method.

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

/**
 * Database unique verification
 * Scenario: For example, the user name is required to be unique, but the database user name does not have a unique key set.
 * @author xgz
 */
@Documented
@Target({FIELD})
@Retention(RUNTIME)
public @interface UniqueField {

  /**
   * Whether to enable verification
   *
   * @return boolean value of whether to force verification
   */
  boolean required() default true;
  String message();

  /**
   * Extended classes such as SQL also need to be spliced into the appId field for query
   * The default value of extend must not be changed
   * **/
  Class<? extends UniqueExtend> extend() default UniqueExtend.class;
}

3. UniqueValidator verification class

When a parameter class marked with @UniqueType annotation is executed for verification, the initialize method will be executed first, which means initialization, and the parameters of @UniqueType can be obtained.

The isValid method will then be executed. The isValid method returns true to indicate that the verification is passed and the application is released. Returning false indicates failure and an exception will be thrown. The default message is the parameter message of the @UniqueType annotation.

import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jxctjt.workflow.common.constant.SepConstant;
import com.jxctjt.workflow.common.exception.SysException;
import com.jxctjt.workflow.common.util.ReflectUtils;
import java.lang.reflect.Field;
import java.util.Objects;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.Nullable;

/**
 * @author xgz
 */
@Slf4j
public class UniqueValidator implements ConstraintValidator<UniqueType, Object> {

  /**
   * Whether to force verification
   */
  private boolean required;

  /**
   * Service class for operating database
   **/
  private Class<? extends BaseMapper> mapper;

  private Class entity;
  private UniqueExtend extend;
  /**
   * Primary key name in entity class
   * <p>
   */
  private String primaryKeyName;

  /**
   * Primary key value in entity class
   */
  private Object primaryKeyValue;

  @Override
  public void initialize(UniqueType anno) {
    this.required = anno.required();
    this.mapper = anno.mapper();
    if (null == mapper) {
      throw new SysException("service cannot be empty");
    }
    this.entity = anno.entity();
    Class<? extends UniqueExtend> defaultVal = anno.extend();
    this.extend = getUniqueExtend(defaultVal);
  }

  /**
   * Returning true indicates that the verification is successful. Returning false indicates that the verification fails and an exception will be thrown.
   **/
  @Override
  public boolean isValid(Object obj, ConstraintValidatorContext context) {
    if (required) {
      BaseMapper bean = SpringUtil.getBean(mapper);
      if (null == bean) {
        throw new SysException("The mapper of the Unique annotation must be a Bean");
      }
      return doValid(bean, obj, context);
    }
    return true;
  }

  private boolean doValid(BaseMapper mapper, Object obj, ConstraintValidatorContext context) {
    // Try to get the primary key name of the table and the passed primary key value. Determine whether to add or update by whether there is a primary key value.
    getPrimaryNameAndValue(obj);
  
    Field[] fields = ReflectUtils.getFields(obj.getClass());
    for (Field field : fields) {
      if (field.isAnnotationPresent(UniqueField.class)) {
        UniqueField anno = field.getAnnotation(UniqueField.class);
        String message = anno.message();
        String name = field.getName();
        Class<? extends UniqueExtend> filedExtend = anno.extend();
        UniqueExtend uniqueExtendObj = getUniqueExtend( filedExtend);
        String dbField = toDbField(name);
        Object value = ReflectUtils.invokeGetter(obj, name);
        if (null == value){
          continue;
        }
        Object entity = ReflectUtils.newInstance(this.entity);
        AbstractWrapper wrapper = new QueryWrapper(entity).eq(true, dbField, value)
            .ne(Objects.nonNull(primaryKeyValue), toDbField(primaryKeyName), primaryKeyValue);
        //Execute expansion
        try {
          if (uniqueExtendObj == null){
            if (this.extend != null){
              wrapper = this.extend.apply(wrapper, entity, obj, field);
            }
          }else{
            wrapper = uniqueExtendObj.apply(wrapper, entity, obj, field);
          }
        }catch (Exception e){
          log.error("An error occurred while executing the expansion!");
          wrapper = new QueryWrapper(entity).eq(true, dbField, value)
              .ne(Objects.nonNull(primaryKeyValue), toDbField(primaryKeyName), primaryKeyValue);
        }
        int i = mapper.selectCount(wrapper).intValue();
        if (i > 0){
          log.error(message);
          //Disable the default message value. If not disabled, it will be spliced based on the original default message.
          context.disableDefaultConstraintViolation();
          context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Get the extended class object
   * Get it first in the container
   * **/
  @Nullable
  private static UniqueExtend
                getUniqueExtend(Class<? extends UniqueExtend> extendClass){

    UniqueExtend uniqueExtendObj = null;
   if (!UniqueExtend.class.equals(extendClass)) {
          uniqueExtendObj = SpringUtil.getBean(extendClass);
          if (null == uniqueExtendObj){
            uniqueExtendObj = ReflectUtil.newInstance(extendClass);
          }
    }
    return uniqueExtendObj;
  }

  private void getPrimaryNameAndValue(Object obj) {
    Field[] fields = ReflectUtils.getFields(entity);
    for (Field field : fields) {
      if (field.isAnnotationPresent(TableId.class)) {
        this.primaryKeyName = field.getName();
        primaryKeyValue = ReflectUtils.invokeGetter(obj, this.primaryKeyName);
        break;
      }
    }
  }

  private String toDbField(String name) {
    if (CharSequenceUtil.isBlank(name)) {
      throw new SysException("The field name to be verified cannot be empty");
    } else {
      if (name.contains(SepConstant.UNDERLINE)) {
        return name;
      } else {
        return CharSequenceUtil.toUnderlineCase(name);
      }
    }
  }
}

4. Expansion interface

UniqueExtend


import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import java.lang.reflect.Field;

public interface UniqueExtend {


  /**
   * Application expansion and modification of query SQL
   * @author xgz
   * @date 2023/10/28
   * @param wrapper default wrapper
   * @param The entity object corresponding to the entity table. Note that this is an empty object.
   * However, the field names and other information of the table can be obtained through reflection.
   * @param obj Bo object, with values passed in by each front end
   * @param field The field that currently needs to be verified is the field that the Bo object is annotated with @UniqueField.
   * @return com.baomidou.mybatisplus.core.conditions.Wrapper
  **/
  AbstractWrapper apply(AbstractWrapper wrapper, Object entity, Object obj, Field field);
}

5. An implementation class to extend the interface

import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.jxctjt.workflow.common.util.ReflectUtils;
import java.lang.reflect.Field;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * Expand and add appId conditional judgment
 * **/
@Slf4j
@Component
public class UniqueExtendAppIdImpl implements UniqueExtend{

  @Resource
  private HttpServletRequest request;
  private final String THIRD_PART_NAME_HEADER = "app-id";
  /**
   * Application expansion and modification of query SQL
   *
   * @param wrapper default wrapper
   * @param The entity object corresponding to the entity table. Note that this is an empty object. However, you can obtain table field names and other information through reflection.
   * @param obj Bo object, with values passed in by each front end
   * @param field The field that currently needs to be verified is the field that the Bo object is annotated with @UniqueField.
   * @return com.baomidou.mybatisplus.core.conditions.Wrapper
   * @author xgz
   * @date 2023/10/28
   **/
  @Override
  public AbstractWrapper apply(AbstractWrapper wrapper, Object entity, Object obj, Field field) {
    // First get the appId from obj, if not, then get the appId from the header of the request
    Field appIdField = ReflectUtils.getField(obj.getClass(), "appId");
    Object appId = null;
    if (appIdField != null){
      appId = ReflectUtils.invokeGetter(obj, appIdField.getName());
    }
    if (null == appId){
      String header = request.getHeader(THIRD_PART_NAME_HEADER);
      if (CharSequenceUtil.isNotBlank(header)){
        appId = header;
      }
    }
    if (appId == null){
      log.info("No appId!");
      return wrapper;
    }else {
      return wrapper.eq(true, "app_id", appId);
    }
  }
}

3. Use

1. Controller layer

 @PostMapping("/test")
  public Result test(@Validated(AddGroup.class) @RequestBody CategoryBo bo){
    log.info(bo.toString());
    // logic
    return Result.ok("Test successful!");
  }

2. Bo objects that need to be verified

import com.jxctjt.workflow.common.validate.group.AddGroup;
import com.jxctjt.workflow.common.validate.group.EditGroup;
import com.jxctjt.workflow.common.validate.group.anno.UniqueExtendAppIdImpl;
import com.jxctjt.workflow.common.validate.group.anno.UniqueField;
import com.jxctjt.workflow.common.validate.group.anno.UniqueType;
import com.jxctjt.workflow.domain.entity.ActCxCategoryEntity;
import com.jxctjt.workflow.mapper.ActCxCategoryMapper;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import javax.validation.constraints.NotNull;
import lombok.Data;

/**
 * @author xgz
 */
@Data
@UniqueType(entity = ActCxCategoryEntity.class, mapper = ActCxCategoryMapper.class, groups = {
    AddGroup.class, EditGroup.class})
@ApiModel("Classification parameter object")
public class CategoryBo implements Serializable {

  private static final long serialVersionUID = 1L;

  @ApiModelProperty("category id")
  @NotNull(message = "Category id cannot be empty", groups = {EditGroup.class})
  private String categoryId;

  /**
   * Category Name
   */
  @UniqueField(message = "Category name already exists")
 // @UniqueField(message = "Category name already exists", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("category name")
  @NotNull(message = "Category name cannot be empty", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

  /**
   * icon
   */
  @ApiModelProperty("icon")
  private String icon;

  /**
   * Sort
   */
  @ApiModelProperty("sort")
  @NotNull(message = "Sort cannot be empty", groups = {AddGroup.class, EditGroup.class})
  private Double sort;
}

Note 1:

@Data
@UniqueType(entity = ActCxCategoryEntity.class, mapper = ActCxCategoryMapper.class, groups = {
    AddGroup.class, EditGroup.class})
@ApiModel("Classification parameter object")
public class CategoryBo implements Serializable {omitted}

Note 2:

 /**
   * Category Name
   */
  @UniqueField(message = "Category name already exists")
 // @UniqueField(message = "Category name already exists", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("category name")
  @NotNull(message = "Category name cannot be empty", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

This is to verify that the categoryName received by CategoryBo cannot be repeated in the database.

3. Test

Test Data:

CASE 1: Update scenario parameter has table primary key

parameters:
  {
    "categoryId": "1f03b4e8bd920c6991cdc48d32b96cb40",
    "categoryName": "Project Management System",
    "sort": 0
  }
  Request header: app-id: 1691701502841065473
  result:
  {
    "code": 500,
    "message": "......; default message [category name already exists]] ",
    "result": null,
    "success": false
  }

 
1. When Bo is written like this, no expansion is used
 /**
   * Category Name
   */
  @UniqueField(message = "Category name already exists")
 // @UniqueField(message = "Category name already exists", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("category name")
  @NotNull(message = "Category name cannot be empty", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;
Sql executed in the background:
 SELECT COUNT( * ) FROM act_cx_category
WHERE
(category_name = 'Project Management System'
    AND category_id <> '1f03b4e8bd920c6991cdc48d32b96cb40';
2. Usage expansion
 /**
   * Category Name
   */
  //@UniqueField(message = "Category name already exists")
  @UniqueField(message = "Category name already exists", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("category name")
  @NotNull(message = "Category name cannot be empty", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

SQL executed in the background

 SELECT COUNT( * )
FROM act_cx_category
WHERE (category_name = 'Project Management System'
AND category_id <> '1f03b4e8bd920c6991cdc48d32b96cb40'
AND app_id = '1691701502841065473');

CASE 2: New scenes and parameters have no table primary key

parameters:
  {
    "categoryName": "Project Management System",
    "sort": 0
  }
  Request header: app-id: 1691701502841065473
1. Do not use extensions
 /**
   * Category Name
   */
  @UniqueField(message = "Category name already exists")
  @ApiModelProperty("category name")
  @NotNull(message = "Category name cannot be empty", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

Execute SQL in the background

SELECT
    COUNT(*)
FROM
    act_cx_category
WHERE
    (category_name = 'Project Management System')
2. Use expansion
 /**
   * Category Name
   */
  @UniqueField(message = "Category name already exists", extend = UniqueExtendAppIdImpl.class)
  @ApiModelProperty("category name")
  @NotNull(message = "Category name cannot be empty", groups = {AddGroup.class, EditGroup.class})
  private String categoryName;

SQL executed in the background

SELECT
    COUNT(*)
FROM
    act_cx_category
WHERE
    (category_name = 'Project Management System' AND app_id = '1691701502841065473')

4. Description

This article uses 2 custom annotations + 1 extended interface to implement the annotation method to verify whether the fields in the database are repeated.

The two annotations need to be used together. You can define the implementation class of the extended interface yourself and implement verification rules that conform to your own logic.