发布于 2026-01-06 0 阅读
0

Drizzle 还是 Prisma?我开发了两次应用程序,以找出哪个更好。

Drizzle 还是 Prisma?我开发了两次应用程序,以找出哪个更好。

对于 TypeScript 爱好者来说,Prisma长期以来一直是构建以数据库为中心的应用程序的完美解决方案。但最近,一个新的挑战者出现了。如果你一直密切关注 ORM 领域,你可能听说过Drizzle,这款新型 ORM 因其灵活性、高性能和更佳的用户体验而广受欢迎。在本文中,我将对它们进行比较。遵循“展示而非讲述”的原则,我将分别使用 Drizzle 和 Prisma 构建相同的 API 两次。

免责声明:我是ZenStack的作者,ZenStack 是一个基于 Prisma 构建的全栈工具包。我对 Prisma 的经验比 Drizzle 更丰富,但我会尽力做到客观公正。

要求

我不想让 API 变成另一个“Hello World”示例,因为那样无法产生任何有价值的信息。但是,使用过于复杂的 API 也很难在一篇博客文章中完整呈现。因此,我决定采用一个简单的“博客文章”场景,但加入“多租户”的概念。

以下是具体要求:

  • Space构成租赁边界。ASpace包含一个列表Post
  • Users 是全球性的,可以由空间管理员邀请加入Spaces。也就是说,Space它们之间User形成多对多的关系。
  • AUser在空间中可以扮演以下两种角色之一:MEMBERADMIN
  • Users 可以在s中创建Posts。A有一个状态,指示它是否对所有人可见。SpacePostpublished
  • Post如果 A 被发布,则所有人都可以阅读它;而对于它的作者和空间所有者/管理员来说,它始终是可读的。

ER图

比较

模式

Drizzle 和 Prisma 最显著的区别在于模式的定义方式。Drizzle 完全基于 TypeScript。如果您了解 TypeScript,您就知道如何使用 Drizzle,无需其他任何知识。它的模式构建器 API 允许您描述表、关系和约束的各个方面。以下是我们 API 的模式示例:

// Drizzle schema

export const spaceUserRoleEnum = pgEnum('SpaceUserRole', ['MEMBER', 'ADMIN']);

// User table
export const users = pgTable(
    'users',
    {
        id: serial('id').primaryKey(),
        email: varchar('email', { length: 256 }).notNull(),
    },
    (users) => {
        return {
            emailIndex: uniqueIndex('email_idx').on(users.email),
        };
    }
);

// Space table
export const spaces = pgTable(
    'spaces',
    {
        id: serial('id').primaryKey(),
        slug: varchar('slug', { length: 8 }).notNull(),
        name: varchar('name', { length: 256 }).notNull(),
        ownerId: integer('ownerId').references(() => users.id, {
            onDelete: 'cascade',
        }),
    },
    (spaces) => {
        return {
            slugIndex: uniqueIndex('slug_idx').on(spaces.slug),
        };
    }
);

// Space <-> User join table
export const spaceUsers = pgTable(
    'spaceUsers',
    {
        id: serial('id').primaryKey(),
        spaceId: integer('spaceId').references(() => spaces.id, {
            onDelete: 'cascade',
        }),
        userId: integer('userId').references(() => users.id, {
            onDelete: 'cascade',
        }),
        role: spaceUserRoleEnum('role').notNull().default('MEMBER'),
    },
    (spaceUsers) => {
        return {
            uniqueSpaceUser: uniqueIndex('space_user_idx').on(
                spaceUsers.spaceId,
                spaceUsers.userId
            ),
        };
    }
);

// Post table
export const posts = pgTable('posts', {
    id: serial('id').primaryKey(),
    title: varchar('title', { length: 256 }).notNull(),
    published: boolean('published').default(false),
    spaceId: integer('spaceId').references(() => spaces.id, {
        onDelete: 'cascade',
    }),
    authorId: integer('authorId').references(() => users.id, {
        onDelete: 'cascade',
    }),
});

Enter fullscreen mode Exit fullscreen mode

Prisma 的模式构建方式完全不同。它使用领域特定语言 (DSL) 来完成这项工作。您需要学习其语法,但它相当直观,容易上手。以下是 Prisma 版本的模式示例:

// Prisma schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id              Int         @id @default(autoincrement())
  email           String      @unique
  posts           Post[]
  spaceMembership SpaceUser[]
  ownedSpaces     Space[]
}

