首页 > 其他分享 >基于hibernate-validator实体字段唯一性检查 ,UniqueKey注解

基于hibernate-validator实体字段唯一性检查 ,UniqueKey注解

时间:2024-08-01 17:18:20浏览次数:8  
标签:hibernate return String fields value UniqueKey import validator

基于hibernate-validator实体字段唯一性检查 ,UniqueKey注解

前言

经常会在新增或修改时,检查某个字段或者多个字段的唯一性,如果重复就需要返回错误信息,重复代码写多了就准备写校验注解解决这个问题,分为两个版本,hibernate和mybatisplus

1.mybatisplus

注解

/**
 * 唯一约束
 * <p>
 * <a href="https://juejin.cn/post/7048658743197696008#heading-6">基于注解Uni检验字段是否重复</a><br/>
 * <a href="https://stackoverflow.com/questions/3495368/unique-constraint-with-jpa-and-bean-validation/3499111#3499111">Unique constraint with JPA and Bean Validation</a><br/>
 * <a href="https://stackoverflow.com/questions/17092601/how-to-validate-unique-username-in-spring">How to @Validate unique username in spring?</a><br/>
 * <p>
 *  使用:
 *  <pre>
 *      @UniqueKey(fields = "title")
 *      @UniqueKey.List(value = { @UniqueKey(fields = { "title" }), @UniqueKey(fields = { "author" }) }) // more than one unique keys
 *  </pre>
 * @author zengli
 * @date 2024/06/20
 */
@Documented
@Constraint(validatedBy = UniqueFieldsValidator.class)
@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RUNTIME)
public @interface UniqueKey {
    String message() default "{desc}--->{values}已存在";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    /**
     * 唯一约束的属性
     *
     * @return {@link String[]}
     */
    String[] fields();


    /**
     * 实体类
     * 如果在dto上使用,请将该值设置为数据库实体类
     * @return {@link Class}<{@link ?}>
     */
    Class<?> entityClass() default Object.class;

    /**
     * mapper类 spring bean 名称
     *
     * @return {@link String}
     */
    String mapperName() default "";

    @Target({ ElementType.TYPE })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        UniqueKey[] value();
    }
}


Validator

public class UniqueFieldsValidator implements ConstraintValidator<UniqueKey, Object> {
    
    
    private String[] fields;
    private Class<?> clazz;
    private BaseMapper baseMapper;

    @Override
    public void initialize(UniqueKey constraintAnnotation) {
        this.fields = constraintAnnotation.fields();
        clazz = constraintAnnotation.entityClass();
        String mapperName = constraintAnnotation.mapperName();
        if(StrUtil.isNotEmpty(mapperName)){
            baseMapper = SpringUtil.getBean(mapperName, BaseMapper.class);
        }
    }

    @Override
    public boolean isValid(Object object, ConstraintValidatorContext context) {
        QueryWrapper queryWrapper = new QueryWrapper<>();
        if(ObjectUtil.equals(clazz,Object.class)){
            clazz = object.getClass();
        }
        TableInfo tableInfo = TableInfoHelper.getTableInfo(clazz);
        
        // 更新时主键不相等
        Field keyField = ReflectUtil.getField(clazz, tableInfo.getKeyProperty());
        String keyColumn = tableInfo.getKeyColumn();
        Object id = ReflectUtil.getFieldValue(object, keyField.getName());
        queryWrapper.ne(id!=null,keyColumn, id);
        
        Assert.notNull(tableInfo,"不存在实体{}对应的表", clazz); 
        if(baseMapper==null){
            baseMapper = getMapperByEntityClass(clazz);
        }
        List<TableFieldInfo> tableInfoFieldList = tableInfo.getFieldList();
        Map<String, TableFieldInfo> fieldInfoMap = tableInfoFieldList.stream().collect(Collectors.toMap(e -> e.getField().getName(), e -> e));
        Set<String> propertyList = tableInfoFieldList.stream().map(e -> e.getProperty()).collect(Collectors.toSet());
        Assert.isTrue(CollUtil.containsAll(propertyList, Arrays.asList(fields)), "注解中存在不是实体类{}的字段", clazz);
        Arrays.stream(fields).forEach(field -> {
            Object value = ReflectUtil.getFieldValue(object, field);
            String column = fieldInfoMap.get(field).getColumn();
            queryWrapper.eq(column, value);
        });

        int count = baseMapper.selectCount(queryWrapper);
        boolean b = count == 0;
        if(!b){
            // 错误消息
            String desc =Arrays.stream(fields).map(e->{
                Field field = fieldInfoMap.get(e).getField();
                String value = field.getAnnotation(ApiModelProperty.class).value();
                return value;
            }).collect(Collectors.joining(","));
            String values=Arrays.stream(fields).map(e->{
                Object value = ReflectUtil.getFieldValue(object, e);
                return Convert.toStr(value);
            }).collect(Collectors.joining(","));
            context.unwrap(HibernateConstraintValidatorContext.class)
                    .addMessageParameter("desc", desc)
                    .addMessageParameter("values", values);
        }
        return b;
    }

