跳到主要内容

如何在 React Router 7 中使用 Prisma ORM

10 分钟

简介

本指南将向您展示如何在 React Router 7 中使用 Prisma ORM,React Router 7 是一个多策略路由器,既可以提供极简的声明式路由,也可以提供全栈框架的完整功能。

您将学习如何使用 React Router 7 设置 Prisma ORM 和 Prisma Postgres 并处理迁移。您可以在 GitHub 上找到一个可部署的示例

先决条件

1. 设置您的项目

在您要创建项目的目录中,运行 create-react-router 创建一个新的 React Router 应用程序,您将在本指南中使用它。

npx create-react-router@latest react-router-7-prisma

您将被提示选择以下选项,请选择 Yes(是)

信息
  • 初始化一个新的 git 仓库? Yes
  • 使用 npm 安装依赖? Yes

现在,导航到项目目录

cd react-router-7-prisma

2. 安装和配置 Prisma

2.1. 安装依赖项

要开始使用 Prisma,您需要安装一些依赖项

npm install prisma tsx --save-dev
npm install @prisma/extension-accelerate @prisma/client

安装完成后,在您的项目中初始化 Prisma

npx prisma init --db --output ../app/generated/prisma
信息

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

这将创建

  • 一个包含 schema.prisma 文件的 prisma 目录。
  • 一个 Prisma Postgres 数据库。
  • 项目根目录中包含 DATABASE_URL.env 文件。
  • 一个用于生成的 Prisma Client 的 output 目录,路径为 app/generated/prisma

2.2. 定义您的 Prisma Schema

prisma/schema.prisma 文件中,添加以下模型并更改生成器以使用 prisma-client 提供程序

prisma/schema.prisma
generator client {
provider = "prisma-client"
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])
}

这将创建两个模型:UserPost,它们之间存在一对多关系。

2.3. 配置 Prisma Client 生成器

现在,运行以下命令来创建数据库表并生成 Prisma Client

npx prisma migrate dev --name init

2.4. 填充数据库

添加一些填充数据以填充数据库中的示例用户和帖子。

prisma/ 目录中创建一个名为 seed.ts 的新文件

prisma/seed.ts
import { PrismaClient, Prisma } from "../app/generated/prisma/client.js";

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();

现在,通过更新 package.json 来告诉 Prisma 如何运行此脚本

package.json
{
"name": "react-router-7-prisma",
"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"
}
}

运行填充脚本

npx prisma db seed

并打开 Prisma Studio 检查您的数据

npx prisma studio

3. 将 Prisma 集成到 React Router 7 中

3.1. 创建一个 Prisma Client

在您的 app 目录内,创建一个新的 lib 目录,并在其中添加一个 prisma.ts 文件。此文件将用于创建和导出您的 Prisma Client 实例。

像这样设置 Prisma 客户端

app/lib/prisma.ts
import { PrismaClient } from "../generated/prisma/client.js";
import { withAccelerate } from '@prisma/extension-accelerate'

const globalForPrisma = global as unknown as {
prisma: PrismaClient
}

const prisma = globalForPrisma.prisma || new PrismaClient().$extends(withAccelerate())

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

export default prisma
警告

我们建议使用连接池(例如 Prisma Accelerate)来高效管理数据库连接。

如果您选择不使用连接池,请**避免**在长期运行的环境中全局实例化 PrismaClient。相反,请按需创建和销毁客户端,以防止耗尽您的数据库连接。

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

3.2. 使用 Prisma 查询您的数据库

现在您已经初始化了 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 加载器 来查询 User 模型以获取所有用户,然后将它们显示在一个列表中。

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

3.4 更新您的数据(可选)

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

  • 通过您选择的 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
import { type RouteConfig, index, route } from "@react-router/dev/routes";

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">
{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">{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 获取帖子,该 idparams 对象获取。
  • 如果帖子不存在(可能是已删除或您输入了错误的 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>
);
}

您还无法在应用程序中打开 posts/new 页面。为此,您需要再次将其添加到 routes.tsx

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;

现在您可以在新的 URL 上查看表单了。它看起来不错,但它还没有做任何事情。更新 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>
);
}

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

尝试导航到 localhost:5173/posts/new 并提交表单。

7. 下一步

现在您已经拥有一个使用 Prisma ORM 的正常运行的 React Router 应用程序,以下是一些您可以扩展和改进应用程序的方法

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

更多信息和更新

© . All rights reserved.