项目实战四-元数据模块-省份、城市

省份、城市

  1. 经分析省份跟城市可以放在一张表中,因此将之前的做的省份模板删除:po、service、query、controller层,都删了
  2. 省份与城市共同的表叫做plate_region

     //省份、城市放在同一张表格,城市的parent_id指向省份的id
     id  name    plate       pingyin     parent_id
     3   广东      粤       GUANG_DONG      0
     4   福建      闽       FU_JIAN         0
     5   广州      A        guangzhou       3
    
  3. 通过EasyCode-MybatisCodeHepler生成对应层的类
    1. 生成的类有po(PlateRegion)、mapper(PlateRegionMapper)、service(PlateRegionService/PlateRegionServiceImpl)、controller(PlateRegionController)
  4. 各层的代码示例
    1. PlateRegion

       //com.zh.jk.pojo.po
       @Data
       public class PlateRegion {
           /**
           * 主键
           */
           private Integer id;
           /**
           * 名称
           */
           private String name;
           /**
           * 车牌
           */
           private String plate;
           /**
           * 拼音
           */
           private String pinyin;
                  
           private Integer parentId;
       }
      
    2. query

       //com.zh.jk.pojo.query
       //CityQuery
       @EqualsAndHashCode(callSuper = true)
       @Data
       public class CityQuery extends KeywordQuery {
           /**
            * 省份id
            */
           private Integer parentId;
       }
       //ProvinceQuery
       public class ProvinceQuery extends KeywordQuery {}
      
    3. mapper

       //com.zh.jk.mapper
       public interface PlateRegionMapper extends BaseMapper<PlateRegion> {}
      
    4. service

       //com.zh.jk.service
       //PlateRegionService
       public interface PlateRegionService extends IService<PlateRegion> {
           //分页查询所有省份
           void listProvinces(ProvinceQuery query);
           //分页查询所有城市
           void listCities(CityQuery query);
           //不分页查询所有省份
           List<PlateRegion> listProvinces();
       }
       ////com.zh.jk.service.impl
       //PlateRegionServiceImpl
       @Service
       @Transactional
       public class PlateRegionServiceImpl extends ServiceImpl<PlateRegionMapper, PlateRegion> implements PlateRegionService {       
           @Override
           @Transactional(readOnly = true)
           public void listProvinces(ProvinceQuery query) {
               MpQueryWrapper<PlateRegion> wrapper = new MpQueryWrapper<>();
               wrapper.like(query.getKeyword(),
                       PlateRegion::getName,
                       PlateRegion::getPlate,
                       PlateRegion::getPinyin);
               //查询所有的省份,getParentId都是0的,都是省份
               wrapper.eq(PlateRegion::getParentId,0);
               wrapper.orderByDesc(PlateRegion::getId);
               baseMapper.selectPage(new MpPage<>(query),wrapper).updateQuery();
              
           }
              
           @Override
           @Transactional(readOnly = true)
           public void listCities(CityQuery query) {
               MpQueryWrapper<PlateRegion> wrapper = new MpQueryWrapper<>();
               wrapper.like(query.getKeyword(),
                       PlateRegion::getName,
                       PlateRegion::getPlate,
                       PlateRegion::getPinyin);
               Integer provinceId = query.getParentId();
               if (provinceId != null && provinceId >0 ){
                   //查询指定省份的城市
                   wrapper.eq(PlateRegion::getParentId,provinceId);
               }else {
                   //查询所有省份的城市,getParentId != 0
                   wrapper.ne(PlateRegion::getParentId,0);
               }
               wrapper.orderByDesc(PlateRegion::getId);
               baseMapper.selectPage(new MpPage<>(query),wrapper).updateQuery();
           }
              
           @Override
           @Transactional(readOnly = true)
           public List<PlateRegion> listProvinces() {
               MpQueryWrapper<PlateRegion> wrapper = new MpQueryWrapper<>();
               wrapper.eq(PlateRegion::getParentId,0);
               wrapper.orderByDesc(PlateRegion::getPinyin);
               return baseMapper.selectList(wrapper);
           }
       }
      

Mybaits-plus 默认方法增强(重点理解!!!)

  1. 编辑一个省份信息时,保存的只有省份名称、车牌,并没有保存省份的拼音,那么数据库中pinyin字段肯定是空值,那么也就无法实现拼音的搜索

    图1

  2. 如何实现汉字自动转化成拼音,而且还能自动保存到数据库中呢?
  3. 当前保存使用的是BaseController的save方法

     @PostMapping("/save")
     public R save(T entity){
         if (getService().saveOrUpdate(entity)){
             return Rs.ok("保存成功");
         }else {
             throw new CommonException("保存失败");
         }
     }
    
    1. 该方法是使用service的saveOrUpdate方法,而PlateRegionServiceImpl继承自ServiceImpl
    2. ServiceImpl为mybitis自己封装的类,里面有常用的sql方法,其中包含saveOrUpdate
  4. ServiceImpl的saveOrUpdate分析

     public boolean saveOrUpdate(T entity) {
         if (null == entity) {
             return false;
         } else {
             ...
             return StringUtils.checkValNull(idVal) || Objects.isNull(getById((Serializable) idVal)) ? save(entity) : updateById(entity);
         }
     }
    
    1. 可以看到本质是调用了this.updateById、this.save,这两个方法
    2. 而且PlateRegionServiceImpl类是继承了ServiceImpl这个类,因此可以想到在子类中重写这2个方法,做个方法的增强
  5. 改造PlateRegionServiceImpl类

     //重写父类方法,手动实现拼音的存储,到数据库
     @Override
     public boolean save(PlateRegion entity) {
         //手动处理将汉字转为拼音,然后赋值给PlateRegion的pinyin属性
         processPinyin(entity);
         return super.save(entity);
     }
        
     @Override
     public boolean updateById(PlateRegion entity) {
         //手动处理将汉字转为拼音,然后赋值给PlateRegion的pinyin属性
         processPinyin(entity);
         return super.updateById(entity);
     }
    
  6. 如何实现汉字转拼音呢?
    1. pom.xml添加依赖tinypinyin

       <!-- 将汉字转为拼音-->
       <dependency>
           <groupId>com.github.promeg</groupId>
           <artifactId>tinypinyin</artifactId>
           <version>${tinypinyin.version}</version>
       </dependency>
      
    2. PlateRegionServiceImpl封装方法

       private void processPinyin(PlateRegion region) {
           String name = region.getName();
           if (name == null) return;
           //将汉字转为拼音
           //使用第三方库,tinypinyin
           //guang_dong
           region.setPinyin(Pinyin.toPinyin(name,"_"));
       }
      

数据的一致性

  1. 一张表的数据跟多张表有关联,如果删除这张表的某条数据,那么它所关联的其他表的数据也要删除,这就是数据的一致性,比如:省份表,每个省份下面有n个城市,如果删除了某个省份,那么这个省份下的所有城市也应该删除
  2. 解决方法:
    1. 方法一: 通过数据库的外键解决,本质是数据库底层直接实现,但是这种方法比较耗性能,目前市场不再使用
    2. 方法二: 通过应用层面来解决
  3. 通过应用层面实现数据一致性的思路
    1. 自定义注解,注解每个有关联po对象(每个po对应一张表)的属性,然后扫描所有的po对象
    2. 通过AOP拦截service层所有的impl实现类的remove方法,然后在remove方法中进行分析

