mybatis-plus icon indicating copy to clipboard operation
mybatis-plus copied to clipboard

需求:逻辑删除新方案,回收站

Open jinceon opened this issue 2 years ago • 7 comments

目前一提到逻辑删除,就是在表内增加一个字段,比如deleted,=1时表示已删除,=0时表示未删除。 但它也会带来一些问题。 1、唯一索引无法使用。 2、大部分读操作(如果只是单表,自然没有问题;但对复杂报表、多表关联极其不友好)都需要过滤掉处于删除状态的记录。 3、开发人员无脑乱用。业务上的冻结、失效,本就应该和逻辑删除严格区分,不能用逻辑删除的概念去表达冻结、失效。比如人员离职,我们不会把员工删除,应该是要用一个字段表示离职这个状态,但是有些开发人员无脑用逻辑删除。 4、(我目前更多的场景)对于企业系统来说, 已经审批通过的记录,一般就不会提供删除功能。未审批的,草稿状态,都是人工录进去的,删掉了的后果也就是重新录入一次。

问:真的有必要用【表内】的逻辑删除吗? 逻辑删除的本质是备份,备份一定要在表内加deleted字段吗?能不能在表外备份?类似回收站的概念。 让80%的操作都能直接简单粗暴的物理删除,另外的20%真的有需要备份的,我们底层去帮它做表外的备份。 比如我们建个trash回收站表,里面存 源表名、内容(大json)、删除人、删除时间。当调用delete时,先把这条记录存到trash表,然后在源表delete掉。

所以,实现一个【回收站】组件? 思路: 1、用户在需要使用的表上加注解,如 @Trash 2、拦截delete操作,将要删除的记录复制到 trash 表后正常执行delete操作,将数据从源表里删除

也许可以同时维护一个回收站管理? 比如回收站默认存30天,30天后就真正删除(定时任务)? 比如提供一个管理界面,给(用户/管理员)恢复误删的记录

@TableName("user")
@Trash                             //在表这里加 @Trash 表示删除时要放入回收站
public class User {
    private long id;
    private String name;
}

jinceon avatar Apr 18 '22 01:04 jinceon

欢迎PR

huayanYu avatar Apr 18 '22 12:04 huayanYu

声明:本条仅作抛砖引玉之讨论,仅供参考。本人对该功能持中立态度。

我觉得这个需求是不是还欠讨论…

必要性

1、唯一索引无法使用。 2、大部分读操作(如果只是单表,自然没有问题;但对复杂报表、多表关联极其不友好)都需要过滤掉处于删除状态的记录。 3、开发人员无脑乱用。业务上的冻结、失效,本就应该和逻辑删除严格区分,不能用逻辑删除的概念去表达冻结、失效。比如人员离职,我们不会把员工删除,应该是要用一个字段表示离职这个状态,但是有些开发人员无脑用逻辑删除。 4、(我目前更多的场景)对于企业系统来说, 已经审批通过的记录,一般就不会提供删除功能。未审批的,草稿状态,都是人工录进去的,删掉了的后果也就是重新录入一次。

我觉得前两条是真实存在的痛点,但是后面两个嘛……更多属于业务和代码规范层面的东西。因为部分开发人员的滥用(实际上是偷懒)就砍掉功能或者大改内部实现,会不会有点奇怪 :grin:

安全性

按我个人的经验和理解,逻辑删除的备份属性有两层:

  1. 归档,即证明某些数据历史上产生过,可在必要的时候追溯到;
  2. 防丢失,即在因为业务或技术原因误删除后可以找回数据。

如果采用楼主说的主动式回收站(即显式指定哪些数据可被回收),很显然功能 1 可以被保留,但功能 2 却可能部分或完全丧失,因为开发过程中可能无法预见哪些数据会被误删除。那么如果某些数据因此永久丢失了,逻辑删除的意义也就不存在了,数据安全很成问题。

当然我们可以通过全量备份(即所有数据都进回收站)的方式避免这种风险,但这样显式指定的功能就完全退化了,而且原本一条 update 语句能完成的必须通过两条才能完成。

