0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Next.js】PrismaをNext.jsに導入する

0
Last updated at Posted at 2026-01-31

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の設定ファイル)

prisma.config.ts
// 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に利用するので providerpostgresql となります。

prisma/schema.prisma
// 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 オプションを追加する必要があります。

.env
DATABASE_URL="postgresql://app:root1234@react-work-sample-postgresql:5432/sample?schema=public"

2. Prismaスキーマの定義

prisma/schema.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.tsmigrations.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.prismaclient.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 にアクセス

スクリーンショット 2025-12-08 14.59.07.png (83.9 kB)

4. 初期データの登録

生成したクライアントコードを利用して、データベースに初期データを登録します。

prisma/seed.ts
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がこのスクリプトをどのように実行すべきかを指定します

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"),
  },
});

seed scriptの実行

pnpm prisma db seed

データの確認

pnpm prisma studio

5. Prisma Clientのラッパーを実装

DBにアクセスするたびにクライアントインスタンスを生成するのでは、DBへの無駄な接続が増えます。クライアントインスタンスはシングルトンとして、ワーカーで一つの接続を使い回すようにします。

src/lib/prisma.ts
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でクエリを実行してみる

ユーザー一覧ページ

src/app/page.tsx
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>
  );
}

スクリーンショット 2025-12-08 15.06.29.png (18.2 kB)

投稿一覧ページ

src/app/posts/page.tsx
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>
  );
}
スクリーンショット 2025-12-08 15.09.51.png (28.5 kB)

投稿の詳細ページ

src/app/posts/[id]/page.tsx
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>
  )

}
スクリーンショット 2025-12-08 15.11.08.png (25.1 kB)

投稿の新規作成ページ

src/app/posts/new/page.tsx
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>

  )
}
スクリーンショット 2025-12-08 15.11.40.png (37.0 kB)
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?