新增类、字符串处理工具类

  1. 类的处理工具Classes

     //com.zh.jk.common.util
     public class Classes {
         /**
          * 返回第一个不是Object.class的类
          */
         public static Class<?> notObject(Class<?>... sources) {
             if (sources == null) return null;
             for (Class<?> source : sources) {
                 if (!source.equals(Object.class)) return source;
             }
             return null;
         }
        
         /**
          * 获取cls类中的fieldName属性
          */
         public static Field getField(Class<?> cls, String fieldName) throws Exception {
             return enumerateFields(cls, (field, curCls) -> {
                 if (field.getName().equals(fieldName)) return Stop.create(field);
                 return null;
             });
         }
        
         /**
          * 遍历cls的所有属性
          */
         public static <T> T enumerateFields(Class<?> cls,
                                             FieldConsumer<T> fieldConsumer) throws Exception {
             if (fieldConsumer == null || cls == null) return null;
             Class<?> curCls = cls;
             while (!curCls.equals(Object.class)) {
                 for (Field field : curCls.getDeclaredFields()) {
                     Stop<T> stop = fieldConsumer.accept(field, curCls);
                     if (stop != null) return stop.getData();
                 }
                 curCls = curCls.getSuperclass();
             }
             return null;
         }
        
         public interface FieldConsumer<T> {
             Stop<T> accept(Field field, Class<?> ownerCls) throws Exception;
         }
     }
    
  2. 字符串的处理工具Strings

     //com.zh.jk.common.util
     public class Strings {
         private static final int DELTA = 'a' - 'A';
        
         public static boolean isEmpty(String source) {
             return source == null || source.equals("");
         }
        
         /**
          * 首字母变小写
          * @return TestCase -> testCase
          */
         public static String firstLetterLowercase(String source) {
             if (isEmpty(source)) return source;
             StringBuilder res = processFirstLetterLowercase(source);
             int len = source.length();
             for (int i = 1; i < len; i++) {
                 res.append(source.charAt(i));
             }
             return res.toString();
         }
        
         private static StringBuilder processFirstLetterLowercase(String source) {
             StringBuilder res = new StringBuilder();
             // 拼接首字符
             char firstChar = source.charAt(0);
             if (isBigLetter(firstChar)) {
                 res.append((char) (firstChar + DELTA));
             } else {
                 res.append(firstChar);
             }
             return res;
         }
        
         /**
          * 驼峰 -> 下划线
          * @return TestCase -> test_case
          */
         public static String camel2underline(String source) {
             if (isEmpty(source)) return source;
             StringBuilder res = processFirstLetterLowercase(source);
             // 其他字符
             int len = source.length();
             for (int i = 1; i < len; i++) {
                 char c = source.charAt(i);
                 if (isBigLetter(c)) {
                     res.append("_");
                     res.append((char) (c + DELTA));
                 } else {
                     res.append(c);
                 }
             }
             return res.toString();
         }
        
         /**
          * 下划线 -> 小驼峰
          * @return test_case -> testCase
          */
         public static String underline2smallCamel(String source) {
             return underline2camel(source, false);
         }
        
         /**
          * 下划线 -> 大驼峰
          * @return test_case -> TestCase
          */
         public static String underline2bigCamel(String source) {
             return underline2camel(source, true);
         }
        
         private static String underline2camel(String source, boolean big) {
             if (isEmpty(source)) return source;
             StringBuilder res = new StringBuilder();
             // 其他字符
             int len = source.length();
             // 上一个字符是下划线
             boolean prevUnderline = false;
             for (int i = 0; i < len; i++) {
                 char c = source.charAt(i);
                 if (c == '_') {
                     prevUnderline = true;
                     continue;
                 }
                 if (res.length() == 0) { // 首字符
                     if (big && isSmallLetter(c)) { // 大驼峰
                         res.append((char) (c - DELTA));
                     } else if (!big && isBigLetter(c)) { // 小驼峰
                         res.append((char) (c + DELTA));
                     } else {
                         res.append(c);
                     }
                 } else if (prevUnderline && isSmallLetter(c)) {
                     res.append((char) (c - DELTA));
                 } else {
                     res.append(c);
                 }
                 prevUnderline = false;
             }
             return res.toString();
         }
        
         public static boolean isBigLetter(char source) {
             return source >= 'A' && source <= 'Z';
         }
        
         public static boolean isSmallLetter(char source) {
             return source >= 'a' && source <= 'z';
         }
        
         /**
          * 返回第一个不为empty的字符串
          */
         public static String notEmpty(String... sources) {
             if (sources == null) return null;
             for (String source : sources) {
                 if (!isEmpty(source)) return source;
             }
             return null;
         }
     }
    
  3. enhance中新增一个增强类Stop

     //com.zh.jk.common.enhance
     @Data
     public class Stop<T> {
     private T data;
            
     public static <T> Stop<T> create() {
         return new Stop<>();
     }
            
     public static <T> Stop<T> create(T data) {
         Stop<T> stop = new Stop<>();
         stop.setData(data);
         return stop;
         }
     }
    

自定义注解

  1. 新建一个枚举ForeignCascade

     //com.zh.jk.common.foreign.anno
     //设置一个枚举,用于设置子表的某条数据删除,主表对应的那条数据是否也删除,DEFAULT不删除,DELETE直接删除
     public enum ForeignCascade {
         //默认不可以删、可直接删除
         DEFAULT, DELETE
     }
    
  2. 修饰外键字段注解:ForeignField,即一个属性被该注解修饰,说明该字段被其他表引用

     //com.zh.jk.common.foreign.anno
     //生成文档使用,标明是否生成javadoc文档,不重要
     @Documented
     /**
      * 注解的保留策略,注解必须有保留策略:SOURCE/ClASS/RUNTIME
      * SOURCE 文件是java源码阶段---磁盘中
      * ClASS 源码文件被编译成class字节码时----磁盘中
      * RUNTIME 应用程序启动被加载到运行内存时----运行内存中
      * 因为别的代码要读取某个注解肯定是从运行内存中读取,因此是RUNTIME
      */
     @Retention(RetentionPolicy.RUNTIME)
     // 当前注解被应用到哪个地方:类、属性、方法,还是其他
     @Target(ElementType.FIELD)
     @Repeatable(ForeignField.ForeignFields.class)
     public @interface ForeignField {
         /**
          * 当前这个字段的类型是什么
          */
         Class<?> value() default Object.class;
        
         /**
          * 当前这个字段被引用的主表类型是什么
          */
         Class<?> mainTable() default Object.class;
        
         /**
          * 被引用的主表类对应的属性名是什么,默认值是id
          */
         String mainField() default "id";
        
         /**
          * 当前这个属性在数据库中的字段名是什么
          */
         String column() default "";
        
         /**
          * 级联类型,是否是关联性删除,默认不删除
          */
         ForeignCascade cascade() default ForeignCascade.DEFAULT;
        
         /**
          * 定义注解类型,目的是为了在一个类、属性、方法 上多次使用ForeignField注解
          * 比如:一个类可以多多个字段使用ForeignField注解
          */
         @Documented
         @Retention(RetentionPolicy.RUNTIME)
         @Target(ElementType.FIELD)
         @interface ForeignFields {
             /**
              * 被引用的主表类
              */
             ForeignField[] value() default {};
         }
     }
    
  3. 修饰包含外键属性的类注解:ForeignTable,即一个类被该注解修饰,则该类中有字段包含外键

     @Documented
     @Retention(RetentionPolicy.RUNTIME)
     @Target(ElementType.TYPE)
     public @interface ForeignTable {
         /**
          * 数据库默认表名
          */
         String value() default "";
        
         /**
          * 对应数据库表名
          */
         String name() default "";
     }
    

