跳到主要内容

如何在 React Router 7 中使用 Prisma ORM

10 分钟

引言

本指南将向您展示如何在 React Router 7 中使用 Prisma ORM,这是一个多策略路由器,可以从声明式路由极简地使用,也可以像一个全栈框架一样功能齐全。

您将学习如何设置 Prisma ORM 和 Prisma Postgres 与 React Router 集成并处理数据库迁移。您可以在 GitHub 上找到一个可直接部署的示例

先决条件

在开始本指南之前,请确保您已安装

  • 安装 Node.js 20+
  • 一个帐户

1. 设置项目

在您要创建项目的目录中,运行 create-react-router 命令,创建一个我们将用于本指南的新 React Router 应用。

npx create-react-router@latest my-app

系统会提示您是否要初始化新的 git 仓库并使用 npm 安装依赖。请都选择是。

现在,导航到项目目录

cd my-app

2. 设置 Prisma ORM

2.1 安装 Prisma ORM 并创建您的第一个模型

首先,我们需要安装一些依赖。在项目的根目录中,打开您的终端并运行

npm install prisma --save-dev
npm install tsx --save-dev

然后,运行 prisma init --db 命令初始化 Prisma ORM 并为您的项目创建一个 Prisma Postgres 数据库。

npx prisma init --db --output ../app/generated/prisma
注意

您在设置 Prisma Postgres 数据库时需要回答几个问题。选择离您位置最近的区域,并为您的数据库选择一个容易记住的名称,例如“My React Router Project”。

这将在您的项目中创建一个新的 prisma 目录,并在其中包含一个 schema.prisma 文件。schema.prisma 文件是您定义数据库模型的地方。

prisma init 命令还会在项目根目录中创建一个 .env 文件,用于存储您的数据库连接字符串,并将生成的 Prisma Client 输出到 /app/generated/prisma 目录中。

接下来,让我们更改生成器以使用 prisma-client 提供者,并在 schema.prisma 文件中添加两个模型。一个 User 模型和一个 Post 模型。

prisma/schema.prisma
generator client {
provider = "prisma-client"
provider = "prisma-client-js"
output = "../app/generated/prisma"
}

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

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}

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

这代表一个简单的博客,包含用户和帖子。每个 Post 可以有一个 User 作为作者,并且每个 User 可以拥有多个 Post

现在我们有了 Prisma Schema 和模型,接下来迁移我们的 Prisma Postgres 数据库。

2.2 更新数据库 schema

警告

如果您连接的是现有数据的数据库,请使用 prisma db pull 命令,然后跳到设置 Prisma Client

我们可以使用 prisma migrate dev 命令将您的新 schema 应用到数据库中。

npx prisma migrate dev --name init

这会创建一个初始迁移,创建 UserPost 表,并将该迁移应用到您的数据库中。

现在,让我们向数据库中添加一些初始数据。

2.3 填充数据库(Seed)

Prisma ORM 内置支持用初始数据填充数据库。要做到这一点,您可以在 prisma 目录中创建一个名为 seed.ts 的新文件。

prisma/seed.ts
import { PrismaClient, Prisma } from '../app/generated/prisma'

const prisma = new PrismaClient()

const userData: Prisma.UserCreateInput[] = [
{
name: 'Alice',
email: 'alice@prisma.io',
posts: {
create: [
{
title: 'Join the Prisma Discord',
content: 'https://pris.ly/discord',
published: true,
},
{
title: 'Prisma on YouTube',
content: 'https://pris.ly/youtube',
},
],
},
},
{
name: 'Bob',
email: 'bob@prisma.io',
posts: {
create: [
{
title: 'Follow Prisma on Twitter',
content: 'https://www.twitter.com/prisma',
published: true,
},
],
},
}
]

export async function main() {
for (const u of userData) {
await prisma.user.create({ data: u })
}
}

main()

现在,将 prisma.seed 配置添加到您的 package.json 文件中。

