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形成多对多的关系。- A
User在空间中可以扮演以下两种角色之一:MEMBER或ADMIN。 Users 可以在s中创建Posts。A有一个状态,指示它是否对所有人可见。SpacePostpublishedPost如果 A 被发布,则所有人都可以阅读它;而对于它的作者和空间所有者/管理员来说,它始终是可读的。
比较
模式
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',
}),
});
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?
}
那么,哪个更好呢?它们的功能完全相同,但 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;
CRUD 操作
Drizzle 和 Prisma 都提供了完全类型的数据库客户端 API。然而,它们的理念却截然不同。
Drizzle 的定位更像是“SQL 查询构建器”。它的查询语法直接模仿了 SQL 查询的编写方式,当然,它还拥有流畅的 API、静态类型检查、IDE 自动补全等优势。这意味着,要充分利用 Drizzle,你需要对 SQL 有深入的了解,并且能够自如地“用 SQL 的思维方式思考”。下面我将用一个例子来解释我的意思。
如“需求”部分所述,要使列表Post对当前用户可读,我们需要找到符合以下任一条件的项:
- 它已发布,当前用户是其所属空间的成员。
- 当前用户即为作者
- 当前用户是该空间的拥有者。
- 当前用户是其所属空间的管理员。
在 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')
)
);
相比之下,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'
}
}
}
}
]
}
});
我更喜欢 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()])
}
Post这样一来,向当前用户列出 s 的查询就可以大大简化:
db.post.findMany({
include: { author: true },
where: {
space: { slug: req.params.slug },
},
});
我们会开发一个基于 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