自定义增强类

  1. 外键属性对应的类:ForeignFieldInfo

     //com.zh.jk.common.foreign.info
     /**
      * 该类用来实例化一个“属性”的所有信息:字段属性、列名、哪张表、
      */
     @Getter
     @Setter
     public class ForeignFieldInfo {
         /**
          * 属性名
          */
         private Field field;
         /**
          * 列名
          */
         private String column;
         /**
          * 表
          */
         private ForeignTableInfo table;
         /**
          * 当前字段是子表字段,这个字段可能由多张主表外键组成
          */
         private List<ForeignFieldInfo> mainFields;
         /**
          * 当前字段是主表字段,当前字段被多张表引用,每张表都引用的字段集合
          */
         private List<ForeignFieldInfo> subFields;
         /**
          * 级联类型
          */
         private ForeignCascade cascade;
        
         public void setField(Field field) {
             field.setAccessible(true);
             this.field = field;
         }
        
         public void addSubField(ForeignFieldInfo subField) {
             if (subFields == null) {
                 subFields = new ArrayList<>();
             } else if (subFields.contains(subField)) {
                 return;
             }
             subFields.add(subField);
         }
        
         public void addMainField(ForeignFieldInfo mainField) {
             if (mainFields == null) {
                 mainFields = new ArrayList<>();
             } else if (mainFields.contains(mainField)) {
                 return;
             }
             mainFields.add(mainField);
         }
     }
    
  2. 拥有外键属性对应表对应的类:ForeignTableInfo

     //com.zh.jk.common.foreign.info
     /**
      * 该类用来实例化一个“表”的所有信息,表对应的类、表名、外键是什么
      */
        
     @Getter
     @Setter
     public class ForeignTableInfo {
         // 表对应的类:ForeignTableInfo
         private static final Map<Class<?>, ForeignTableInfo> cache = new HashMap<>();
         //当前表对应的类
         private Class<?> cls;
         //当前表对应的表名
         private String table;
         //当前表对应的外键,当前表是主表,哪个字段被其他表引用。
         private ForeignFieldInfo mainField;
         /**
          * key是field的属性名 value是ForeignFieldInfo
          * 当前表是子表,存放着表中所有的外键字段
          */
         private Map<String, ForeignFieldInfo> subFields = new HashMap<>();
        
         /**
          * 根据一个表对应的类,获取到这个类的ForeignTableInfo对象
          */
         public static ForeignTableInfo get(Class<?> tableCls) {
             return get(tableCls, false);
         }
        
         /**
          * 从缓存中取出table
          * @param newIfAbsent 如果找不到就新建一个
          */
         public static ForeignTableInfo get(Class<?> tableCls, boolean newIfAbsent) {
             if (!newIfAbsent) return cache.get(tableCls);
             return cache.computeIfAbsent(tableCls, k -> {
                 ForeignTableInfo table = new ForeignTableInfo();
                 // 类
                 table.setCls(tableCls);
        
                 // 表名
                 ForeignTable tableAnno = tableCls.getAnnotation(ForeignTable.class);
                 String tableName;
                 if (tableAnno != null) {
                     tableName = Strings.notEmpty(tableAnno.name(), tableAnno.value());
                 } else {
                     tableName = Strings.camel2underline(tableCls.getSimpleName());
                 }
                 table.setTable(tableName);
                 return table;
             });
         }
         /**
          * 根据一个 外键属性 Field 封装成一个ForeignFieldInfo对象
          * 当前表是主表
          */
         public ForeignFieldInfo getMainField(Field field) {
             if (mainField == null) {
                 mainField = new ForeignFieldInfo();
                 mainField.setTable(this);
                 mainField.setField(field);
                 mainField.setColumn(getFieldColumn(field));
                    
                 //判断当前字段是否被注解
                 ForeignField ff = field.getAnnotation(ForeignField.class);
                 if (ff != null) {
                     //被注解,要设置它是否级联删除
                     mainField.setCascade(ff.cascade());
                 } else {
                     //没有备注接,不删除
                     mainField.setCascade(ForeignCascade.DEFAULT);
                 }
             }
             return mainField;
         }
        
         /**
          * 根据一个属性Field封装成一个ForeignFieldInfo对象
          * 当前表是子表
          */
         public ForeignFieldInfo getSubField(Field field) {
             String fieldName = field.getName();
             return subFields.computeIfAbsent(fieldName, k -> {
                 ForeignFieldInfo subField = new ForeignFieldInfo();
                 subField.setTable(this);
                 subField.setField(field);
                 subField.setColumn(getFieldColumn(field));
                 return subField;
             });
         }
         /**
          * 根据 类的属性Field获取到 表中的列名称
          */
         private String getFieldColumn(Field field) {
             ForeignField ff = field.getAnnotation(ForeignField.class);
             //1. 如果有特别注解,那么直接从注解中获取
             if (ff != null) {
                 String column = ff.column();
                 if (!Strings.isEmpty(column)) return column;
             }
             //2. 如果没有注解,那么这个数据库中的列名就是驼峰转下划线
             return Strings.camel2underline(field.getName());
         }
     }
    

