如何在 React Router 7 中使用 Prisma ORM
简介
本指南将向您展示如何在 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,您需要安装一些依赖项
- Prisma Postgres(推荐)
- 其他数据库
npm install prisma tsx --save-dev
npm install @prisma/extension-accelerate @prisma/client
npm install prisma tsx --save-dev
npm install @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
提供程序
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])
}
这将创建两个模型:User
和 Post
,它们之间存在一对多关系。
2.3. 配置 Prisma Client 生成器
现在,运行以下命令来创建数据库表并生成 Prisma Client
npx prisma migrate dev --name init
2.4. 填充数据库
添加一些填充数据以填充数据库中的示例用户和帖子。
在 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 如何运行此脚本
{
"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 客户端
- Prisma Postgres(推荐)
- 其他数据库
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
import { PrismaClient } from "../generated/prisma/client.js";
const globalForPrisma = global as unknown as {
prisma: PrismaClient
}
const prisma = globalForPrisma.prisma || new PrismaClient()
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
文件并用以下代码替换现有代码
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 生成所需的类型。
这为您提供了一个带有标题和用户列表的基本页面。但是,用户列表是静态的。更新页面以从数据库中获取用户并使其动态化。
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
文件中
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
页面
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
将加载,但内容是静态的。更新它以使其动态化,类似于主页
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
文件中
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>
);
}
然后为此页面添加一个新路由
export default [
index("routes/home.tsx"),
route("posts", "routes/posts/home.tsx"),
route("posts/:postId", "routes/posts/post.tsx"),
] satisfies RouteConfig;
如前所述,此页面是静态的。更新它以根据传递给页面的 params
进行动态化
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
获取帖子,该id
从params
对象获取。 - 如果帖子不存在(可能是已删除或您输入了错误的 ID),您将抛出错误以显示 404 页面。
- 然后,您显示帖子的标题、内容和作者。如果帖子没有内容,则显示占位符消息。
它不是最漂亮的页面,但这是一个很好的开始。尝试导航到 localhost:5173/posts/1
和 localhost:5173/posts/2
。您还可以通过导航到 localhost:5173/posts/999
来测试 404 页面。
6. 添加新的帖子创建页面
为了完善您的应用程序,您将为帖子添加一个“创建”页面。这将允许您编写自己的帖子并将其保存到数据库中。
与其它页面一样,您将从一个静态页面开始,然后将其更新为动态页面。
touch 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
中
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
以将帖子保存到数据库中
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 进行可视化数据库管理
更多信息和更新
- Prisma ORM 文档
- Prisma Client API 参考
- React Router 文档
- 加入我们的 Discord 社区
- 关注我们的 Twitter 和 YouTube