软删除:Zenstack中Prisma和解决方案中的实现问题
#网络开发人员 #生产率 #database #prisma

软删除是SaaS产品的常见要求。但是当前的Prisma解决方案存在某些问题。让我们看看Zenstack如何解决它。

什么是软删除

通常,当您在数据库中运行删除语句时,数据就消失了。 Soft Delete不是永久删除记录,而是更新记录中的列,通常命名为“ DELETED_AT”或“ IS_DELETED”,以指示记录已标记为删除。

软删除的优点

在过去6年中,

软删除在建造商业SaaS产品时几乎是我必须的。我会根据我自己的经验向您展示为什么。

可恢复性

我敢打赌,您曾经后悔删除某些东西并试图恢复它,因为人类犯了一个错误。这就是为什么包括操作系统在内的许多应用程序也通过引入回收/垃圾箱来采用软删除。

作为服务提供商,甚至提供存档功能,并在用户从数据库中删除记录之前,向用户显示无数的警告和麻烦:

warning

悲剧仍然发生。您可以说我已经做到的一切可以安慰自己。但是,当客户来找您几乎哭泣以寻求帮助时,您如何忍受他拒绝?

根据Muphy的定律:

任何可能出错的事情都会出错。

因此,最有效的方法是避免授予用户完全从数据库中删除数据的机会,从而完全进行软删除

数据的完整性

您可以看到这是经常提出的。我认为这是从数据库的角度到主要参考完整性。从实际的角度来看,利益仍然用于恢复。

例如,假设您有两个表用户并在数据库中发布:

post-user-model

如果用户意外地删除了用户,则该用户的所有帖子都被删除了以供级联反应,要么成为孤立的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)

尽管页面很长,但解决方案非常简单。它主要做两件事:

  1. 对于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 }
            }
          }
    }
    
    
  2. 对于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,最大的问题是,当滤波器中的关系字段涉及时,它将不足。有两种主要情况:

  1. 可以处理include

    const user = await prisma.user.findMany({
        where: {
            id: 1,
        },
        include: {
            posts: true,
        },
    });
    

    删除的帖子也将包括在结果中。

  2. 可以处理关系过滤器

    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提供了一个非常灵活的过滤器,其中包括ANDORNOTsomesomeeverynone等几个运算符,这也需要大量努力来单独考虑每个人。我想这可能是为什么问题尚未解决的原因。 ð

Zenstack中的软删除

ZenStackâ是一种工具包,它可以用强大的访问控制层增强Prisma并释放其充分的全堆栈开发潜力。

作为Prisma的增强,在支持自定义属性问题之后:

对我们来说,这看起来像是一个很好的下一步。对于delete操作,我认为您仍然可以使用上面的中间件方法或新的客户端扩展名来实现它。那应该不是问题。

对于最复杂的查询逻辑,由于Zenstack支持访问控制策略,因此自然而然地支持它。因为从技术上过滤出来的软删除记录也是一种访问策略。

因此,对于任何模型,如果要进行软删除,则只需添加以下访问策略:

model Post {
  ...
  deleted Boolean @default(false) @omit
  @@deny(read, deleted)
  ...
}

然后,当使用Zenstack进行查询时,它将自动排除软删除的记录。如果您还要包含软删除的记录,则可以简单地使用原始Prisma客户端。

说实话,我们不知道Zenstack在客户在我们的Discord平台上报告问题之前,都有支持它的问题。解决这个问题后,我们深入了解了它的复杂性以及为什么Prisma在现有的代码和使用情况下不支持它。幸运的是,由于Zenstack的遗产较少,我们能够快速解决该问题。

相反,我们的现实情况较少。因此,如果你们遇到了我们错过的任何案件,那么如果您可以在Github中创建一个问题或将其直接扔给我们的Discord,我们将不胜感激。让我们一起使ZenStack更好!