【Next.js】Prismaをつかってみたい
Node.js および TypeScript の ORM であるPrisma。
Next.js でつかってみたいので、設定していきます!
そもそも ORM とは?
ORM(Object Relational Mapper) とは、オブジェクト指向のように RDB を操作できるもの。
SQL を書く必要がなく、プログラミング言語と RDB の間を取り持ってくれるというイメージ。
つまり、プログラム ↔ ORM ↔ RDBとなります。
あらためて Prisma とは?
こちらの公式ドキュメントによると、
プリズマは、オープンソース次世代 ORM。これは次の部分で構成されます。
Prisma Client : Node.js および TypeScript 用の自動生成されたタイプセーフなクエリ ビルダー
Prisma Migrate : 移行システム
Prisma Studio : データベース内のデータを表示および編集するための GUI。
Prisma Studio のみオープンソースではない点にご注意ください!
Node.js や TypeScript 用の ORMということですね。
Prisma が必要としている要件やサポートしているバージョンは下記をご覧ください!
・要件
・サポートされているフレームワーク
・サポートされている DB
バージョン
バージョン | |
---|---|
Prisma | 5.2.0 |
Prisma Client | 5.2.0 |
Prisma Studio | 0.494.0 |
Node.js | 18.17.0 |
Next.js | 13.4.19 |
Docker | 20.10.21 |
PostgreSQL | 15.4 |
完成したアプリケーション
こちらのGitHub リポジトリに完成したアプリケーションを push しました。
ご参考ください!!
下準備
Next.js のアプリを作成
もととなる Next.js のサンプルアプリを作成します。
コマンド実行後にきかれるものはすべてデフォルトのままにしておきます。
$ npx create-next-app sample-prisma-next-app
作成出来たら、動くか確認しておきます
$ cd sample-prisma-next-app
$ npm run dev
http://localhost:3000 にアクセスして、表示されれば OK です!
PostgreSQL コンテナを起動
↓ を参考にコンテナを起動していきます。
PostgreSQL コンテナ用のフォルダを作成します。
ファイルの構成は以下とします。
PostgreSQL$ tree
.
├── config
│ └── postgresql.conf
├── docker-compose.yml
└── sql
└── v # コンテナの初回起動時のみ実行したいファイル
各ファイルの中身はこんな感じ
listen_addresses = '*'
version: '3'
services:
db:
image: postgres:15.4
container_name: postgres
# confファイルの指定
command: -c 'config_file=/etc/postgresql/postgresql.conf'
ports:
- 5432:5432
volumes:
# DBのデータをボリュームマウント
- db-store:/var/lib/postgresql/data
# sqlファイルをコンテナにバインドマウント
# コンテナの初回起動時のみ実行されるsql
- ./sql/init:/docker-entrypoint-initdb.d
# 任意のSQLファイルを実行できるようにバインドマウント
- ./sql/tmp:/tmp
# confファイルをコンテナにバインドマウント
- ./config/postgresql.conf:/etc/postgresql/postgresql.conf
environment:
# スーパーユーザーのパスワード
- POSTGRES_PASSWORD=passw0rd
volumes:
db-store:
-- Prismaが使用するユーザー作成
CREATE USER prismauser WITH PASSWORD 'prismapassword';
ALTER ROLE prismauser SET CLIENT_ENCODING TO 'utf8';
ALTER ROLE prismauser SET DEFAULT_TRANSACTION_ISOLATION TO 'read committed';
ALTER ROLE prismauser SET TIMEZONE TO 'Asia/Tokyo';
-- prisma migrateする際に必要な権限
-- https://www.prisma.io/docs/concepts/components/prisma-migrate/shadow-database#shadow-database-user-permissions
ALTER ROLE prismauser CREATEDB;
-- DATABASE作成
CREATE DATABASE prismadb;
-- 作成したDBへ切り替え
\c prismadb
-- スキーマ作成
CREATE SCHEMA prismaschema;
-- 権限追加
GRANT CREATE ON DATABASE prismadb TO prismauser;
GRANT USAGE ON SCHEMA prismaschema TO prismauser;
GRANT CREATE ON SCHEMA prismaschema TO prismauser;
GRANT DELETE, INSERT, SELECT, UPDATE ON ALL TABLES IN SCHEMA prismaschema TO prismauser;
GRANT SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA prismaschema TO prismauser;
ユーザー名やデータベース名、スキーマ名はわかりやすいように prismaにしていますが、なんでもいいです
ファイルを作成したら、コンテナを起動して入ってみます
// コンテナを起動
PostgreSQL$ docker compose up -d
[+] Running 3/3
✔ Network postgresql_default Created 0.0s
✔ Volume "postgresql_db-store" Created 0.0s
✔ Container postgres Started
// コンテナに入る
PostgreSQL$ docker exec -it postgres /bin/sh
// PostgreSQLにログイン
# psql -h localhost -U prismauser prismadb
psql (15.4 (Debian 15.4-1.pgdg120+1))
Type "help" for help.
// スキーマを確認
prismadb=> \dn;
List of schemas
Name | Owner
--------------+-------------------
prismaschema | postgres
public | pg_database_owner
(2 rows)
// スキーマを確認
prismadb=> select prismaschema;
// テーブル確認:作ってないのでない
prismadb-> \dt;
Did not find any relations.
// ログアウト
prismadb-> \q
// コンテナから抜ける
# exit
PostgreSQL$
うまく起動できているのでこれで OK です!
Prisma をつかってみる
Next.js アプリのルートディレクトリに移動して下さい
Prisma をインストール
// インストール
sample-prisma-next-app$ npm install prisma --save-dev
added 2 packages, and audited 332 packages in 6s
117 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
// 確認
sample-prisma-next-app$ npx prisma --version
prisma : 5.2.0
@prisma/client : Not found
Current platform : debian-openssl-3.0.x
Query Engine (Node-API) : libquery-engine 2804dc98259d2ea960602aca6b8e7fdc03c1758f (at node_modules/@prisma/engines/libquery_engine-debian-openssl-3.0.x.so.node)
Schema Engine : schema-engine-cli 2804dc98259d2ea960602aca6b8e7fdc03c1758f (at node_modules/@prisma/engines/schema-engine-debian-openssl-3.0.x)
Schema Wasm : @prisma/prisma-schema-wasm 5.2.0-25.2804dc98259d2ea960602aca6b8e7fdc03c1758f
Default Engines Hash : 2804dc98259d2ea960602aca6b8e7fdc03c1758f
Studio : 0.494.0
--save-dev をつけるのはなぜなのかは ↓ の記事を読んでみるとわかると思います
Prisma をセットアップ
npx prisma init
↑ のコマンドを実行します。
PostgreSQL 以外を使用する方は、datasource-provider オプションで指定します。(参考)
これによって
- prisma フォルダとその配下に schema.prisma を作成
- .env を作成 ※git にコミットしないように.gitignore に.env を追記しましょう
がなされます。
(実行結果)
sample-prisma-next-app$ npx prisma init
<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="✔" src="https://s.w.org//images/core/emoji/14.0.0/svg/2714.svg">" src="https://s.w.org//images/core/emoji/14.0.0/svg/2714.svg"> Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb.
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
// フォルダやファイルが作成されたか確認
sample-prisma-next-app$ ll prisma/
total 0
drwxrwxrwx 1 ****** ****** 4096 Aug 28 23:44 ./
drwxrwxrwx 1 ****** ****** 4096 Aug 28 23:44 ../
-rwxrwxrwx 1 ****** ****** 236 Aug 28 23:44 schema.prisma*
sample-prisma-next-app$ ll .env
-rwxrwxrwx 1 ****** ****** 519 Aug 28 23:44 .env*
DB の接続情報を設定する
先ほど作成された schema.json の中身を見てみます
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
このうち、DB に関する記述は 8~11 行目の datasource db の部分になります
url は値を直書きせずに、DATABASE_URL という環境変数から読み取るようになっています
そこで、schema.json と一緒に作成された .env ファイルをみます
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
DATABASE_URL が定義されているので、値を書き換えていきます。
PostgreSQL の場合は、下記のフォーマットになっています
// 大文字:人によって値が異なるため、適宜書き換える
postgresql://USER:PASSWORD@HOST:PORT/DATABASE?schema=SCHEMA
.env を
DATABASE_URL="postgresql://prismauser:prismapassword@localhost:5432/prismadb?schema=prismaschema"
に書き換えます
Prisma Migrate してみる
schema.prisma で Data Model を定義して、DB に反映してみます
まず、schema.prisma に Data Model を定義します
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Post {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
title String @db.VarChar(255)
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
model Profile {
id Int @id @default(autoincrement())
bio String?
user User @relation(fields: [userId], references: [id])
userId Int @unique
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
profile Profile?
}
↓ のコマンドで schema.prisma の内容を DB に反映させます。
npx prisma migrate dev --name init
これで2つのことが実行されます
- schema.prisma をもとに、DB に実行する用の SQL ファイルを作成
- 1. で作成された SQL ファイルを DB に実行する
(実行結果)
sample-prisma-next-app$ npx prisma migrate dev --name init
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "prismadb", schema "prismaschema" at "localhost:5432"
Applying migration `20230903073559_init`
The following migration(s) have been created and applied from new schema changes:
migrations/
└─ 20230903073559_init/
└─ migration.sql
Your database is now in sync with your schema.
Running generate... (Use --skip-generate to skip the generators)
added 2 packages, and audited 334 packages in 7s
117 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
✔ Generated Prisma Client (v5.2.0) to ./node_modules/@prisma/client in 343ms
エラーが出た方は ↓ を参考にしてみてください
成功したら、prisma フォルダに migrations フォルダと SQL ファイルが作成されています
sample-prisma-next-app$ tree prisma
prisma
├── migrations
│ ├── 20230903073559_init
│ │ └── migration.sql # これが作成・実行されたSQLファイル
│ └── migration_lock.toml
└── schema.prisma
migration.sql を見てみると、
schema.prisma で定義した3 つの Data Model に対応する 3 つのテーブルを作成しています
-- CreateTable
CREATE TABLE "Post" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"title" VARCHAR(255) NOT NULL,
"content" TEXT,
"published" BOOLEAN NOT NULL DEFAULT false,
"authorId" INTEGER NOT NULL,
CONSTRAINT "Post_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Profile" (
"id" SERIAL NOT NULL,
"bio" TEXT,
"userId" INTEGER NOT NULL,
CONSTRAINT "Profile_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"email" TEXT NOT NULL,
"name" TEXT,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "Profile_userId_key" ON "Profile"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- AddForeignKey
ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Prisma Client をインストール
Prisma を Next.js のアプリから呼び出せるように Prisma Client をインストールします
npm install @prisma/client
こちらのコマンドを実行すると、
- パッケージのインストール
- prisma generate : schema.prisma で定義した内容を Prisma Client へインポート
を行います。
prisma migrate や prisma db push を実行した際にも prisma generate が実行され、Prisma Client が更新されます
(実行結果)
sample-prisma-next-app$ npm install @prisma/client
up to date, audited 334 packages in 3s
117 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
Prisma Client をつかってみる
Next.js アプリ内でPrisma Client を使って、DB へのデータの読み書きを行います
下記のようなアプリケーションを作成してみます
- プロフィールとともにユーザーを登録する
- 登録されているユーザー一覧を取得し、表示する
ディレクトリ構成
src
├── app
│ ├── api
│ │ └── user
│ │ └── route.ts # ファイル作成
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── components
│ ├── addUser.tsx # ファイル作成
│ └── userList.tsx # ファイル作成
└── lib
└── prisma.ts # ファイル作成
コード
・src/app/global.css
これがあると画面が見づらいので中身を空にしておきます
・src/app/page.tsx
後述するコンポーネントを呼び出します
import AddUser from "@/components/addUser";
import UserList from "@/components/userList"
export default async function Home() {
return (
<div>
{/* User登録を行うコンポーネント */}
<AddUser />
{/* User一覧を表示するコンポーネント */}
<UserList />
</div>
);
}
・src/app/user/route.ts
Prisma Client を使用して DB とやり取りする処理を API 化しておく
http://localhost:3000/api/user がエンドポイントになります
import { NextRequest, NextResponse } from "next/server";
// Prisma Clientのインスタンスをインポート
import prisma from "@/lib/prisma";
export async function GET() {
const userArray = await prisma.user.findMany({
include: {
// true: 外部キーを設定しているテーブルの情報も一緒に持ってくる
posts: true,
profile: true,
},
});
// Response を jsonで返す
return NextResponse.json(userArray);
}
export async function POST(req: NextRequest) {
// リクエストボディ
const { name, email, postTitle, profile } = await req.json();
const res = await prisma.user.create({
data: {
name: name,
email: email,
// ネストすることで参照先のテーブルに書き込める
// Postテーブルに書き込む
posts: {
create: { title: postTitle },
},
// Profileテーブルに書き込む
profile: {
create: { bio: profile },
},
},
});
return NextResponse.json(res);
}
・src/components/addUser.tsx
ユーザー登録を行うコンポーネント
// input タグのonChangeを使うためにServer Componentにする
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
export default function AddUser() {
const router = useRouter();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [postTitle, setPostTitle] = useState("");
const [profile, setProfile] = useState("");
// Userテーブルへデータを書き込む
// 注意:emailが重複禁止となっている
const fetchAsyncAddUser = async () => {
// 入力されていないものがあれば、登録しない
if (name == "" || email == "" || postTitle == "" || profile == "") {
console.log("すべての項目を埋めてください");
return;
}
// APIのURL
const url = "http://localhost:3000/api/user";
// リクエストパラメータ
const params = {
method: "POST",
// JSON形式のデータのヘッダー
headers: {
"Content-Type": "application/json",
},
// リクエストボディ
body: JSON.stringify({
name: name,
email: email,
postTitle: postTitle,
profile: profile,
}),
};
// APIへのリクエスト
await fetch(url, params);
// 入力値を初期化
setName("");
setEmail("");
setPostTitle("");
setProfile("");
// 画面をリフレッシュ
router.refresh();
};
// inputタグのvalueに変化があった際に実行される
const changeNameInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value);
};
// inputタグのvalueに変化があった際に実行される
const changeEmailInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setEmail(e.target.value);
};
// inputタグのvalueに変化があった際に実行される
const changePostTitleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setPostTitle(e.target.value);
};
// inputタグのvalueに変化があった際に実行される
const changeProfileInput = (e: React.ChangeEvent<HTMLInputElement>) => {
setProfile(e.target.value);
};
return (
<div>
<h2>Add User</h2>
<div>
<div>
<label>Name:</label>
<input
type="text"
name="name"
value={name}
onChange={changeNameInput}
/>
</div>
<div>
<label>Email:</label>
<input
type="text"
name="email"
value={email}
onChange={changeEmailInput}
/>
</div>
<div>
<label>Post Title:</label>
<input
type="text"
name="postTitle"
value={postTitle}
onChange={changePostTitleInput}
/>
</div>
<div>
<label>Profile:</label>
<input
type="text"
name="profile"
value={profile}
onChange={changeProfileInput}
/>
</div>
<div>
<button onClick={fetchAsyncAddUser}>追加</button>
</div>
</div>
</div>
);
}
・src/components/userList.tsx
ユーザー一覧を表示するコンポーネント
export default async function UserList() {
// APIのURL
const url = "http://localhost:3000/api/user";
// APIへリクエスト
const res = await fetch(url, {
cache: "no-store",
});
// レスポンスボディを取り出す
const data = await res.json();
return (
<div>
<h2>All Users</h2>
{data.map((user: any, index: any) => (
<div key={index}>
Name: {user.name} Email: {user.email} Posts:
{user.posts.map((value: any) => `${value.title},`)} Profile:
{user.profile?.bio}
</div>
))}
</div>
);
}
・src/lib/prisma.ts
ベストプラクティスに沿って、Prisma Client をシングルトンにしておきます
このファイルでのみ Prisma Client をインスタンス化し、ほかのファイルから呼び出します
// Prisma Clientのインポート
import { PrismaClient } from "@prisma/client";
const prismaClientSingleton = () => {
return new PrismaClient({
log: ["query"],
});
};
type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>;
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClientSingleton | undefined;
};
const prisma = globalForPrisma.prisma ?? prismaClientSingleton();
export default prisma;
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
動作確認
できたら、サーバーを立ち上げます
npm run dev
http://localhost:3000/ にアクセスして、↓ のように表示されると思います
各項目を埋めて「追加」ボタンをクリックしてください。
All Users の下に登録したユーザーが表示されれば、データの読み書きともにうまくいってそうです。
(例えば ↓ のような感じです)
動作確認が終わったら、Ctrl + C 等でアプリを終了させてください
Prisma Studio をつかってみる
**Prisma Studio(データベース内のデータを表示および編集するための GUI)**を起動させてみます。
npx prisma studio
すると、ブラウザに自動で表示されます
ここから、テーブルやレコードの状態を確認できます
最後に
Prisma のチュートリアルを通して、Next.js で使ってみました
OR Mは便利ですねーー!
Prisma を GraphQL と組み合わせても面白そうなので、挑戦してみます。
本記事でさくせいしたアプリを Vercel へデプロイしてみたい方はこちらもチェック
参考文献
Discussion