Next.jsにおけるORMのスタンダードであるPrismaを触ってみます。
実装したソースコードはこちら
1. Prismaのインストールと設定
pnpm add -D prisma tsx @types/pg
pnpm add @prisma/client @prisma/adapter-pg dotenv pg
Prismaの初期化
pnpm prisma init --datasource-provider postgresql
初期化に成功すると以下のファイルが生成されます。
-
prisma.config.ts: Prismaの設定用ファイル -
prisma/schema.prisma: データベース接続情報とモデル定義ファイル .env
prisma.config.ts (prismaの設定ファイル)
// This file was generated by Prisma and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig, env } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma", // DBのテーブル定義ファイルのパス
migrations: {
path: "prisma/migrations", // DBのマイグレーションスクリプトの配置ディレクトリ
},
datasource: {
url: env("DATABASE_URL"), // データベースへの接続情報
},
});
schema.prisma (DBのテーブル定義ファイル)
モデルのクライアントの出力先を任意のディレクトリ(今回は ../src/generated/prisma
)に設定します。
今回はPostgreSQLをバックエンドのDBに利用するので provider は postgresql となります。
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client"
output = "../src/generated/prisma" # ★クライアントの出力先を好きなディレクトリに変更
}
datasource db {
provider = "postgresql"
}
.env 環境変数ファイル
アプリ起動時に読み込む環境変数ファイルにDBの接続URL( DATABASE_URL ) を追加します。
※ 接続先のDBがRDSの場合は デフォルトでSSL接続が強制されているため、 sslmode=no-verify オプションを追加する必要があります。
DATABASE_URL="postgresql://app:root1234@react-work-sample-postgresql:5432/sample?schema=public"
2. Prismaスキーマの定義
prisma/schema.prisma ファイルを開き、サンプルのモデルを定義します。
// ... 省略 ...
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
// 1対多のリレーション: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/one-to-many-relations
author User @relation(fields: [authorId], references: [id])
// 暗黙的な多対多リレーション: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#implicit-many-to-many-relations
tags Tag[]
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
// 暗黙的な多対多リレーション: https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#implicit-many-to-many-relations
posts Post[]
}
モデルの定義はこのあたりのドキュメントが参考になりそうです。
3. マイグレーション
マイグレーションファイルの生成
モデル定義が完成したら、モデルとDBを同期させるためのマイグレーションファイルを生成します。
マイグレーションファイルは prisma.config.ts の migrations.path に設定したディレクトリ配下に生成されます。 (今回の場合は prisma/migrations )
# マイグレーションファイルを作成
pnpm prisma migrate dev --name init --create-only
マイグレーション実行
マイグレーションファイルを実行してDBを更新します。
pnpm prisma migrate dev # dev環境
pnpm prisma migrate deploy # 本番環境
マイグレーションステータスの確認
pnpm prisma migrate status
# Loaded Prisma config from prisma.config.ts.
#
# Prisma schema loaded from prisma/schema.prisma
# Datasource "db": PostgreSQL database "sample", schema "public" at "react-work-sample-postgresql:5432"
#
# 2 migrations found in prisma/migrations
#
# Database schema is up to date!
Prisma Clientの生成
Prismaを通してDBにアクセスするために、最新のスキーマでクライアントコードを生成します。
クライアントコードは prisma/schema.prisma の client.output で指定したパスに生成されます。 (今回の場合は ../src/generated/prisma )
pnpm prisma generate
確認
psqlで直接ログインして確認
PGPASSWORD=root1234 psql -U app -h react-work-sample-postgresql -d sample -p 5432 -c "\d"
List of relations
Schema | Name | Type | Owner
--------+--------------------+----------+-------
public | Post | table | app
public | Post_id_seq | sequence | app
public | Tag | table | app
public | Tag_id_seq | sequence | app
public | User | table | app
public | User_id_seq | sequence | app
public | _PostToTag | table | app
public | _prisma_migrations | table | app
(8 rows)
Prisma Studio で確認
prisma studio を使うとブラウザでDBの構造やテーブルの中身を閲覧することができます。
pnpm prisma studio
# Loaded Prisma config from prisma.config.ts.
# Prisma Studio is running at: http://localhost:51212
ブラウザで http://localhost:51212 にアクセス
4. 初期データの登録
生成したクライアントコードを利用して、データベースに初期データを登録します。
import { PrismaClient, Prisma } from '@/generated/prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import 'dotenv/config';
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,
tags: {
connectOrCreate: [ // 要素を作成してリレーションを作成。要素が存在する場合はリレーションのみ作成
{ where: {name: "a"}, create: {name: "a"} },
{ where: {name: "b"}, create: {name: "b"} },
]
}
},
{
title: "Prisma on YouTube",
content: "https://pris.ly/youtube",
tags: {
connectOrCreate: [
{ where: {name: "b"}, create: {name: "b"} },
{ where: {name: "c"}, create: {name: "c"} },
]
}
},
],
},
},
{
name: "Bob",
email: "bob@prisma.io",
posts: {
create: [
{
title: "Follow Prisma on Twitter",
content: "https://www.twitter.com/prisma",
published: true,
tags: {
connectOrCreate: [
{ where: {name: "c"}, create: {name: "c"} },
{ where: {name: "d"}, create: {name: "d"} },
]
}
},
],
},
},
]
export async function main() {
for (const u of userData) {
await prisma.user.create({data: u})
}
}
// tsx の ES Modules (ESM) モード で実行する場合、Top-level awaitが利用できる。
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"),
},
});
seed scriptの実行
pnpm prisma db seed
データの確認
pnpm prisma studio
5. Prisma Clientのラッパーを実装
DBにアクセスするたびにクライアントインスタンスを生成するのでは、DBへの無駄な接続が増えます。クライアントインスタンスはシングルトンとして、ワーカーで一つの接続を使い回すようにします。
import { PrismaClient, Prisma } from '@/generated/prisma/client';
import { PrismaPg, } from '@prisma/adapter-pg';
// ワーカーごとに一つの接続を使い回す
const globalForPrisma = global as unknown as {
prisma: PrismaClient
}
const adapter = new PrismaPg({
connectionString: process.env.DATABASE_URL
})
const prisma = globalForPrisma.prisma || new PrismaClient({
adapter: adapter
})
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma
}
export default prisma
6. Prisma ORMでクエリを実行してみる
ユーザー一覧ページ
import prisma from '@/lib/prisma';
export default async function Home() {
const users = await prisma.user.findMany();
console.log(users)
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)] text-[#333333]">
Superblog
</h1>
<ol className="list-decimal list-inside font-[family-name:var(--font-geist-sans)]">
{
users.map((user) => {
return (
<li key={user.id} className="mb-2">{user.name}</li>
)
})
}
</ol>
</div>
);
}
投稿一覧ページ
import prisma from '@/lib/prisma';
export default async function Posts() {
const posts = await prisma.post.findMany({
include: { author: true }
});
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) => {
return (
<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>
);
}
投稿の詳細ページ
import prisma from '@/lib/prisma';
import { notFound } from 'next/navigation';
export default async function Post({ params }: {params: Promise<{id: string}>}) {
const {id} = await params;
const post = await prisma.post.findUnique({
where: {id: parseInt(id)},
include: {
author: true,
tags: true
}
});
if (!post) {
notFound()
}
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-center">by {post.author.name}</p>
<ol>
{
post.tags.map((tag) => {
return (
<li key={tag.id}>{tag.name}</li>
)
})
}
</ol>
<div className="prose prose-gray mt-8">
{post.content || "No content available."}
</div>
</article>
</div>
)
}
投稿の新規作成ページ
import prisma from '@/lib/prisma';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import Form from 'next/form'
export default async function NewPost() {
async function createPost(formData: FormData) {
"use server";
const title = formData.get("title") as string;
const content = formData.get("content") as string;
const tags = (formData.get("tags") as string).split(",").map((tag) => {
return tag.trim()
})
await prisma.post.create({
data: {
title: title,
content: content,
authorId: 1,
tags: {
connectOrCreate: tags.map((tag) => {
return {where: {name: tag}, create: {name: tag}}
})
}
}
})
revalidatePath("/posts"); // 特定パスのキャッシュを無効化
redirect("/posts")
}
return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create New Post</h1>
<Form action={createPost} 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="tags" className="block text-lg mb-2">
Tags
</label>
<input
type="text"
id="tags"
name="tags"
placeholder="Enter your post tags"
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>
)
}