如何將 Prisma ORM 與 React Router 7 搭配使用
簡介
本指南將向您展示如何將 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 提供者
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])
}
這會建立兩個模型:User 和 Post,且兩者之間具有一對多關係。
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 的新檔案
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 如何執行此指令碼
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:
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 檔案,並將現有程式碼替換為以下內容
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 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 檔案中
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 來獲取每篇文章的作者,以便顯示作者名稱。
這種「列表視圖」是網頁應用程式中最常見的模式之一。您接下來將為應用程式加入另外兩個常用的頁面:「詳細視圖」和「建立視圖」。
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 根據從
params物件中獲取的id來獲取文章。 - 如果文章不存在(可能是被刪除了,或是您輸入了錯誤的 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 上追蹤我們