跳到主内容

如何从 Drizzle 迁移到 Prisma ORM

15 分钟

引言

本指南将向您展示如何将应用程序从 Drizzle 迁移到 Prisma ORM。我们将使用一个基于 Drizzle Next.js 示例的示例项目来演示迁移步骤。您可以在 GitHub上找到本指南使用的示例。

您可以在 Prisma ORM 与 Drizzle 对比页面了解 Prisma ORM 与 Drizzle 的对比。

先决条件

在开始本指南之前,请确保您已具备

  • 要迁移的 Drizzle 项目
  • 已安装 Node.js(版本 16 或更高)
  • PostgreSQL 或其他受支持的数据库
  • 对 Drizzle 和 Next.js 有基本的了解
注意

本迁移指南使用 Neon PostgreSQL 作为示例数据库,但也同样适用于 Prisma ORM 支持的任何其他关系型数据库。

您可以在 Prisma ORM 与 Drizzle 对比页面了解 Prisma ORM 与 Drizzle 的对比。

迁移过程概述

请注意,从 Drizzle 迁移到 Prisma ORM 的步骤始终相同,无论您构建何种应用程序或 API 层

  1. 安装 Prisma CLI
  2. 内省数据库
  3. 创建基线迁移
  4. 安装 Prisma Client
  5. 逐步用 Prisma Client 替换您的 Drizzle 查询

这些步骤适用,无论您构建的是 REST API(例如使用 Express、koa 或 NestJS)、GraphQL API(例如使用 Apollo Server、TypeGraphQL 或 Nexus),还是使用 Drizzle 访问数据库的任何其他类型的应用程序。

Prisma ORM 非常适合增量采用。这意味着您不必一次性将整个项目从 Drizzle 迁移到 Prisma ORM,而是可以逐步将数据库查询从 Drizzle 迁移到 Prisma ORM。

步骤 1. 安装 Prisma CLI

采用 Prisma ORM 的第一步是在您的项目中安装 Prisma CLI

npm install prisma --save-dev

步骤 2. 内省数据库

2.1. 设置 Prisma ORM

在内省数据库之前,您需要设置您的 Prisma schema 并将 Prisma 连接到您的数据库。在项目根目录运行以下命令以创建基本的 Prisma schema 文件

npx prisma init --output ../generated/prisma

此命令为您创建了一个名为 prisma 的新目录,其中包含以下文件

  • schema.prisma: 您的 Prisma schema,指定您的数据库连接和模型
  • .env: 一个 dotenv 文件,用于将您的数据库连接 URL 配置为环境变量
注意

您可能已经有一个 .env 文件。如果是这样,prisma init 命令将向其中追加行,而不是创建一个新文件。

Prisma schema 当前如下所示

prisma/schema.prisma
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

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

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

如果您使用 VS Code,请务必安装 Prisma VS Code 扩展,以获得语法高亮、格式化、自动补全以及更多炫酷功能。

2.2. 连接数据库

如果您不使用 PostgreSQL,则需要将 datasource 块上的 provider 字段调整为您当前使用的数据库

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

完成后,您可以在 .env 文件中配置您的数据库连接 URL。Drizzle 和 Prisma ORM 使用相同的连接 URL 格式,因此您现有的连接 URL 应该可以正常工作。

2.3. 使用 Prisma ORM 内省数据库

设置好连接 URL 后,您可以内省数据库以生成 Prisma 模型

npx prisma db pull

如果您使用的是示例项目,将创建以下模型

prisma/schema.prisma
model todo {
id Int @id
text String
done Boolean @default(false)
}

生成的 Prisma 模型代表一个数据库表。Prisma 模型是您的程序化 Prisma Client API 的基础,该 API 允许您向数据库发送查询。

2.4. 创建基线迁移

要继续使用 Prisma Migrate 来演进您的数据库 schema,您需要为数据库设置基线

首先,创建一个 migrations 目录,并在其中添加一个您喜欢的迁移名称的子目录。在此示例中,我们将使用 0_init 作为迁移名称

mkdir -p prisma/migrations/0_init

接下来,使用 prisma migrate diff 生成迁移文件。使用以下参数

  • --from-empty: 假设您从中迁移的数据模型是空的
  • --to-schema-datamodel: 使用 datasource 块中 URL 的当前数据库状态
  • --script: 输出 SQL 脚本
npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql

检查生成的迁移文件以确保一切正确。

接下来,使用带有 --applied 参数的 prisma migrate resolve 将该迁移标记为已应用。

npx prisma migrate resolve --applied 0_init

该命令将通过将其添加到 _prisma_migrations 表中来将 0_init 标记为已应用。

您现在已拥有当前数据库 schema 的基线。要对数据库 schema 进行进一步更改,您可以更新 Prisma schema 并使用 prisma migrate dev 将更改应用到数据库。

2.5. 调整 Prisma schema(可选)

通过内省生成的模型当前与您的数据库表完全映射。在本节中,您将学习如何调整 Prisma 模型的命名,以符合 Prisma ORM 的命名约定

所有这些调整都是完全可选的,如果您暂时不想进行任何调整,可以自由地跳到下一步。您可以在以后的任何时候回来进行调整。

与 Drizzle 模型当前的 camelCase 命名法不同,Prisma ORM 的命名约定是

  • 模型名称使用 PascalCase
  • 字段名称使用 camelCase

您可以使用 @@map@map 将 Prisma 模型和字段名称映射到底层数据库中现有的表名和列名来调整命名。

以下是关于如何修改上述模型的示例

prisma/schema.prisma
model Todo {
id Int @id
text String
done Boolean @default(false)

@@map("todo")
}

步骤 3. 安装和生成 Prisma Client

下一步,您可以在项目中安装 Prisma Client,以便开始替换项目中当前使用 Drizzle 发送的数据库查询。

npm install @prisma/client

安装后,您需要运行 generate,以便您的 schema 反映在 TypeScript 类型和自动补全中。

npx prisma generate

步骤 4. 用 Prisma Client 替换您的 Drizzle 查询

在本节中,我们将基于示例 REST API 项目的示例路由,展示一些从 Drizzle 迁移到 Prisma Client 的示例查询。有关 Prisma Client API 与 Drizzle 的详细对比,请参阅对比页面

首先,设置 PrismaClient 实例,您将使用它从各种路由处理程序发送数据库查询。在 db 目录中创建一个名为 prisma.ts 的新文件

touch db/prisma.ts

现在,实例化 PrismaClient 并从文件中导出,以便稍后在路由处理程序中使用。

db/prisma.ts
import { PrismaClient } from '@prisma/client'

export const prisma = new PrismaClient()

4.1. 替换 getData 查询

全栈 Next.js 应用有几个 actions,包括 getData

getData action 当前实现如下

actions/todoActions.ts
import db from "@/db/drizzle";
import { todo } from "@/db/schema";

export const getData = async () => {
const data = await db.select().from(todo);
return data;
};

这是使用 Prisma Client 实现的相同 action

src/controllers/FeedAction.ts
import { prisma } from "@/db/prisma";

export const getData = async () => {
const data = await prisma.todo.findMany();
return data;
};

4.2. 替换 POST 请求中的查询

示例项目有四个 actions 在 POST 请求期间被使用

  • addTodo: 创建新的 Todo 记录
  • deleteTodo: 删除现有 Todo 记录
  • toggleTodo: 切换现有 Todo 记录上的布尔字段 done
  • editTodo: 编辑现有 Todo 记录上的文本字段 text

addTodo

addTodo action 当前实现如下

actions/todoActions.ts
import { revalidatePath } from "next/cache";

import db from "@/db/drizzle";
import { todo } from "@/db/schema";

export const addTodo = async (id: number, text: string) => {
await db.insert(todo).values({
id: id,
text: text,
});
revalidatePath("/");
};

这是使用 Prisma Client 实现的相同 action

actions/todoActions.ts
import { revalidatePath } from "next/cache";

import { prisma } from "@/db/prisma";

export const addTodo = async (id: number, text: string) => {
await prisma.todo.create({
data: { id, text },
})
revalidatePath("/");
};

deleteTodo

deleteTodo action 当前实现如下

actions/todoActions.ts
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

import db from "@/db/drizzle";
import { todo } from "@/db/schema";

export const deleteTodo = async (id: number) => {
await db.delete(todo).where(eq(todo.id, id));
revalidatePath("/");
};

