软删除是SaaS产品的常见要求。但是当前的Prisma解决方案存在某些问题。让我们看看Zenstack如何解决它。
什么是软删除
通常,当您在数据库中运行删除语句时,数据就消失了。 Soft Delete不是永久删除记录,而是更新记录中的列,通常命名为“ DELETED_AT”或“ IS_DELETED”,以指示记录已标记为删除。
软删除的优点
在过去6年中,软删除在建造商业SaaS产品时几乎是我必须的。我会根据我自己的经验向您展示为什么。
可恢复性
我敢打赌,您曾经后悔删除某些东西并试图恢复它,因为人类犯了一个错误。这就是为什么包括操作系统在内的许多应用程序也通过引入回收/垃圾箱来采用软删除。
作为服务提供商,甚至提供存档功能,并在用户从数据库中删除记录之前,向用户显示无数的警告和麻烦:
悲剧仍然发生。您可以说我已经做到的一切可以安慰自己。但是,当客户来找您几乎哭泣以寻求帮助时,您如何忍受他拒绝?
根据Muphy的定律:
任何可能出错的事情都会出错。
因此,最有效的方法是避免授予用户完全从数据库中删除数据的机会,从而完全进行软删除。
数据的完整性
您可以看到这是经常提出的。我认为这是从数据库的角度到主要参考完整性。从实际的角度来看,利益仍然用于恢复。
例如,假设您有两个表用户并在数据库中发布:
如果用户意外地删除了用户,则该用户的所有帖子都被删除了以供级联反应,要么成为孤立的setNull。即使您可以找到一种恢复用户的方法,帖子也消失了。
分析
无论您是产品主导还是以市场为主导的增长模型,您的产品数据分析是如今的基本业务。
让您说您正在构建类似概念的产品。如果您想知道上个月用户实际创建了多少页,则可以轻松地写下SQL:
SELECT COUNT(*) FROM pages
WHERE created_at >= DATE_TRUNC('month', CURRENT_DATE - INTERVAL '1' MONTH)
AND created_at < DATE_TRUNC('month', CURRENT_DATE)
等待一分钟,它包括用户创建然后删除的页面吗?
有些人建议应该通过记录来完成这种要求。问题就像您永远不知道您将满足什么样的分析要求。如果第二天,产品经理希望看到上个月创建了多少个包含表或视频的帖子怎么办?您可能最终会记录数据库中出现的所有内容以满足要求。即使您也可以这样做,分析过程也变得更加复杂,因为您必须从数据库和记录系统中收集和合并数据。
诊断
非一致的复制错误始终是最难处理的错误。有几次用户发送视频或屏幕快照,说发生了一些奇怪的事情。当您想调查更多时,您会发现用户删除了怪异的数据。这是可以理解的,因为没人愿意在那里留下一些缺陷。
幸运的是,当我们使用软删除时,我们仍然可以找到数据以重建犯罪现场以最终在代码中找到错误。
缺点
人们不使用软删除的最重要原因是实施挑战。除了查询更新外,还有其他需要考虑的问题,例如索引,独特的约束等,这绝对不是一件容易的事,并且肯定会增加系统的复杂性。
软删除在Prisma中
好处是,更少的人直接在SQL上工作了,大多数ORM都有解决方案。
您可以看到Prisma如何使用中间件执行以下软删除:
Middleware sample: soft delete (Reference)
尽管页面很长,但解决方案非常简单。它主要做两件事:
-
对于
delete
操作,将其更改为update
操作
if (params.action == 'delete') { // Delete queries // Change action to an update params.action = 'update' params.args['data'] = { deleted: true } } if (params.action == 'deleteMany') { // Delete many queries params.action = 'updateMany' if (params.args.data != undefined) { params.args.data['deleted'] = true } else { params.args['data'] = { deleted: true } } } }
-
对于
find
操作,添加一个过滤器以滤除软删除的记录:
if (params.action === 'findUnique' || params.action === 'findFirst') { // Change to findFirst - you cannot filter // by anything except ID / unique with findUnique params.action = 'findFirst'; // Add 'deleted' filter // ID filter maintained params.args.where['deleted'] = false; } if (params.action === 'findMany') { // Find many queries if (params.args.where) { if (params.args.where.deleted == undefined) { // Exclude deleted records if they have not been explicitly requested params.args.where['deleted'] = false; } } else { params.args['where'] = { deleted: false }; } }
但是,这种方法存在一些问题,我认为这也是GitHub中问题仍然开放的原因:
Soft deletes (e.g. deleted_at) #3398
将这种功能添加到Core可能很不错,因此您可以在不混乱应用程序查询
的情况下获得过滤的视图打开问题:
- 将此过滤器添加到所有查询中会很慢吗?
- 我们只有在需要时才能做吗?
- 可以在光子中更好地处理?
tldr,最大的问题是,当滤波器中的关系字段涉及时,它将不足。有两种主要情况:
-
可以处理
include
const user = await prisma.user.findMany({ where: { id: 1, }, include: { posts: true, }, });
删除的帖子也将包括在结果中。
-
可以处理关系过滤器
const us1 = await prisma.user.findMany({ where: { posts: { some: { title: { contains: 'Prisma' }, }, }, }, });
如果仅使用用户的删除帖子标题包含``Prismaâ'',则该用户也将返回。
您可能认为,通过添加已删除的过滤器,似乎很难为这两者修复它。可能是上面的示例,但是该示例怎么样:
const user = await prisma.user.findMany({
where: {
posts: {
every: {
title: { contains: 'Prisma' },
},
},
},
});
为什么不考虑如何在阅读之前添加过滤器? ðÖ
您必须将其更改为以下查询:
const user = await prisma.user.findMany({
where: {
posts: {
none: {
AND: [
{ deleted: true },
{ title: { not: { contains: 'Prisma' } }}
],
},
},
},
});
Prisma提供了一个非常灵活的过滤器,其中包括AND
,OR
,NOT
,some
,some
,every
,none
等几个运算符,这也需要大量努力来单独考虑每个人。我想这可能是为什么问题尚未解决的原因。 ð
Zenstack中的软删除
ZenStackâ是一种工具包,它可以用强大的访问控制层增强Prisma并释放其充分的全堆栈开发潜力。
作为Prisma的增强,在支持自定义属性问题之后: