引言

    最近在修改公司项目中由于用户数据软删除引发的一系列问题时,对于外键的使用也进行了一波思考。
相信看过阿里开发手册的朋友们应该看到过这一段话

因此特意百度了一番,众说纷纭。当然今天不是为了探讨外键的使用与否,今天我思考的问题是

在不使用外键的情况下,如何方便地保证关联数据的完整性

或者暂不提使不使用外键

在关联数据软删除的情况下,如何方便地保证关联数据的完整性

    至于为什么会有对于这个问题的思考呢,最近修改公司项目中由于用户软删除引发的一系列数据完整性问题时,开始了对于这方面的思考。其实公司项目使用了外键,相关联数据如果被删除会抛出DataIntegrityViolationException异常,因此在不考虑是否应该使用外键的情况下,这是可以解决问题的。问题是我们的用户数据是软删除,无法使用外键进行统一的关联校验,对于数据完整性要求比较高的场景下,这样肯定是不可以的。
    至于说在删除的地方一个一个校验使用的地方,可不可以呢,当然是可以,但是肯定不是最好的方法。不仅代码冗余,而且这样我岂不是每增加一个使用的地方就要回过头去修改一遍删除校验的代码。
    因此针对这个问题,我提出一点自己的想法,如果各位有更好的方法希望可以告诉我~
    我提出的方案是使用AOP 注解的方式,在相关联的字表的DO实体的字段上加上关联注解(LinkedField),说明是关联的哪张表中的哪个字段,并在spring容器启动的过程中就将这些关联关系同步到redis中,然后在删除方法上也加上一个注解(LinkedSource),说明需要删除哪张表中哪个字段,然后在AOP的增强中获取当前主表字段有多少子表关联数据,就可以实现了。其实,换一句话说,我把数据库中的外键关系copy到了应用层中。

正文

一、创建关联关系注解LinkedField

    LinkedField注解,你可以理解为就是应用层的外键,它说明了主表和子表中对应的字段关联关系。它是一个Field类型的注解,说明是加载属性上的。
    这个注解虽然是加在子表的DO的子段上的,实际对子表的增删改查没有影响,只是找一个地方定义一个关联关系,当然我这里认为“您将会为每一个表都创建一个对应的DO”

/*** 关联数据注解*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface LinkedField {/*** 主表*/String sourceTable();/*** 主表中关联字段*/String sourceField();/*** 子表*/String linkedTable();/*** 子表中关联字段*/String linkedField();
}

二、创建数据库表一一对应的DO

    假设我有一张sys_user表作为主表,另外有四张表,分别是教师表(teacher)、学生表(student)、成绩表(score)、男生表(boy),四个主表中都有个user_id字段对应用户表中的主键id,我现在要实现,当该用户数据在四个子表中有关联数据,不能删除。
创建四个子表的DO(对应的建表语句文中就不贴出来了)
1、teacher

@Component
@Data
public class Teacher {private Long id;private String name;@LinkedField(sourceTable = "sys_user",sourceField = "id",linkedTable = "teacher",linkedField = "user_id")private Long userId;
}

2、student

@Component
@Data
public class Student {private Long id;private String name;@LinkedField(sourceTable = "sys_user",sourceField = "id",linkedTable = "student",linkedField = "user_id")private Long userId;
}

3、分数表

@Component
@Data
public class Score {private Long id;private String name;@LinkedField(sourceTable = "sys_user", sourceField = "id", linkedTable = "score", linkedField = "user_id")private Long createUser;
}

4、男生表

@Component
@Data
public class Boy {private Long id;private String name;@LinkedField(sourceTable = "sys_user",sourceField = "id",linkedTable = "boy",linkedField = "user_id")private Long userId;
}

三、Spring启动过程中将所有关联关系同步到 redis中

想要在spring启动过程中假如自定义处理可以通过实现BeanPostProcessor,看一下这个接口都有哪些方法。有两个类似的方法,方法参数中bean是spring启动过程中初始化的所有的类,在这里就可以通过反射拿到类的属性,同样的也就可以拿到属性上面LinkedField注解的内容。将所有的关联关系同步到redis中,方便后续做切面的时候读取。

public interface BeanPostProcessor {@Nullabledefault Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}@Nullabledefault Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {//bean是spring启动过程中初始化的所有的类,在这里就可以通过反射拿到类的属性,同样的也就可以拿到属性上面LinkedField注解的内容return bean;}
}

看一下加入了自定义处理逻辑的代码