    public <T> BaseMapper<T> getMapperByEntityClass(Class<T> entityClass) {
        String mapperName = entityClass.getSimpleName() + "Mapper";
        // 将首字母小写
        mapperName = Character.toLowerCase(mapperName.charAt(0)) + mapperName.substring(1);
        return (BaseMapper<T>) SpringUtil.getBean(mapperName);
    }
}

使用

@Data
@UniqueKey(fields = {"contractNo"},entityClass = SalesOrder.class)
public class SalesOrderOverviewDto {
    
    /** 主键id */
    @TableId(value = "id", type = IdType.AUTO)
    private String id;


    // @NotEmpty(message = "合同编号不能为空") 可以为空
    @ApiModelProperty(value = "合同编号")
    private String contractNo;


}

hibernate

注解


import java.io.Serializable;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

/**
 * <pre>
 *     @UniqueKey(property = "title")
 *     @UniqueKey.List(value = { @UniqueKey(property = { "title" }), @UniqueKey(property = { "author" }) }) // more than one unique keys
 * </pre>
 * 
 */
@Constraint(validatedBy = { UniqueKeyValidator.class})
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueKey {

	/**
	 * 唯一约束的属性
	 *
	 * @return {@link String[]}
	 */
	String[] fields();
	
	/**
	 * 实体类
	 * 如果在dto上使用,请将该值设置为数据库实体类
	 * @return {@link Class}<{@link ?}>
	 */
	Class<?> entityClass() default Object.class;


	String message() default "{desc}--->{values}已存在";

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

	Class<? extends Payload>[] payload() default {};

	@Target({ ElementType.TYPE })
	@Retention(RetentionPolicy.RUNTIME)
	@Documented
	@interface List {
		UniqueKey[] value();
	}

}


Validator



import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ReflectUtil;
import io.swagger.annotations.ApiModelProperty;
import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

import javax.annotation.Resource;
import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.Id;
import javax.persistence.PersistenceContext;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder;
import javax.validation.ConstraintValidatorContext.ConstraintViolationBuilder.NodeBuilderDefinedContext;


/**
 * 参考:<br/>
 * <a href="https://stackoverflow.com/questions/3495368/unique-constraint-with-jpa-and-bean-validation/3499111#3499111">Unique constraint with JPA and Bean Validation</a><br/>
 * <a href="https://lucasterdev.altervista.org/wordpress/2012/07/28/unique-constraint-validation-part-1/">example</a>
 * <p>
 * 存在一个问题,@Validated 会在controller执行,并且会在hibernate持久化之前执行,也就是执行了两次
 * entityManager 在hibernate持久化时会为空
 * 解决办法:<br/>
 * <a href="https://stackoverflow.com/questions/24955817/jsr303-custom-validators-being-called-twice">jsr303-custom-validators-being-called-twice</a>
 * <a href="https://stackoverflow.com/questions/65071004/how-to-do-custom-validation-on-entity-for-multitenant-setup-using-spring-hiber">how-to-do-custom-validation-on-entity-for-multitenant-setup-using-spring-hiber</a>
 * <pre><code>
 *     spring.jpa.properties.javax.persistence.validation.mode=none
 * </code></pre>
 * <pre>{@code 
 * @Component
 * public class HibernateCustomizer implements HibernatePropertiesCustomizer {
 *
 *     private final ValidatorFactory validatorFactory;
 *
 *     public HibernateCustomizer(ValidatorFactory validatorFactory) {
 *         this.validatorFactory = validatorFactory;
 *     }
 *
 *     public void customize(Map<String, Object> hibernateProperties) {
 *         hibernateProperties.put("javax.persistence.validation.factory", validatorFactory);
 *     }
 * }
 * 
 * }
 * {@code 
 * @Configuration
 * public class BeanValidationConfig {
 *    @Bean
 *    public LocalValidatorFactoryBean getValidator() {
 *        return new LocalValidatorFactoryBean();
 *    }
 * }   
 * }
 * </pre>
 */