model Space {
  id      Int         @id @default(autoincrement())
  owner   User        @relation(fields: [ownerId], references: [id], onDelete: Cascade)
  ownerId Int
  name    String
  slug    String      @unique
  posts   Post[]
  members SpaceUser[]
}

enum SpaceUserRole {
  MEMBER
  ADMIN
}

model SpaceUser {
  id      Int           @id @default(autoincrement())
  space   Space         @relation(fields: [spaceId], references: [id], onDelete: Cascade)
  spaceId Int
  user    User          @relation(fields: [userId], references: [id], onDelete: Cascade)
  userId  Int
  role    SpaceUserRole @default(MEMBER)

  @@unique([spaceId, userId])
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  published Boolean @default(false)
  author    User?   @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId  Int?
  space     Space?  @relation(fields: [spaceId], references: [id], onDelete: Cascade)
  spaceId   Int?
}

Enter fullscreen mode Exit fullscreen mode

那么,哪个更好呢?它们的功能完全相同,但 Prisma 的模式更简洁、更清晰,因此更易于阅读。这就是使用自定义语言的优势所在,当然,学习成本也会更高。

Drizzle 的方法,由于它只是 TypeScript 代码,确实提供了更大的灵活性。例如,你可以在模式中使用条件分支,并使用函数来提取可重用的代码块。但我很好奇这些方法在实践中是否经常使用。

迭代速度

就本地开发的反馈循环速度而言,Drizzle 显然更胜一筹。它的 API 类型完全由 TypeScript 的类型推断驱动,无需代码生成。您在模式端所做的更改会立即反映在数据库客户端 API 端。

相比之下,Prisma 的工作流程略显笨拙。prisma generate每次更改 schema 文件后,都必须重新生成客户端代码。虽然速度很快,但仍然会增加开发人员的工作量,而且如果忘记执行此操作,很容易造成混乱。此外,当批量覆盖文件时,IDE 的语言服务器也容易出现延迟。

这又是使用DSL需要付出的另一个代价。

移民

迁移是指生成并重放一组数据库模式更改,使数据库达到新状态的过程。

在不断改进 API 架构的过程中,我分别使用 Drizzle 和 Prisma 进行了多次迁移。两者的使用体验基本相同,但我更欣赏 Drizzle 对重命名列的处理方式,这长期以来一直是 Prisma 用户的痛点。当 Drizzle 检测到可能需要重命名列时,它会友好地进入交互模式,让您选择重命名意图:

毛毛雨迁移

相反,Prisma 会简单地删除旧列并创建一个新列。如果您未能注意到并进行必要的手动更改,则可能导致灾难性后果——这是长期存在的未解决的可用性问题之一。

-- AlterTable
ALTER TABLE
  "Post" DROP COLUMN "userId",
ADD
  COLUMN "ownerId" INTEGER;
Enter fullscreen mode Exit fullscreen mode

CRUD 操作

Drizzle 和 Prisma 都提供了完全类型的数据库客户端 API。然而,它们的理念却截然不同。

Drizzle 的定位更像是“SQL 查询构建器”。它的查询语法直接模仿了 SQL 查询的编写方式,当然,它还拥有流畅的 API、静态类型检查、IDE 自动补全等优势。这意味着,要充分利用 Drizzle,你需要对 SQL 有深入的了解,并且能够自如地“用 SQL 的思维方式思考”。下面我将用一个例子来解释我的意思。

如“需求”部分所述,要使列表Post对当前用户可读,我们需要找到符合以下任一条件的项:

  1. 它已发布,当前用户是其所属空间的成员。
  2. 当前用户即为作者
  3. 当前用户是该空间的拥有者。
  4. 当前用户是其所属空间的管理员。

在 Drizzle 中,这需要以下代码(一些不太简单的 SQL 构造!):

// Using Drizzle to find the list of Posts for current user

db
    .selectDistinctOn([posts.id], {
        id: posts.id,
        title: posts.title,
        published: posts.published,
        author: { id: users.id, email: users.email },
    })
    .from(posts)
    .where(eq(posts.spaceId, space.id))
    .leftJoin(users, eq(posts.authorId, users.id))
    .leftJoin(spaces, eq(posts.spaceId, spaces.id))
    .leftJoin(
        spaceUsers,
        and(
            eq(spaceUsers.spaceId, spaces.id),
            eq(spaceUsers.userId, req.uid!)
        )
    )
    .where(
        or(
            // 1. published and current user is a member of the space
            and(
                eq(posts.published, true),
                eq(spaceUsers.userId, req.uid!)
            ),
            // 2. authored by the current user
            eq(posts.authorId, req.uid!),
            // 3. belongs to space owned by the current user
            eq(spaces.ownerId, req.uid!),
            // 4. belongs to space where the current user is an admin
            eq(spaceUsers.role, 'ADMIN')
        )
    );