/*** spring启动时初始化关联关系到redis中*/
@Component
public class LinkedFieldAnnotationStartUpListener implements BeanPostProcessor {@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {return bean;}@Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {//当前redis容器中存储的所有关联记录List<LinkedFieldEntity> result = redisTemplate.opsForList().range("linkedField", 0, -1);Field field;Field[] fields = bean.getClass().getDeclaredFields();for (int i = 0; i < fields.length; i++) {//关闭反射安全检查,达到提升反射速度的目的fields[i].setAccessible(true);}List<LinkedFieldEntity> linkedFieldEntityList = new ArrayList<>();for (int i = 0; i < fields.length; i++) {try {//反射拿到所有bean的字段field = bean.getClass().getDeclaredField(fields[i].getName());//拿到加了LinkedField注解的字段注解信息LinkedField linkedField = field.getAnnotation(LinkedField.class);if (linkedField != null) {System.out.println("\r\nspring容器启动过程中获取加了LinkedField注解的字段,注解参数:");System.out.println("sourceTable: " + linkedField.sourceTable()+ " ;sourceField: " + linkedField.sourceField()+ " ;linkedTable: " + linkedField.linkedTable()+ " ;linkedField: " + linkedField.linkedField());LinkedFieldEntity linkedFieldEntity = new LinkedFieldEntity();linkedFieldEntity.setSourceTable(linkedField.sourceTable());linkedFieldEntity.setSourceField(linkedField.sourceField());linkedFieldEntity.setLinkedTable(linkedField.linkedTable());linkedFieldEntity.setLinkedField(linkedField.linkedField());//判断当前字表是否已经同步到redis容器中,避免重复存储List<LinkedFieldEntity> matchedList = result.stream().filter(item -> (item.getSourceTable().equals(linkedFieldEntity.getSourceTable())&& item.getSourceField().equals(linkedFieldEntity.getSourceField())&& item.getLinkedTable().equals(linkedFieldEntity.getLinkedTable())&& item.getLinkedField().equals(linkedFieldEntity.getLinkedField()))).collect(Collectors.toList());if (matchedList == null || matchedList.size() == 0) {//新创建的关联关系才存储到redis中linkedFieldEntityList.add(linkedFieldEntity);}}} catch (NoSuchFieldException e) {e.printStackTrace();}}if (linkedFieldEntityList.size() > 0) {//将新创建的关联记录同步到redis中redisTemplate.opsForList().rightPushAll("linkedField", linkedFieldEntityList);}return bean;}
}

四、在删除(不使用外键,或软删除)的方法上做切点

1、删除方法的注解

LinkedSource 注解是METHOD类型注解。table参数说明是删除操作的是哪张表,field表明操作的是哪个字段。

/*** 目标关联数据注解*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LinkedSource {String table();String field();
}

2、切面
这个切面的作用是根据删除方法上注解定义的正在操作的表,找出事先定义好的子表关联关系,然后在切面内统一查询是否有关联数据,如果有,进行后续处理,一般是抛出自定义异常,转化成json格式数据提示前台,我这里就简单打印一下。

@Component
@Aspect
public class LinkedSourceImpl {@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate CountService countService;/*** 切点*/@Pointcut("@annotation(com.datacheck.demo.anno.LinkedSource)")public void linkedSourcePointCut() {}/*** 环绕*/@Around("linkedSourcePointCut()")public void linkedFieldAround(ProceedingJoinPoint point) {try {MethodSignature signature = (MethodSignature) point.getSignature();Method method = signature.getMethod();LinkedSource linkedSource = method.getAnnotation(LinkedSource.class);if (linkedSource != null) {//注解上的参数//删除的字段String field = linkedSource.field();//删除字段对应的的表名String table = linkedSource.table();//从环绕增强中取出方法参数String targetFieldValue = point.getArgs()[0]+"";System.out.println("LinkedSource.table:" + table + ";field:" + field + ";targetFieldValue:" + targetFieldValue);//从redis中取出全部数据List<LinkedFieldEntity> result = redisTemplate.opsForList().range("linkedField", 0, -1);System.out.println("关联关系 全部(条数): " + result.size());System.out.println("关联关系 全部(数据): " + result.toString());//筛选出当前删除的字段相关联的子表List<LinkedFieldEntity> operatingTableList = result.stream().filter(item->(item.getSourceTable().equals(table)&&item.getSourceField().equals(field))).collect(Collectors.toList());System.out.println("关联关系 当前操作相关的(条数): " + operatingTableList.size());System.out.println("关联关系 当前操作相关的(数据): " + operatingTableList.toString());//遍历查询子表中是否有关联记录for (LinkedFieldEntity entity : operatingTableList) {//动态拼接表名查询,查询关联字表中是否有关联的数据Integer count = countService.count(entity.getLinkedTable(), entity.getLinkedField(), targetFieldValue);System.out.println("\r\n"+entity.getLinkedTable()+"表中关联数据条数:"+count);if (count != null && count > 0) {System.out.println("注意:"+entity.getLinkedTable()+"表有关联数据");}}}} catch (Throwable throwable) {throwable.printStackTrace();}}
}

五、效果演示

1、启动过程中日志

我总共加了5个LinkedField注解,其中有四个都是关联的用户表中的主键id

2、删除用户

演示删除之前还得做两件事

(1)编写controller接口

增加删除方法,作用是根据用户id删除用户

@RestController
public class UserController {@LinkedSource( table = "sys_user",field = "id")@DeleteMapping(value = "/deleteById")public JsonResult deleteById(Integer id) {return ResultTool.success("测试删除接口");}
}

(2)增加表数据

增加几条关联数据,如下(我在四个表中加了3条关联数据)

(3)删除关联数据

六、结束语

这只是我站在一个菜鸟的角度,提出的一种方案,如果大佬们有更好的见解希望告诉我~~

关于不使用外键(或软删除)的情况下如何保证关联数据完整性的思考相关推荐

