最近几天有点忙,所以我们今天来一篇短的,简单地介绍一下数据库设计中的一种模式——Soft Delete。
可以说,该模式毁誉参半,甚至有非常多的人认为该模式是一个Anti-Pattern。因此在本篇文章中,我们不仅仅会对该模式进行介绍,同时也会列出该模式可能导致的一系列问题,以帮助大家正确地决定是否使用该模式。
Soft Delete简介
首先先来想一个需求,那就是对用户操作的回滚支持。例如我现在正在用Word编写这篇文章。当我执行了一个错误操作的时候,我仅仅需要键入Ctrl + Z就可以进行回滚。而在有些Web应用中,我们同样需要这种功能。
例如Rally是一个Web应用,以用来在软件开发过程中对进度和任务进行管理。在任务管理功能中,每次对任务的创建,修改以及删除都会被记录在系统中。
现在问题来了,如果需要支持回滚,那么系统该如何记录一条已经被用户删除了的任务呢?最直观的想法就是在数据库中添加一列deleted来记录该任务是否已经被删除:
1 @Entity 2 class Task extends … { 3 private boolean deleted; 4 …… 5 private boolean isDeleted() { 6 return deleted; 7 } 8 9 private void setDeleted(boolean deleted) { 10 this.deleted = deleted; 11 } 12 }
如果一个任务被删除了,那么它的deleted将为true。也就是说,在用户删除一个任务的时候,系统实际上并没有将该任务彻底地从数据库中删除,而仅仅是通过deleted来标示其已经被删除了。而在恢复该任务的时候,只需要将deleted设置为false即可。
OK,这就是有关Soft Delete模式的介绍。是不是很简单?但也正是因为它非常简单,进而导致了对它的滥用,从而使它成为了一个很多人眼中的Anti-Pattern。这种事情在IT技术中发生的还真是不少。最简单的就是Java的Checked Exception。的确它是一个好的功能,让使用Java编程变得更加严谨。但是过分的滥用导致很多类库都将用户完全无法处理的异常暴露在了类库接口中,反而使很多软件开发人员养成了直接忽略所有异常的坏习惯:
1 try { 2 obj.someFunction(); 3 } catch (Throwable e) { 4 }
相信读者已经看出了这么做的危害:catch甚至将表示系统错误的Error类型实例都抓住了。但这里不能忽略的一个事实是,当软件开发人员对某些行为无能为力,那么他极有可能忽略某些编码准则,而首先选择使用一种能让系统在正确运行的情况下工作起来的方法。例如对于上面的函数调用obj.someFunction(),如果其抛出的异常和类库内部运行逻辑相关,而且每次都可能导致这种问题,那么软件开发人员就极有可能使用上面的代码忽略掉该异常。这种问题甚至在一些广为使用的类库中存在着。例如OData4j曾经把取得OData元数据时产生的所有异常都当作是目标服务没有暴露元数据的情况来处理。
OK,说得有点远了。总结起来就是,一旦一个技术过于简单而且能够处理某个情况,那么软件开发人员将不会仔细研究使用该技术所需要的语境,从而导致滥用。和Checked Exception一样,Soft Delete也是这样的一个例子。
Soft Delete的问题
那么该数据库模式有什么问题呢?简单地说,那就是太容易出错,而且是隐蔽的错误。试想一下,如果用户需要列出所有的任务,那么在SQL语句中就需要使用WHERE deleted = ‘N’这样的条件。而且该条件几乎在所有处理任务的SQL语句中都要出现。一旦在一个SQL语句中忘记了该条件,那么这极有可能是一个Bug,而且这种Bug有时候还非常隐蔽。例如如果在一个COUNT语句中忘记标示了该条件,而且系统中任务很多,但是被删除的任务很少,那么该Bug可能存在几年都不会被发现。
同时如果一旦决定需要在系统中大量地使用Soft Delete,那么SQL将变得非常混杂。在统计功能中,我们可能需要筛选出所有包含任务的用例的个数,甚至是包含这些用例的项目的个数,那么我们就需要在SQL中同时标明多个WHERE deleted = ‘N’的条件:
1 SELECT COUNT(*) 2 FROM project, story, task 3 WHERE … project.deleted = ‘N’ AND story.deleted = ‘N’ AND task.deleted = ‘N’
那么在调试这些语句的时候,或者查看这些语句的执行计划以进行性能调优的时候,软件开发人员都会发现由于这些条件的引入导致SQL的执行变得非常复杂。
还有一个问题就是,如果一个系统常常执行对记录的软删除,那么数据库中所记录的数据将比实际所需要记录的数据多得多。这种垃圾数据可能会导致数据库的索引变得很大,甚至可能会由此而严重影响数据库的性能。
另一个问题则和级联有关。数据库提供了级联操作,在删除一个数据记录的时候,数据库会根据该数据与其它记录之间的关联关系来自动完成对其它关联记录的操作。这也是数据库保持其数据完整性的一种方法。但是一旦用户使用了Soft Delete,那么在对其进行软删除的时候就不会将其从数据库中移除,那么与其关联的那些记录也便不会被数据库移除。也就是说,软件开发人员需要自行完成数据完整性的管理。除此之外,软件开发人员还需要在数据访问层(DAL,Data Access Layer)中完成事务的组织,并且一旦数据库表的定义发生了变化,这些事务组织的逻辑也可能需要进行更改。
Soft Delete的实现
也正是由于Soft Delete拥有这么多的问题,因此软件开发人员们提出了很多Soft Delete的变通实现方法,大大地减少了开发和维护Soft Delete模式数据的成本。
一种方法就是利用数据库所提供的View功能。在该方法中,我们需要在数据库中创建一个View,以显示表中deleted值为’N’的各行数据。而在对数据进行操作的各SQL语句中,我们只需要直接操作该View,从而避免了每次都需要在SQL语句中标明WHERE deleted = ‘N’这种条件。
而另一种方法则是将这些数据分散到两个不同的表中。这两个表中的一个表记录deleted值为’N’的各行数据,而另一个表则记录已经被软删除的deleted值为’Y’的各行数据。而且在具有两个表的情况下,系统甚至可以很较为容易地实现垃圾箱的功能。
而在某些情况下,我们也可以在数据库设计中借鉴Soft Delete的思路。Soft Delete需要用户自行管理数据库中数据的关联关系。这是一份额外的工作,但也带来了更多的灵活性。
Rally中的任务删除操作的回滚自然不必多说,垃圾箱功能的添加也将变为非常容易的事。而对于某些自定义的删除逻辑,Soft Delete所带来的灵活性将更为突出。例如在Rally中,如果我们需要实现“删除用户用例时如果用户用例中包含任务,那么这些任务将挪至父用例中”这样一个需求,那么我们就可以在Soft Delete的自定义删除逻辑中完成该功能。
好了,今天就到这里。