po类添加注解

  1. DictItem

     //子表
     @Data
     //当前这张表是什么,由于类与表的转化默认的就是驼峰转下划线,所以这句可以不写
     //@ForeignTable(name = "dict_item")
     public class DictItem {
         /**
         * 主键
         */
         private Integer id;
         /**
         * 名称
         */
         private String name;
         /**
         * 值
         */
         private String value;
         /**
         * 排列顺序,默认0。值越大,就排在越前面
         */
         private Integer sn;
         /**
         * 是否禁用。0代表不禁用(启用),1代表禁用
         */
         private Short disabled;
            
         /**
         * 所属的类型
         * 当前表中的字段是type_id,是引用dict_type表中的id主键
         * 
             //一个id引用多张表,也支持
             @ForeignField(DictType.class)
             @ForeignField(Other.class)
             等价于
             @ForeignField({
                 @ForeignField(DictType.class)
                 @ForeignField(Other.class)
             })
         */
         //当前这张表引用的是哪张表,即主表mainTable是谁,引用主表的那个字段属性mainField,column当前这个字段在数据库中的字段是什么
         // @ForeignField(mainTable = DictType.class,mainField = "id",column = "type_id")
         //由于mainTable、mainField与默认值一样,因此mainTable、mainField可以不写
         @ForeignField(DictType.class)
         private Integer typeId;
     }
    
  2. DictType

     //主表
     @Data
     //当前这张表是什么
     //@ForeignTable(name = "dict_type")
     public class DictType {
         /**
         * 主键
         */
         //表明这个属性被其他表引用着,在数据库中的字段是id,而且如果其他表数据删了,这个表数据可以删除DELETE,DEFAULT代表必能删除
         //属性默认转表字段是原值或者下划线转驼峰、cascade使用DEFAULT,因此下面这句可以不写
         //@ForeignField(column = "id",cascade = ForeignCascade.DEFAULT)
         private Integer id;
         /**
         * 名称
         */
         private String name;
         /**
         * 值
         */
         private String value;
         /**
         * 简介
         */
         private String intro;
     }
    
  3. PlateRegion

     @Data
     public class PlateRegion {
         /**
         * 主键
         */
         private Integer id;
         /**
         * 名称
         */
         private String name;
         /**
         * 车牌
         */
         private String plate;
         /**
         * 拼音
         */
         private String pinyin;
        
         //城市引用着省份的id
         @ForeignField(PlateRegion.class)
         private Integer parentId;
     }
    