这是使用 Prisma Client 实现的相同 action

actions/todoActions.ts
import { revalidatePath } from "next/cache";

import { prisma } from "@/db/prisma";

export const deleteTodo = async (id: number) => {
await prisma.todo.delete({ where: { id } });
revalidatePath("/");
};

toggleTodo

ToggleTodo action 当前实现如下

actions/todoActions.ts
import { eq, not } from "drizzle-orm";
import { revalidatePath } from "next/cache";

import db from "@/db/drizzle";
import { todo } from "@/db/schema";

export const toggleTodo = async (id: number) => {
await db
.update(todo)
.set({
done: not(todo.done),
})
.where(eq(todo.id, id));
revalidatePath("/");
};

这是使用 Prisma Client 实现的相同 action

actions/todoActions.ts
import { revalidatePath } from "next/cache";

import { prisma } from "@/db/prisma";

export const toggleTodo = async (id: number) => {
const todo = await prisma.todo.findUnique({ where: { id } });
if (todo) {
await prisma.todo.update({
where: { id: todo.id },
data: { done: !todo.done },
})
revalidatePath("/");
}
};

请注意,Prisma ORM 没有“就地”编辑布尔字段的能力,因此必须先获取记录。

editTodo

editTodo action 当前实现如下

actions/todoActions.ts
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

import db from "@/db/drizzle";
import { todo } from "@/db/schema";

export const editTodo = async (id: number, text: string) => {
await db
.update(todo)
.set({
text: text,
})
.where(eq(todo.id, id));

revalidatePath("/");
};

这是使用 Prisma Client 实现的相同 action

actions/todoActions.ts
import { revalidatePath } from "next/cache";

import { prisma } from "@/db/prisma";

export const editTodo = async (id: number, text: string) => {
await prisma.todo.update({
where: { id },
data: { text },
})
revalidatePath("/");
};

更多

隐式多对多关系

与 Drizzle 不同,Prisma ORM 允许您隐式建模多对多关系。也就是说,在多对多关系中,您无需在 schema 中显式管理关联表(有时也称为 JOIN 表)。以下是 Drizzle 与 Prisma ORM 的对比示例

schema.ts
import { boolean, integer, pgTable, serial, text } from "drizzle-orm/pg-core";

export const posts = pgTable('post', {
id: serial('serial').primaryKey(),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false).notNull(),
});

export const categories = pgTable('category', {
id: serial('serial').primaryKey(),
name: text('name').notNull(),
});

export const postsToCategories = pgTable('posts_to_categories', {
postId: integer('post_id').notNull().references(() => users.id),
categoryId: integer('category_id').notNull().references(() => chatGroups.id),
});

此 schema 等同于以下 Prisma schema

schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
postsToCategories PostToCategories[]

@@map("post")
}

model Category {
id Int @id @default(autoincrement())
name String
postsToCategories PostToCategories[]

@@map("category")
}

model PostToCategories {
postId Int
categoryId Int
category Category @relation(fields: [categoryId], references: [id])
post Post @relation(fields: [postId], references: [id])

@@id([postId, categoryId])
@@index([postId])
@@index([categoryId])
@@map("posts_to_categories")
}

在此 Prisma schema 中,多对多关系通过关联表 PostToCategories 显式建模。

通过遵循 Prisma ORM 关联表的约定,该关联可以如下所示

schema.prisma
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
categories Category[]
}

model Category {
id Int @id @default(autoincrement())
name String
posts Post[]
}

这也将使 Prisma Client API 更符合人体工程学且更简洁,以便修改此关联中的记录,因为您可以从 Post 直接到达 Category(反之亦然),而无需先遍历 PostToCategories 模型。

警告

如果您的数据库提供者要求表具有主键,则您必须使用显式语法,并手动创建带有主键的 join 模型。这是因为 Prisma ORM(通过 @relation 表达)使用隐式语法为多对多关系创建的关联表(JOIN 表)没有主键。


与 Prisma 保持联系

通过以下方式与 Prisma 保持联系,继续您的 Prisma 之旅 我们活跃的社区。随时了解最新信息,参与其中,并与其他开发者协作

我们真诚地重视您的参与,并期待您成为我们社区的一员!