Enter fullscreen mode Exit fullscreen mode

相比之下,Prisma 的查询语法更“面向对象”,或者更准确地说,更“像图一样”。它为遍历和查询关系提供了更高层次的抽象。同样的查询在 Prisma 中可以这样写:

// Using Prisma to find the list of Posts for current user

prisma.post.findMany({
    include: { author: true },
    where: {
        spaceId: space.id,
        OR: [
            // 1. published and current user is a member of the space
            {
                published: true,
                space: { members: { some: { userId: req.uid! } } },
            },            
            // 2. authored by the current user
            { authorId: req.uid! },
            // 3. belongs to space owned by the current user
            { space: { ownerId: req.uid! } },
            // 4. belongs to space where the current user is an admin
            { 
                space: {
                    members: {
                        some: {
                            userId: req.uid!,
                            role: 'ADMIN' 
                        }
                    }
                }
            }
        ]
    }
});
Enter fullscreen mode Exit fullscreen mode

我更喜欢 Prisma 的方法,因为它省去了思考“连接”机制的精力。查询语句直观地采用自顶向下的结构,并在需要时自然地遍历关系。不可否认,Drizzle 的方法更加灵活,因为它允许你直接控制生成的 SQL 查询语句。此外,由于它保证“每个查询只生成一条 SQL”,因此有时性能会优于 Prisma。但 Prisma 的查询语句编写起来更加流畅易懂。

我们为什么要在 Prisma 之上进行建设?

如前所述,我是ZenStack的作者——ZenStack 是一个工具包,它通过访问控制和自动 CRUD API 及钩子为 Prisma 提供了强大的功能。为什么我们选择在 Prisma 之上构建这些功能呢?

  • Prisma 的模式更“易于静态分析”。

与功能齐全的编程语言 TypeScript 相比,DSL 更容易分析和推理。

  • Prisma的查询语法功能有限。

    Prisma 的查询 API 灵活但不至于过于灵活。它并没有试图完全暴露 SQL 的强大功能,而这种克制恰恰是我们所需要的。我们可以通过向 Prisma 的查询输入对象注入数据来强制执行访问策略,但如果我们必须面对 SQL 的全部灵活性,那么实现起来可能会非常困难。

使用 ZenStack 时,您可以直接在模式中对访问策略进行建模,例如:

model Post {
    id Int @id @default(autoincrement())
    title String
    published Boolean @default(false)
    author User? @relation(fields: [authorId], references: [id], onDelete: Cascade)
    authorId Int?
    space Space? @relation(fields: [spaceId], references: [id], onDelete: Cascade)
    spaceId Int?

    @@allow('all', 
        auth() == author // author has full access
        || space.owner == auth() // space owner has full access
        || space.members?[user == auth() && role == ADMIN]) // space admin has full access

    // published posts can be read by anyone in the space
    @@allow('read', published && space.members?[user == auth()])
}
Enter fullscreen mode Exit fullscreen mode

Post这样一来,向当前用户列出 s 的查询就可以大大简化:

db.post.findMany({
    include: { author: true },
    where: {
        space: { slug: req.params.slug },
    },
});
Enter fullscreen mode Exit fullscreen mode

我们会开发一个基于 Drizzle 的 ZenStack 版本吗?我希望可以,但这需要我们重新思考很多事情。

结论

那么,哪个更好呢?没错,你猜对了:“视情况而定”。随着竞争日益激烈,一些开发者可能会频繁更换平台。但我预测 Drizzle 能够发展到相当可观的市场份额。届时,它们各自将拥有相对稳定的用户群体——因为总有一些开发者偏爱控制、灵活性和透明度,而另一些开发者则更倾向于简洁、易用和节省脑力。

我目前仍然支持 Prisma。我非常欣慰地看到 Drizzle 一直在督促 Prisma 团队修复那些困扰用户多年的长期问题。

源代码

您可以在这里找到这两个实现的源代码:


ZenStack是我们开源的 TypeScript 工具包,旨在帮助您更快、更智能、更高效地构建高质量、可扩展的应用程序。它将数据模型、访问策略和验证规则集中到一个基于 Prisma 的声明式模式中,非常适合 AI 增强型开发。立即开始将ZenStack集成到您现有的技术栈中吧!

文章来源:https://dev.to/zenstack/drizzle-or-prisma-i-built-an-app-twice-to-find-out-which-is-better-1f82