使用AOP切面编程

  1. pom.xml中引入

     <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-aop</artifactId>
     </dependency>
    
  2. foreign包下新建切入类

     //com.zh.jk.common.foreign
     @Aspect
     @Component
     public class ForeignAspect implements InitializingBean {
         private static final String FOREIGN_SCAN = "classpath*:com/zh/jk/pojo/po/**/*.class";
         @Autowired
         private ApplicationContext ctx;
         @Autowired
         private ResourceLoader resourceLoader;
        
         // 拦截service层的所有删除操作,删除省份,是否删除省份对应的城市
         @Around("execution(* com.zh.jk.service..*.remove*(..))")
         public Object handleRemove(ProceedingJoinPoint point) throws Throwable {
             Object target = point.getTarget();
             if (!(target instanceof IService)) return point.proceed();
        
             // 获取PO类
             Class<?> poCls = ((IService<?>) target).getEntityClass();
        
             // 根据po类拿到表对象
             ForeignTableInfo table = ForeignTableInfo.get(poCls);
             if (table == null) return point.proceed();
        
             // 主键,拿到这个表的引用Field
             ForeignFieldInfo mainField = table.getMainField();
             //如果为空,说明当前表没有关联的外键,或者当前表是子表,至二级返回
             if (mainField == null) return point.proceed();
        
             // 获取外键约束,根据这个主表的字段拿到引用该字段的所有字段(每个字段对应一张表)
             List<ForeignFieldInfo> subFields = mainField.getSubFields();
             if (CollectionUtils.isEmpty(subFields)) return point.proceed();
        
             // 获取参数值
             Object arg = point.getArgs()[0];
             List<Object> ids;
             if (arg instanceof List) {  //1. 批量删除省份
                 ids = (List<Object>) arg;
             } else {
                 ids = new ArrayList<>(); //2. 单个删除省份
                 ids.add(arg);
             }
        
             //遍历所有的子表
             for (ForeignFieldInfo subField : subFields) {
                 //根据子表字段获取到当前表的表类
                 ForeignTableInfo subTable = subField.getTable();
                 //获取到当前表对应的mapper
                 BaseMapper<Class<?>> mapper = getMapper(subTable.getCls());
                 //查询出这n个省份的ids对应的所有市数据
                 QueryWrapper<Class<?>> wrapper = new QueryWrapper<>();
                 wrapper.in(subField.getColumn(), ids);
                 if (mainField.getCascade() == ForeignCascade.DEFAULT) { // 默认
                     Integer count = mapper.selectCount(wrapper);
                     if (count == 0) continue;
                     Rs.raise(String.format("有%d条【%s】数据相关联,无法删除!", count, subTable.getTable()));
                 } else { // 删除关联数据
                     mapper.delete(wrapper);
                 }
             }
             return point.proceed();
         }
        
         // 拦截service层的所有添加操作,添加城市,如果插入的城市没有对应的省份,报错提示
         @Around("execution(* com.zh.jk.service..*.save*(..)) || execution(* com.zh.jk.service..*.update*(..)) ")
         public Object handleSaveOrUpdate(ProceedingJoinPoint point) throws Throwable {
             Object target = point.getTarget();
             if (!(target instanceof IService)) return point.proceed();
        
             // 获取po对象
             Class<?> poCls = ((IService<?>) target).getEntityClass();
        
             // 表格,获取到子表类
             ForeignTableInfo table = ForeignTableInfo.get(poCls);
             if (table == null) return point.proceed();
        
             // 获取外键约束:根据子表类获取到该子表所有引用的外键字段,可能有n个外键,引用n张主表
             Collection<ForeignFieldInfo> subFields = table.getSubFields().values();
             if (CollectionUtils.isEmpty(subFields)) return point.proceed();
        
             // 参数:判断参是否是当前的po类
             Object model = point.getArgs()[0];
             if (model.getClass() != poCls) {
                 return point.proceed();
             }
        
             // 遍历外键约束
             for (ForeignFieldInfo subField : subFields) {
                 //根据当前子表的所有外键字段拿到所有主表的表字段
                 List<ForeignFieldInfo> mainFields = subField.getMainFields();
                 if (CollectionUtils.isEmpty(mainFields)) continue;
                 // 引用的主键超过1个,无法智能处理,需要手动处理
                 if (mainFields.size() > 1) continue;
        
                 Object subValue = subField.getField().get(model);
                 // 跳过空值(代表此字段不进行更新)
                 if (subValue == null) continue;
        
                 // 唯一的一个主键
                 ForeignFieldInfo mainField = mainFields.get(0);
                 BaseMapper<Class<?>> mapper = getMapper(mainField.getTable().getCls());
                 QueryWrapper<Class<?>> wrapper = new QueryWrapper<>();
                 wrapper.eq(mainField.getColumn(), subValue);
                 if (mapper.selectCount(wrapper) == 0) {
                     Rs.raise(String.format("%s=%s不存在", subField.getColumn(), subValue));
                 }
             }
             return point.proceed();
         }
        
         /**
          * 在spring的bean的生命周期中,实例化->生成对象->属性填充后会进行afterPropertiesSet方法
          * 1. 通过加载器获取到FOREIGN_SCAN 下的所有资源
          * 2. 读取出所有资源对应的类名
          */
         @Override
         public void afterPropertiesSet() throws Exception {
             ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
             Resource[] rs = resolver.getResources(FOREIGN_SCAN);
             if (rs.length == 0) {
                 Rs.raise("FOREIGN_SCAN配置错误,找不到任何类信息");
             }
        
             MetadataReaderFactory readerFactory = new CachingMetadataReaderFactory(resourceLoader);
             for (Resource r : rs) {
                 parseCls(readerFactory.getMetadataReader(r).getClassMetadata().getClassName());
             }
         }
        
         private BaseMapper<Class<?>> getMapper(Class<?> poCls) {
             return (BaseMapper<Class<?>>) ctx.getBean(Strings.firstLetterLowercase(poCls.getSimpleName()) + "Mapper");
         }
         /**
          * 遍历出一个po类对应的所有带ForeignField注解的属性
          * 1. 并根据po类创建ForeignTableInfo实例并保存到ForeignTableInfo的cache中去
          */
         private void parseCls(String clsName) throws Exception {
             //获取po类
             Class<?> subCls = Class.forName(clsName);
             //根据po类创建对应的表类ForeignTableInfo,本质会存储到ForeignTableInfo的cache成员中
             ForeignTableInfo subTable = ForeignTableInfo.get(subCls, true);
             // 遍历这个po类所有的属性
             Classes.enumerateFields(subCls, (subField, curCls) -> {
                 //获取到某个属性对应的ForeignField注解
                 ForeignField ff = subField.getAnnotation(ForeignField.class);
                 parseForeignField(subTable, subField, ff);
                 //获取到某个属性对应的ForeignFields注解
                 ForeignFields ffs = subField.getAnnotation(ForeignFields.class);
                 if (ffs == null) return null;
                 for (ForeignField subFf : ffs.value()) {
                     parseForeignField(subTable, subField, subFf);
                 }
                 return null;
             });
         }
        
         /**
          * 
          * @param subTable 表类ForeignTableInfo
          * @param subField 带标注的字段
          * @param ff  标注内容
          * @throws Exception
          */
         private void parseForeignField(ForeignTableInfo subTable,
                                        Field subField,
                                        ForeignField ff) throws Exception {
             // 跳过没有ForeignField注解的属性
             if (ff == null) return;
             // 主表的类
             Class<?> mainCls = Classes.notObject(ff.mainTable(), ff.value());
             // 说明ForeignField注解的是主键属性(因为缺乏mainCls),就是当前这个字段被别人引用,而不是引用别人
             if (mainCls == null || mainCls.equals(Object.class)) return;
        
             // 主表中的主键属性
             Field mainField = Classes.getField(mainCls, ff.mainField());
             // 跳过错误(找不到)的主键属性
             if (mainField == null) return;
        
             //根据主表类获取到主表的表类ForeignTableInfo
             ForeignTableInfo mainTable = ForeignTableInfo.get(mainCls, true);
             //根据子表类获取到获取到子表字段类
             ForeignFieldInfo subFieldInfo = subTable.getSubField(subField);
             //根据主表类获取到获取到主表字段类
             ForeignFieldInfo mainFieldInfo = mainTable.getMainField(mainField);
        
             // 存储到缓存中
             // 将主表字段类存储到子表字段类中
             subFieldInfo.addMainField(mainFieldInfo);
             //将子表字段类存储到主表字段类中
             mainFieldInfo.addSubField(subFieldInfo);
         }
     }
    
  3. 运行程序,删除【数据字典类型】中的某条数据,就无法删除,因为这条数据id被数据字典条目中的某些数据引用着了

Table of Contents