  1. Homework 1_SQL Server中由于外键约束而删除数据失败

    SQL Server中由于外键约束而删除数据失败 原因分析:外键约束问题.在配置文件中配置了一对一的关系,外键也是唯一的.数据库中数据有严格的依赖关系. 而在业务逻辑中,在往数据库里删除数据之前,却忘 ...

  2. mysql创建外键级联更新_MySQL使用外键实现级联删除与更新的方法

    本文实例讲述了MySQL使用外键实现级联删除与更新的方法.分享给大家供大家参考,具体如下: MySQL支持外键的存储引擎只有InnoDB,在创建外键的时候,要求父表必须有对应的索引,子表在创建外键的时 ...

  3. mysql级联删除外键约束_MySQL外键设置 级联删除

    . cascade方式 在父表上update/delete记录时,同步update/delete掉子表的匹配记录 . set null方式 在父表上update/delete记录时,将子表上匹配记录的 ...

  4. Mysql外键约束怎么删除

    记录一下碰到的问题.由于我是使用PowerDesigner来建mysql物理模型的,为了表与表之间的关系更加清楚,我给他们连线了.之后我就用它生成的SQL语句在navicat把表建出来,我看见没问题就 ...

  5. mysql中外键设置级联删除_MySQL中利用外键实现级联删除、更新

    MySQL中利用外键实现级联删除.更新 MySQL支持外键的存储引擎只有InnoDB,在创建外键的时候,要求父表必须有对应的索引,子表在创建外键的时候也会自动创建对应的索引.在 创建索引的时候,可以指 ...

  6. 从qq服务器删除误收邮件,QQ邮箱撤回时显示撤回失败,对方已读 收件箱里的消息却是对方已阅读或已删除 这种情况下邮件有没有被删除...

    QQ邮箱撤回时显示撤回失败,对方已读 收件箱里的消息却是对方已阅读或已删除 这种情况下邮件有没有被删除以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容 ...

  7. MySql 修改外键 支持级联删除

    首先必须要有外键,InnoDB甚么的都不说了,直接上修改句子. 要先删除该外键,然后添加. 具体原因貌似是因为不支持alter外键的操作. ALTER TABLE `t_terminal` DROP ...

  8. mysql创建外键级联更新_MySQL中利用外键实现级联删除、更新

    MySQL支持外键的存储引擎只有InnoDB,在创建外键的时候,要求父表必须有对应的索引,子表在创建外键的时候也会自动创建对应的索引.在创建索引的时候,可以指定在删除.更新父表时,对子表进行的相应操作 ...

  9. 删除 索引 外键 mysql_MySQL无法删除外键约束中所需的索引

    MySQL无法删除外键约束中所需的索引 我需要更改现有数据库以添加列. 因此,我还想更新UNIQUE字段以包含该新列. 我试图删除当前索引但仍然收到错误MySQL Cannot drop index ...

最新文章

  1. openpyxl 操作 Excel表的格基本用法
  2. idea中刷新项目快捷键_解决 IDEA 使用过程中让你觉得不爽的一些问题
  3. pcie的ack/nak机制
  4. quartz 数据库配置
  5. ALV字段编辑时,输入长度受限制解决方法
  6. SQL Server 中关于EXCEPT和INTERSECT的使用方法
  7. 什么是MCU里应尽量遵循的寄存器谨慎赋值法?
  8. 1290. 二进制链表转整数
  9. IE9 Platform Preview 3昨天发布
  10. ac3168无线网卡驱动下载_REALTEK芯片无线网卡最新驱动!支持到10.15
  11. rectangle函数与Rect函数的用法
  12. 高斯白噪声仿真-复信号分析
  13. Matlab:添加和删除表行
  14. Paypal学习 3 -- 接受信用卡直接付款 (DoDirectPayment)
  15. 【Unity3D 灵巧小知识点】 ☀️ | UnityHub中提示 许可证过期 了怎么办?
  16. Part3-4-1 搭建自己的SSR
  17. 1 dB压缩点_噪声系数_小信号非线性的数学描述
  18. Angular 组件类测试
  19. 考眼力的游戏你们玩过什么?
  20. 加州大学伯克利分校计算机科学专业,加州大学伯克利分校研究生计算机专业排名及申请...

热门文章

  1. 2022-2028年中国医疗人工智能行业投资策略探讨及市场规模预测报告
  2. 全屏模式下软键盘弹出遮挡输入框问题
  3. A316J(HCPL-316J)驱动电路的检修
  4. 中国移动5星级用户,手机10年不换号,会有什么特殊待遇?
  5. BH1750 传感器实战教学 —— 驱动移植篇
  6. 在亚马逊中如何选择关键词?
  7. Android 检测获取 Mac 权限
  8. A Unified Generative Framework for Aspect-Based Sentiment Analysis
  9. 艰辛的学习之路——错误:Status of node rabbit@LAPTOP-FMKQB7DT ... ** (ArgumentError) argument error (stdlib)
  10. python求100以内质数以及合数