package.json
{
"name": "my-app",
"private": true,
"type": "module",
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "react-router-serve ./build/server/index.js",
"typecheck": "react-router typegen && tsc"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@react-router/node": "^7.3.0",
"@react-router/serve": "^7.3.0",
"isbot": "^5.1.17",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router": "^7.3.0"
},
"devDependencies": {
"@react-router/dev": "^7.3.0",
"@tailwindcss/vite": "^4.0.0",
"@types/node": "^20",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.1",
"prisma": "^6.5.0",
"react-router-devtools": "^1.1.0",
"tailwindcss": "^4.0.0",
"tsx": "^4.19.3",
"typescript": "^5.7.2",
"vite": "^5.4.11",
"vite-tsconfig-paths": "^5.1.4"
}
}

最后,运行 prisma db seed 命令,用我们在 seed.ts 文件中定义的初始数据填充您的数据库。

npx prisma db seed

我们现在拥有一个带有一些初始数据的数据库了!您可以通过运行 prisma studio 命令来查看数据库中的数据。

npx prisma studio

2.4 设置 Prisma Client

现在我们有了一个带有一些初始数据的数据库,我们可以设置 Prisma Client 并将其连接到我们的数据库。

在您的 app 目录中,创建一个新的 lib 目录,并在其中添加一个 prisma.ts 文件。

mkdir -p app/lib && touch app/lib/prisma.ts

现在,将以下代码添加到您的 app/lib/prisma.ts 文件中

app/lib/prisma.ts
import { PrismaClient } from "../generated/prisma";

declare global {
// avoid multiple instances when hot-reloading
var prismaClient: PrismaClient;
}

globalThis.prismaClient ??= new PrismaClient();

const prisma = globalThis.prismaClient;

export default prisma;

此文件创建一个 Prisma Client 并将其附加到全局对象,以便在您的应用程序中仅创建一个客户端实例。这有助于解决在开发中使用 Prisma ORM 时可能出现的热重载问题。

我们将在下一节中使用此客户端来运行您的第一个查询。

3. 使用 Prisma ORM 查询数据库

现在我们已经初始化了 Prisma Client,连接到了数据库,并有了一些初始数据,我们可以开始使用 Prisma ORM 查询数据了。

在我们的示例中,我们将使应用程序的“主页”显示所有用户。

打开 app/routes/home.tsx 文件,将现有代码替换为以下内容

app/routes/home.tsx
import type { Route } from "./+types/home";

export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}

export default function Home({ loaderData }: Route.ComponentProps) {
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)]">
Superblog
</h1>
<ol className="list-decimal list-inside font-[family-name:var(--font-geist-sans)]">
<li className="mb-2">Alice</li>
<li>Bob</li>
</ol>
</div>
);
}
注意

如果您在第一行看到错误 import type { Route } from "./+types/home";,请确保运行 npm run dev 命令,以便 React Router 生成所需的类型。

这为我们提供了一个基本的页面,包含一个标题和一个用户列表。然而,用户列表是静态的。让我们更新该页面,从数据库中获取用户,使其变为动态。

app/routes/home.tsx
import type { Route } from "./+types/home";
import prisma from '~/lib/prisma'

export function meta({}: Route.MetaArgs) {
return [
{ title: "New React Router App" },
{ name: "description", content: "Welcome to React Router!" },
];
}

export async function loader() {
const users = await prisma.user.findMany();
return { users };
}

export default function Home({ loaderData }: Route.ComponentProps) {
const { users } = loaderData;
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)]">
Superblog
</h1>
<ol className="list-decimal list-inside font-[family-name:var(--font-geist-sans)]">
{users.map((user) => (
<li key={user.id} className="mb-2">
{user.name}
</li>
))}
</ol>
</div>
);
}

我们现在正在导入客户端,使用一个 React Router loader来查询 User 模型的所有用户,然后将它们显示在一个列表中。

现在您的主页是动态的,将显示数据库中的用户。

3.1 更新您的数据(可选)

