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.