// @Component
public class UniqueKeyValidator implements
		ConstraintValidator<UniqueKey, Object> {

	@PersistenceContext
	private EntityManager entityManager;

	private UniqueKey constraintAnnotation;

	public UniqueKeyValidator() {}

	public UniqueKeyValidator(final EntityManager entityManager) {
		this.entityManager = entityManager;
	}

	public EntityManager getEntityManager() {
		return entityManager;
	}

	@Override
	public void initialize(final UniqueKey constraintAnnotation) {
		this.constraintAnnotation = constraintAnnotation;
	}

	@Override
	public boolean isValid(final Object target,
			final ConstraintValidatorContext context) {

		if (entityManager == null) {
			// eclipselink may be configured with a BeanValidationListener that
			// validates an entity on prePersist
			// In this case we don't want to and we cannot check anything (the
			// entityManager is not set)
			//
			// Alternatively, you can disalbe bean validation during jpa
			// operations
			// by adding the property "javax.persistence.validation.mode" with
			// value "NONE" to persistence.xml
			// throw new RuntimeException("entityManager 为空");
			// TODO 可以测试分组校验是否能解决这个问题
			return true;
		}
		Class clazz = constraintAnnotation.entityClass();
		if(Objects.equals(clazz,Object.class)){
			clazz=target.getClass();
		}
		final Class<?> entityClass = clazz;

		final CriteriaBuilder criteriaBuilder = entityManager
				.getCriteriaBuilder();

		final CriteriaQuery<Object> criteriaQuery = criteriaBuilder
				.createQuery();

		final Root<?> root = criteriaQuery.from(entityClass);

		try {
			List<String> fields = Arrays.asList(constraintAnnotation.fields());
			if(CollUtil.isEmpty(fields))return true;
			Map<String, Object> field2ValueMap = fields.stream().collect(Collectors.toMap(e -> e, field -> {
				Object value = ReflectUtil.getFieldValue(target, field);
				return value;
			}));

			List<Predicate> predicateList = field2ValueMap.entrySet().stream().map(e->{
				String key = e.getKey();
				Object value = e.getValue();
				return criteriaBuilder.equal(root.get(key), value);
			}).collect(Collectors.toList());

			final Field idField = getIdField(entityClass);
			final String idProperty = idField.getName();
			final Object idValue = ReflectUtil.getFieldValue(target, idProperty);

			if (idValue != null) {
				final Predicate idNotEqualsPredicate = criteriaBuilder
						.notEqual(root.get(idProperty), idValue);
				predicateList.add(idNotEqualsPredicate);
				criteriaQuery.select(root).where(predicateList.toArray(new Predicate[0]));
			} else {
				criteriaQuery.select(root).where(predicateList.toArray(new Predicate[0]));
			}

			final List<Object> resultSet = entityManager.createQuery(criteriaQuery)
					.getResultList();

			if (!resultSet.isEmpty()) {
				// ConstraintViolationBuilder cvb = context
				// 		.buildConstraintViolationWithTemplate(constraintAnnotation
				// 				.message());
				// NodeBuilderDefinedContext nbdc = cvb.addNode(constraintAnnotation
				// 		.property());
				// ConstraintValidatorContext cvc = nbdc.addConstraintViolation();
				// cvc.disableDefaultConstraintViolation();

				// 错误消息
				String desc =fields.stream().map(e->{
					Field field = ReflectUtil.getField(entityClass, e);
					String value = Optional.ofNullable(field.getAnnotation(ApiModelProperty.class)).map(t->t.value()).orElse(e);
					return value;
				}).collect(Collectors.joining(","));
				String values=fields.stream().map(e->{
					Object value = field2ValueMap.get(e);
					return Convert.toStr(value);
				}).collect(Collectors.joining(","));
				context.unwrap(HibernateConstraintValidatorContext.class)
						.addMessageParameter("desc", desc)
						.addMessageParameter("values", values);
				return false;
			}

		} catch (final Exception e) {
			throw new RuntimeException(
					"An error occurred when trying to create the jpa predicate for the @UniqueKey '"
							+ Arrays.toString(constraintAnnotation.fields())
							+ "' on bean "
							+ entityClass + ".", e);
		}

		return true;
	}

	/**
	 * 获取实体类主键
	 *
	 * @param clazz
	 * @return
	 */
	public static Field getIdField(Class<?> clazz) {
		Field[] fields = clazz.getDeclaredFields();
		Field item = null;
		for (Field field : fields) {
			Id id = field.getAnnotation(Id.class);
			if (id != null) {
				field.setAccessible(true);
				item = field;
				break;
			}
		}
		if (item == null) {
			Class<?> superclass = clazz.getSuperclass();
			if (superclass != null) {
				item = getIdField(superclass);
			}
		}
		return item;
	}

}

使用