如果您想看看数据更新时会发生什么,您可以

  • 通过您选择的 SQL 浏览器更新您的 User
  • 更改您的 seed.ts 文件以添加更多用户
  • 更改对 prisma.user.findMany 的调用以重新排序用户、过滤用户或进行类似操作。

只需重新加载页面,您就会看到变化。

4. 添加新的帖子列表页

我们的主页已经可以工作了,但我们应该添加一个新页面来显示所有帖子。

首先,在 app/routes 目录下创建一个新的 posts 目录,并添加一个 home.tsx 文件。

mkdir -p app/routes/posts && touch app/routes/posts/home.tsx

其次,将以下代码添加到 app/routes/posts/home.tsx 文件中

app/routes/posts/home.tsx
import type { Route } from "./+types/home";
import prisma from "~/lib/prisma";

export default function Home() {
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)]">
Posts
</h1>
<ul className="font-[family-name:var(--font-geist-sans)] max-w-2xl space-y-4">
<li>My first post</li>
</ul>
</div>
);
}

其次,更新 app/routes.ts 文件,以便当您访问 /posts 路由时,显示 posts/home.tsx 页面。

app/routes.ts
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
] satisfies RouteConfig;

现在 localhost:5173/posts 将会加载,但内容是静态的。让我们更新它,使其动态化,类似于主页。

app/routes/posts/home.tsx
import type { Route } from "./+types/home";
import prisma from "~/lib/prisma";

export async function loader() {
const posts = await prisma.post.findMany({
include: {
author: true,
},
});
return { posts };
}

export default function Posts({ loaderData }: Route.ComponentProps) {
const { posts } = loaderData;
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)]">
Posts
</h1>
<ul className="font-[family-name:var(--font-geist-sans)] max-w-2xl space-y-4">
<li>My first post</li>
{posts.map((post) => (
<li key={post.id}>
<span className="font-semibold">{post.title}</span>
<span className="text-sm text-gray-600 ml-2">
by {post.author.name}
</span>
</li>
))}
</ul>
</div>
);
}

这与主页类似,但不是显示用户,而是显示帖子。您还可以看到我们在 Prisma Client 查询中使用了 include 来获取每个帖子的作者,以便我们可以显示作者的姓名。

这种“列表视图”是 Web 应用程序中最常见的模式之一。我们将向应用程序添加另外两个您通常也需要的页面:“详细视图”和“创建视图”。

5. 添加新的帖子详情页

为了补充帖子列表页,我们将添加一个帖子详情页。

routes/posts 目录中,创建一个新的 post.tsx 文件。

touch app/routes/posts/post.tsx

此页面将显示单个帖子的标题、内容和作者。就像我们其他页面一样,将以下代码添加到 app/routes/posts/post.tsx 文件中

app/routes/posts/post.tsx
import type { Route } from "./+types/post";
import prisma from "~/lib/prisma";

export default function Post({ loaderData }: Route.ComponentProps) {
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<article className="max-w-2xl space-y-4 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-4xl font-bold mb-8">My first post</h1>
<p className="text-gray-600 text-center">by Anonymous</p>
<div className="prose prose-gray mt-8">
No content available.
</div>
</article>
</div>
);
}

然后为该页面添加一个新的路由

app/routes.ts
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
route("posts/:postId", "routes/posts/post.tsx"),
] satisfies RouteConfig;

和之前一样,这个页面是静态的。让我们根据传递给页面的 params 更新它,使其动态化。

app/routes/posts/post.tsx
import { data } from "react-router";
import type { Route } from "./+types/post";
import prisma from "~/lib/prisma";

export async function loader({ params }: Route.LoaderArgs) {
const { postId } = params;
const post = await prisma.post.findUnique({
where: { id: parseInt(postId) },
include: {
author: true,
},
});

if (!post) {
throw data("Post Not Found", { status: 404 });
}
return { post };
}

