跳至主要内容

如何将 Prisma ORM 与 React Router 7 结合使用

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 @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg dotenv pg
信息

如果你使用的是其他数据库提供程序(MySQL、SQL Server、SQLite),请安装相应的驱动程序适配器包,而不是 @prisma/adapter-pg。有关更多信息,请参阅 数据库驱动程序

安装后,在项目中初始化 Prisma

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

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

这将创建

  • 一个包含 schema.prisma 文件的 prisma 目录。
  • 一个用于配置 Prisma 的 prisma.config.ts 文件
  • 一个 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"
}

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 将 dotenv 添加到 prisma.config.ts

要访问 .env 文件中的变量,它们可以由您的运行时加载,或者通过使用 dotenv 加载。在 prisma.config.ts 顶部包含 dotenv 的导入

import 'dotenv/config'
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
},
datasource: {
url: env('DATABASE_URL'),
},
});

2.4. 配置 Prisma Client 生成器

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

npx prisma migrate dev --name init
npx prisma generate

2.5. 填充数据库

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

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

prisma/seed.ts
import { PrismaClient, Prisma } from "../app/generated/prisma/client.js";
import { PrismaPg } from "@prisma/adapter-pg";

const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});

const prisma = new PrismaClient({
adapter,
});

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.config.ts 来告诉 Prisma 如何运行此脚本

prisma.config.ts
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config';
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: {
path: 'prisma/migrations',
seed: `tsx prisma/seed.ts`,
},
datasource: {
url: env('DATABASE_URL'),
},
});

运行种子脚本

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 { PrismaPg } from "@prisma/adapter-pg";

const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL!,
});

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

const prisma = globalForPrisma.prisma || new PrismaClient({
adapter,
})

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 获取文章,该 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>
);
}

您还无法在应用程序中打开 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 进行可视化数据库管理

获取更多信息和更新

© . This site is unofficial and not affiliated with Prisma Data, Inc.