鲁棒性

  • 如何处理”复制数据“和”删除数据“两部分的事务原子性?两步骤中的任何一步失败后如何交回业务层(例如抛异常还是返回 false)?
  • 进一步地,如何实现批量的删除?
  • 如果原表发生了结构变更(例如新增或删除了列、批量修改了某些列的枚举值...),回收站的数据要如何同步?如果放弃同步表结构,以后恢复数据如何处理?
  • 对于数据量较大的表(千万+),如何解决物理删除可能带来的空洞问题? (注:该问题仅针对 mysql 。我司曾有一些频繁读写的表,每天千万级删除+写入,数据空洞十分头疼……)

Muyangmin avatar Apr 18 '22 15:04 Muyangmin

欢迎PR

VampireAchao avatar Apr 19 '22 05:04 VampireAchao

砍掉功能或者大改内部实现,会不会有点奇怪

是新增加回收站的插件实现。 不砍功能,也不改原来的内部实现

开发过程中可能_无法预见_哪些数据会被误删除

1、在用户层面,删除操作很多时候都会提示删除后数据不可恢复。如果用户坚持要点,他本就承受数据丢失的风险和损失。 2、在产品/业务层面,既然这个位置提供了删除功能给用户去操作,那这个位置的数据是否真有那么重要?如果真有那么重要,在产品设计的时候,就不该提供删除功能不是吗?比如员工离职,不该提供删除员工,而是应该冻结员工。 3、考虑到真就有用户手贱、真就有产品经理不合格,这个回收站其实就是一次后悔药。至于一个数据逻辑删除一年后、两年后要恢复,表结构变化了,哪怕是目前的逻辑删除一般也不是开放给用户直接恢复吧?还是得运维人员手动维护。那既然是手工维护,表结构变化后,人工适配就好啦。大家的职业生涯里,恢复数据工作量有多大?

进一步地,如何实现批量的删除?

把delete语句转select语句,查出来的结构再insert进回收站。 我也没细想。

如果原表发生了结构变更(例如新增或删除了列、批量修改了某些列的枚举值...),回收站的数据要如何同步?如果放弃同步表结构,以后恢复数据如何处理?

前面回复了

对于数据量较大的表(千万+),如何解决物理删除可能带来的空洞问题? (注:该问题仅针对 mysql 。我司曾有一些频繁读写的表,每天千万级删除+写入,数据空洞十分头疼……)

对于每次写删千万这种特殊需求,就不该用@Trash,而应该自己实现备份逻辑。如果这个表不需要唯一索引,也不需要和其他表做关联查询,其实用原来的@TableLogic 就很好呀

================

写在最后

非常感谢大佬这么用心地回复 回收站这个场景还是有的。这个回收站新插件是否一定要考虑解决所有的场景?我觉得不是。 它不必是@TableLogic的替代品,它可以是@TableLogic的补充品。

另外,我觉得这个插件更大的价值也是在于抛砖引玉。 逻辑删除不是只有在表内加deleted这一种方法。 希望可以深入讨论,让思考碰撞出火花

jinceon avatar Apr 19 '22 11:04 jinceon

声明:本条仅作抛砖引玉之讨论,仅供参考。本人对该功能持中立态度。

我觉得这个需求是不是还欠讨论…

必要性

1、唯一索引无法使用。 2、大部分读操作(如果只是单表,自然没有问题;但对复杂报表、多表关联极其不友好)都需要过滤掉处于删除状态的记录。 3、开发人员无脑乱用。业务上的冻结、失效,本就应该和逻辑删除严格区分,不能用逻辑删除的概念去表达冻结、失效。比如人员离职,我们不会把员工删除,应该是要用一个字段表示离职这个状态,但是有些开发人员无脑用逻辑删除。 4、(我目前更多的场景)对于企业系统来说, 已经审批通过的记录,一般就不会提供删除功能。未审批的,草稿状态,都是人工录进去的,删掉了的后果也就是重新录入一次。

我觉得前两条是真实存在的痛点,但是后面两个嘛……更多属于业务和代码规范层面的东西。因为部分开发人员的滥用(实际上是偷懒)就砍掉功能或者大改内部实现,会不会有点奇怪 😁

安全性

