跳至主要內容

如何將 Prisma ORM 與 React Router 7 搭配使用

10 分鐘

簡介

本指南將向您展示如何將 Prisma ORM 與 React Router 7 結合使用。這是一個多策略路由器,既可以像宣告式路由一樣簡潔,也可以像全端框架一樣功能完備。

您將學會如何設定 Prisma ORM 與 Prisma Postgres 搭配 React Router 7,並處理資料庫遷移(migrations)。您可以在 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 資料庫時,您需要回答幾個問題。請選擇離您最近的區域,並為您的資料庫取一個好記的名字,例如「My React Router 7 Project」。

這將建立:

  • 一個包含 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 檔案中,加入下列模型並將 generator 修改為使用 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 在 prisma.config.ts 加入 dotenv

要存取 .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. 進行資料庫植入 (Seed)

加入一些種子資料,以在資料庫中填充範例使用者與貼文。

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 client:

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 loader 來查詢 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 來獲取每篇文章的作者,以便顯示作者名稱。

這種「列表視圖」是網頁應用程式中最常見的模式之一。您接下來將為應用程式加入另外兩個常用的頁面:「詳細視圖」和「建立視圖」。

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 根據從 params 物件中獲取的 id 來獲取文章。
  • 如果文章不存在(可能是被刪除了,或是您輸入了錯誤的 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.