export default function Post({ loaderData }: Route.ComponentProps) {
const { post } = loaderData;
return (
<div className="min-h-screen flex flex-col items-center justify-center -mt-16">
<article className="max-w-2xl space-y-4 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-4xl font-bold mb-8">My first post</h1>
<p className="text-gray-600 text-center">by Anonymous</p>
<div className="prose prose-gray mt-8">
No content available.
</div>
<h1 className="text-4xl font-bold mb-8">{post.title}</h1>
<p className="text-gray-600 text-center">by {post.author.name}</p>
<div className="prose prose-gray mt-8">
{post.content || "No content available."}
</div>
</article>
</div>
);
}

这里有很多改动,让我们分解一下

  • 我们使用 Prisma Client 通过 id 获取帖子,这个 id 来自 params 对象。
  • 如果帖子不存在(可能已被删除,或者您输入了错误的 ID),我们将抛出一个错误来显示 404 页面。
  • 然后我们显示帖子的标题、内容和作者。如果帖子没有内容,我们显示一个占位符消息。

这不是最漂亮的页面,但这只是一个好的开始。通过导航到 localhost:5173/posts/1localhost:5173/posts/2 试试看。您也可以通过导航到 localhost:5173/posts/999 来测试 404 页面。

6. 添加新的帖子创建页

为了完善我们的应用程序,我们将添加一个帖子“创建”页面。这将允许您编写自己的帖子并将其保存到数据库中。

和其他页面一样,我们将从一个静态页面开始,然后更新它使其动态化。

touch app/routes/posts/new.tsx

现在,将以下代码添加到 app/routes/posts/new.tsx 文件中

app/routes/posts/new.tsx
import type { Route } from "./+types/new";
import { Form } from "react-router";

export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const title = formData.get("title") as string;
const content = formData.get("content") as string;
}

export default function NewPost() {
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create New Post</h1>
<Form method="post" className="space-y-6">
<div>
<label htmlFor="title" className="block text-lg mb-2">
Title
</label>
<input
type="text"
id="title"
name="title"
placeholder="Enter your post title"
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label htmlFor="content" className="block text-lg mb-2">
Content
</label>
<textarea
id="content"
name="content"
placeholder="Write your post content here..."
rows={6}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600"
>
Create Post
</button>
</Form>
</div>
);
}

这个表单看起来不错,但它目前什么都没做。让我们更新 action 以将帖子保存到数据库。

app/routes/posts/new.tsx
import type { Route } from "./+types/new";
import { Form, redirect } from "react-router";
import prisma from "~/lib/prisma";

export async function action({ request }: Route.ActionArgs) {
const formData = await request.formData();
const title = formData.get("title") as string;
const content = formData.get("content") as string;

try {
await prisma.post.create({
data: {
title,
content,
authorId: 1,
},
});
} catch (error) {
console.error(error);
return Response.json({ error: "Failed to create post" }, { status: 500 });
}

return redirect("/posts");
}

export default function NewPost() {
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create New Post</h1>
<Form method="post" className="space-y-6">
<div>
<label htmlFor="title" className="block text-lg mb-2">
Title
</label>
<input
type="text"
id="title"
name="title"
placeholder="Enter your post title"
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label htmlFor="content" className="block text-lg mb-2">
Content
</label>
<textarea
id="content"
name="content"
placeholder="Write your post content here..."
rows={6}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600"
>
Create Post
</button>
</Form>
</div>
);
}

现在,为该页面添加一个新的路由

app/routes.ts
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
route("posts/:postId", "routes/posts/post.tsx"),
route("posts/new", "routes/posts/new.tsx"),
] satisfies RouteConfig;

该页面现在有了一个功能齐全的表单!当您提交表单时,它将在数据库中创建一个新帖子,并将您重定向到帖子列表页。

通过导航到 localhost:5173/posts/new 并提交表单来试试看。

7. 下一步

现在您已经有了一个与 Prisma ORM 集成的 React Router 应用程序,以下是一些扩展和改进应用程序的方法

  • 添加身份验证以保护您的路由
  • 添加编辑和删除帖子的功能
  • 为帖子添加评论功能
  • 使用Prisma Studio进行可视化数据库管理

更多信息和更新