@UniqueKey(fields = {"projectId","elevatorCode"},message = "该电梯编码已存在请重新输入")
@Entity
@Data
@Table(name="elevators",uniqueConstraints ={@UniqueConstraint(columnNames= {"project_id","elevator_code"})})
public class Elevators extends BaseEntity{
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "`id`")
    @ApiModelProperty(value = "主键id 自增")
    private Long id;

    @Column(name = "`project_id`")
    private Long projectId;
    
    @Column(name = "`elevator_code`",updatable = false)
    private String elevatorCode;
    

}

  • 注意

    除非你获取了对整个表的锁,否则基本上不可能使用 SQL 查询来检查单一性(任何并发事务都可以在手动检查后但在正在进行的事务提交之前修改数据)。换言之,不可能在 Java 级别实现有效的唯一验证,从而提供验证实现。检查单一性的唯一可靠方法是在提交事务时。

标签:hibernate,return,String,fields,value,UniqueKey,import,validator
From: https://www.cnblogs.com/ntmd32/p/18337054

相关文章

  • hibernate不同实体不同填充创建人
    hibernate不同实体不同填充创建人使用的el-admin框架,框架本身填充的使用@CreatedBy注解加上AuditingEntityListener,@CreatedBy@Column(name="create_by",updatable=false)@ApiModelProperty(value="创建人",hidden=true)privateStringcreateBy;@Component("a......
  • 手写 Hibernate ORM 框架 05-基本效果测试
    手写Hibernate系列手写HibernateORM框架00-hibernate简介手写HibernateORM框架00-环境准备手写HibernateORM框架01-注解常量定义手写HibernateORM框架02-实体Bean定义,建表语句自动生成手写HibernateORM框架03-配置文件读取,数据库连接构建手写Hi......
  • 如何将 UniqueTogetherValidator 显示为字段错误而不是非字段错误?
    我有一个像这样的序列化器:classContactSerializer(serializers.ModelSerializer):classMeta:model=Contactfields=('account','first_name','last_name','email','phone_num......
  • HV000030: No validator could be found for constraint ‘javax.validation.constrai
    原文链接:https://blog.csdn.net/miachen520/article/details/119817478错误原因:数字类型不能使用javax.validation.constraints.Pattern注解解决办法方法一:.将字段类型设置为String类型;方法二:使用其他验证注解验证,数字的有@Size,@Min,@Max,@Range现将@Valid常用注解介绍如下:Be......
  • 【超实用攻略】SpringBoot + validator 轻松实现全注解式的参数校验
    一、故事背景关于参数合法性验证的重要性就不多说了,即使前端对参数做了基本验证,后端依然也需要进行验证,以防不合规的数据直接进入服务器,如果不对其进行拦截,严重的甚至会造成系统直接崩溃!本文结合自己在项目中的实际使用经验,主要以实用为主,对数据合法性验证做一次总结,不了解的朋......
  • gRPC 高级——Validator 验证器
    gRPC验证器(Validator)是一种用于在gRPC通信过程中进行数据验证的工具,通过在.proto文件中定义验证规则(例如长度限制、格式检查等),确保客户端和服务器之间传递的数据符合预期的格式和约束条件。它使用ProtocolBuffers作为序列化机制,并通过生成的代码在传输过程中自动执......
  • Hibernate-validator校验框架
    转载:http://blog.csdn.net/xgblog/article/details/525486591前言Validator开发者使用手册,适用后台校验功能的开发参考。1.1. 背景在我们日常后台的开发中,涉及到很多的字段验证,一般普通的验证框架就基本上可以满足日常的开发需求,但有一些情况,这些普通的验证框架无法达到要求......
  • "HIBERNATE_SEQUENCE" does not exist问题处理
    JavaWeb应用在MySQL环境下可以正常运行,数据迁移至Oracle或者人大金仓后应用运行爆出如下错误:严重:Servlet.service()forservlet[JeeCmsAdmin]incontextwithpath[/dhccms]threwexception[org.hibernate.exception.SQLGrammarException:couldnotgetnextsequence......
  • Hibernate 和 Mybatis 有何区别 ?
    Hibernate和MyBatis都是Java社区中广泛使用的持久层框架,用于在Java应用程序中管理数据库的CRUD操作(创建、读取、更新和删除)。尽管它们都旨在简化数据库操作,但它们在设计理念、实现方式和使用方式上有着显著的区别。HibernateHibernate是一个全功能的对象关系映射(ORM)框架,它......
  • 【p6spy】程序员开发利器P6spy——打印执行sql语句,mybatis、ibatis、Hibernate均可使
    一、前言在开发的过程中,总希望方法执行完了可以看到完整是sql语句,从而判断执行的是否正确,所以就希望有一个可以打印sql语句的插件。p6spy就是一款针对数据库访问操作的动态监控框架,他可以和数据库无缝截取和操纵,而不必对现有应该用程序的代码做任何修改。通过p6spy可......