REST API中的软删除

图片

为了使用户不会因无法挽回的数据丢失而感到痛苦,值得考虑进行软删除。 使用软删除时,实际上不会从数据库中删除记录,而只会将其标记为已删除。 通过重置标志,可以轻松恢复数据。

我最近在我们的一种REST服务中实现了软删除。 那些对我所做的事情感兴趣的人,我邀请你来养猫。

必填项


关于是否使用轻度去除的争论非常古老。 只要看看这里这里的长长的味道。

最合理的是根据情况决定一切的立场。 在某些情况下,软删除是方便的,甚至是必要的;在某些情况下,软删除的反对者的论点值得关注。 顺便说一句,反对软删除的一个重要论点是2018年的答案:如果我们谈论的是用户帐户,则软删除与GDPR相反。

我们决定在用于文件存储的服务中,必须进行软删除。

RESTful方法


如果要在服务中实现软删除,则需要从接口的角度了解它的外观。 在互联网上进行的搜索表明,人们通常会遇到的一个问题是,是否像以前一样使用DELETE {resource},还是将PATCH方法改为包含{status:'deleted'}之类的主体, 。

在这里,人们的意见是明确的:必须像以前一样使用DELETE。 从客户的角度来看,在非洲删除也是删除。 一切都不应改变:如果删除了资源,则无法访问; 如果客户端要删除资源,则他知道HTTP DELETE方法就是为此目的。 不必将客户端专用于服务如何实现删除的确切细节。

但是除此之外,我还担心如何恢复已删除的资源。 当然,通过管理数据库可以解决此问题。 但是,我希望能够通过REST API做到这一点。 在这里,我们陷入了冲突。 事实证明,客户仍然需要专注于实现细节吗?

长时间的搜索没有任何结果,直到我看到了Dan Yoder的一篇好文章 。 本文研究了不同HTTP请求的语义,并建议将远程资源移到存档中 ,而不是物理删除。 此外,如果DELETE返回指向已归档资源的链接,那就很好了。 用户始终可以通过向归档文件发送POST请求来恢复已删除的资源。

设计方案


我们的REST服务使用实体框架基于ASP.NET Web API构建。 正如我所说,我对称为文档的资源进行了软删除。

因此,首先需要将列添加到相应的表中。 作为标志,我使用了一个名为Deleted的时间戳。 如果该值不为NULL,则认为资源已删除。 此外,了解有关谁删除了资源的信息也很有用。

ALTER TABLE Documents ADD Deleted datetime NULL, DeletedBy int NULL GO 

现在,控制器中的DELETE操作将简单地设置这些字段的值,而不是物理地删除记录。 此外,DELETE将返回带有对归档文档的标准引用的正文:

 { "links": { "archive": "documents/{id}/deleted" } } 

实际上,这是很重要的一点:该链接可帮助客户理解文档不是被删除而是被移动了

存档文件的新控制器应提供以下方法:
GET文件/删除获取所有已删除文档的集合
GET文件/ {id} /删除返回删除的文档
POST文件/ {id} /已删除恢复已删除的文档;
不需要身体; 返回201已创建
删除文件/ {id} /删除物理删除文档

实作


最初,我计划向数据库添加两个视图:

 CREATE VIEW DeletedDocuments AS SELECT * FROM Documents WHERE Deleted IS NOT NULL GO CREATE VIEW AvailableDocuments AS SELECT * FROM Documents WHERE Deleted IS NULL GO 

在我看来,这将减少麻烦:我无需在代码中设置条件,而只是在数据库上下文中获得了两个不同的DbSet属性。 的确,模型中必须有两个相同的实体,但这就是EF上下文中POCO对象的属性-每个表仅对应一个实体。

顺便说一下,SQL中的视图在其他方面对实体框架很有用:例如,在它们的帮助下,如果您不想创建多个数据库上下文,则可以引用另一个数据库中的表。

但是,就我而言,带有视图的数字没有通过。 授权期间,您需要使用所有文档,因为用户对已删除文档的权限与现有文档相同。

因此,我决定在DbContext中仅包含一个DbSet文档,并且每次我确定目前到底需要什么时,都在代码中:

 var availableDocuments = DbContext.Documents.Where(d => d.Deleted == null); var deletedDocuments = DbContext.Documents.Where(d => d.Deleted != null); var allDocuments = DbContext.Documents; 

相关资源


文档是与其他资源关联的资源。 例如,我们有一个文档别名。 也就是说,您不仅可以通过documents / {id}路径获取文档,还可以通过documents / {alias}路径获取文档 ,其中alias是唯一字符串。

删除文档后,与之关联的所有别名都应变为“不可见”:如果之前客户端使用GET文档/别名收到了所有别名的列表,则在删除文档后,列表中的别名也将消失。

但是它们仍然保留在数据库中! 我们希望提供以删除状态还原文档的功能。 这可能会使客户端感到困惑:他试图为另一个文档添加新的别名,从GET文档/别名返回的列表不包含这样的行,并且服务仍然拒绝添加它。

我认为这不是一个严重的问题。 不过,如果需要解决,可以添加端点GET document / Deleted / aliases 。 然后一切都准备就绪:该服务无法添加别名,因为远程文档已经使用了该值。

可能会出现问题:是否值得从文档/别名返回的列表中添加别名 ? 让他们留下来! 我认为这样的决定是不正确的。 然后,事实证明别名列表将包含断开的链接,因为如果客户端尝试通过别名获取已删除的文档,则该服务将返回404 Not Found。 如果涉及与文档关联的子资源,则其行为应与我们实际删除文档时的行为完全相同。

档案清理


软删除除了能够轻松恢复数据外,还具有其他一些优点。 关系数据库中的删除操作是一项昂贵的操作。 而且,即使删除一条记录也导致级联删除其他表中的记录,那么这就会充满死锁。 因此,软删除比物理删除更快,更可靠。

但是有一个明显的缺点。 基础开始增长。

因此,在最后阶段,您应该注意自动清理存档。 当然,您可以手动清洁底座,但是最好使此过程自动化。 如果我们有针对性地设置远程对象的到期日期(例如30天),则客户端可以显示归档页面,在该页面上将突出显示其寿命将尽的元素。

我的手还没有完成这项任务。 我们计划将一个任务添加到我们的任务系统,该任务每天运行一次简单的SQL查询,该查询将从存档中删除所有污秽对象。 作为参数,任务应采用到期日期。 必须确保此参数的当前值存储在一个地方的某个地方。 这样就有可能在服务中实现将这个值返回给客户端的方法。

Source: https://habr.com/ru/post/zh-CN440886/


All Articles