按我个人的经验和理解,逻辑删除的备份属性有两层:

  1. 归档,即证明某些数据历史上产生过,可在必要的时候追溯到;
  2. 防丢失,即在因为业务或技术原因误删除后可以找回数据。

如果采用楼主说的主动式回收站(即显式指定哪些数据可被回收),很显然功能 1 可以被保留,但功能 2 却可能部分或完全丧失,因为开发过程中可能_无法预见_哪些数据会被误删除。那么如果某些数据因此永久丢失了,逻辑删除的意义也就不存在了,数据安全很成问题。

当然我们可以通过全量备份(即所有数据都进回收站)的方式避免这种风险,但这样显式指定的功能就完全退化了,而且原本一条 update 语句能完成的必须通过两条才能完成。

鲁棒性

  • 如何处理”复制数据“和”删除数据“两部分的事务原子性?两步骤中的任何一步失败后如何交回业务层(例如抛异常还是返回 false)?
  • 进一步地,如何实现批量的删除?
  • 如果原表发生了结构变更(例如新增或删除了列、批量修改了某些列的枚举值...),回收站的数据要如何同步?如果放弃同步表结构,以后恢复数据如何处理?
  • 对于数据量较大的表(千万+),如何解决物理删除可能带来的空洞问题? (注:该问题仅针对 mysql 。我司曾有一些频繁读写的表,每天千万级删除+写入,数据空洞十分头疼……)

老哥的观点非常有用,我们应该对“已用逻辑删除特性的数据”一视同仁,无论是低到单体数据表的无关联服务功能,还是较复杂的多元关联数据功能。

我也是在寻觅这个框架如何彻底删除逻辑删除数据的用户之一。。。

但经过老哥的提点,也意识到逻辑删除能够被物理删除的话就意味着增加了无数安全性和稳定性的巨量丢失。如果在“框架作者”的角度尽可能的保障所有开发人员(一视同仁again)的程序是尽可能稳定可靠的。 —— 好,说到这里。看上去很通顺,其实也是伪需求。因为即便是不用逻辑删除,直接物理删除一些有关联数据的业务也会出现用selectOne不到、关联查询出现报错而需要捕获并加以处理的!!!(3个叹号来强调)这就是开发业务需要处理的工作了。

有forceDelete系列Iservice方法会比较好。

54853315 avatar May 14 '22 05:05 54853315

逻辑删除最大的问题就是唯一索引冲突,而唯一索引是实际开发中价值高于逻辑冲突的一项方案,不能因小失大

yujunchengg avatar Jun 02 '22 09:06 yujunchengg

逻辑删除的唯一索引冲突, 可以把 deleted的 类型改成 datetime 来规避掉 , 每次删除都把当前时间设到 deleted deleted 有值时表示已删除, deleted 为null时表示未删除

chenxiaolei avatar Jul 15 '22 11:07 chenxiaolei

一个实现方案设计。 1,项目启动时,当检测到entity上有@Trash 时,自动去数据库新建一个trigger,自动建一个备份表。 2,如果备份表已经存在,扫描并对比表结构,如果原始表和备份表结构不一样,自动计算一个alter table语句更新备份表的表结构。 3,监听delete事件,将删除的数据插入备份表

因为这个方案和mybatis无关了,结帖

jinceon avatar Aug 25 '22 16:08 jinceon

方案弊端1,MySQL trigger是基于行,for each row,大量删除时可能有性能问题

jinceon avatar Aug 25 '22 16:08 jinceon

想了另一个方案:视图+逻辑删除 代替 物理表。 假设有个user entity,映射的表名为 user,加了 trash注解。启动的时候,自动将表重命名为 t_user,然后新建一个视图 user。 其中视图 user的逻辑是 select * from t_user where deleted=0 在操作表的时候,就拿这个视图当物理表来用。 PS:简单的单表视图是可以直接操作 insert update delete 的(没有一些group等特殊过滤的、具体限制这里不列出来)

jinceon avatar Aug 26 '22 01:08 jinceon

我在业务中想实现回收站,可以使用逻辑删除吗?还是说逻辑删除字段不参与任何业务,我需要新建一个字段来表示是否删除到回收站?

szluyu99 avatar Oct 15 '22 12:10 szluyu99