Skip to content
Code Blue プログラミング学習ノート
Javascript/Next.js

Next.jsでPrismaを使ったCRUD操作完全ガイド

はじめに

Next.jsアプリケーションでPrismaを使用してデータベースのCRUD(Create, Read, Update, Delete)操作を行う方法を、初心者向けに徹底解説します。Prismaのセットアップやモデルの作成、マイグレーション、API Route・Server Actionsを利用したデータ操作、一覧表示や編集・削除機能の実装までを順を追って学習します。

実際にメモ帳アプリを開発しながら、実践的なデータベース連携の流れを理解し、Next.jsで本格的なWebアプリケーションを開発するための基礎を身に付けます。

Prisma Clientの基本

Prismaは、データベースを簡単に操作できる現代的なORM(Object-Relational Mapping)ツールです。TypeScriptと相性が良く、Next.jsアプリケーションとの統合もスムーズに行えます。Prisma ORMのセットアップは以前の記事に書いてます。

セットアップで準備したPrisma Clientを使用して、データベース操作を行います。まず、Prisma Clientの基本的な使い方を確認しましょう。

import prisma from '../lib/prisma'

// すべてのユーザーを取得
const allUsers = await prisma.user.findMany()

Prisma Clientは、モデルごとに自動生成されたメソッドを提供します。これらのメソッドは強力な型サポートがあり、コードを書く際に自動補完が効くのが特徴です。

CREATE操作 – データの作成

単一レコードの作成

create()メソッドは、Prismaでデータベースに新しいレコードを追加するためのメソッドです。以下の例では、userテーブルに「山田太郎」という名前と「taro@example.com」というメールアドレスを持つユーザーを登録しています。登録するデータはdataプロパティにオブジェクト形式で指定します。

// 新しいユーザーを作成
const newUser = await prisma.user.create({
  data: {
    name: '山田太郎',
    email: 'taro@example.com',
  },
})
console.log(newUser)

処理が成功すると、新しく作成されたレコードが戻り値として返され、idやcreatedAtなど、データベースによって自動生成された値も取得できます。そのため、登録後に作成されたユーザー情報を画面へ表示したり、後続の処理で利用したりすることが可能です。

複数レコードの作成

createMany()メソッドを使用して複数のユーザーデータを一括で登録しています。dataに登録するレコードを配列で指定し、skipDuplicates: trueを設定することで、重複するデータがあった場合はエラーを発生させずにスキップします。処理後は、実際に登録されたレコード数をcountから取得して表示しています。

// 複数のユーザーを一括作成
const createdUsers = await prisma.user.createMany({
  data: [
    { name: '鈴木一郎', email: 'suzuki@example.com' },
    { name: '佐藤花子', email: 'sato@example.com' },
  ],
  skipDuplicates: true, // 重複をスキップ
})
console.log(`作成されたユーザー数: ${createdUsers.count}`)

リレーションのあるデータの作成

create()メソッドを利用してユーザーと投稿データを同時に作成しています。postsのcreateを使用することで、関連する投稿レコードも一緒に登録できます。また、includeにposts: trueを指定しているため、作成されたユーザー情報だけでなく、関連する投稿データもあわせて取得できます。

// ユーザーと投稿を同時に作成
const userWithPost = await prisma.user.create({
  data: {
    name: '田中健太',
    email: 'tanaka@example.com',
    posts: {
      create: {
        title: '初めての投稿',
        content: 'Prismaを使い始めました!',
      },
    },
  },
  include: {
    posts: true, // 作成された投稿も返す
  },
})

READ操作 – データの読み取り

すべてのレコードを取得

findMany()メソッドは、指定したテーブルに登録されているすべてのレコードを取得します。検索条件を指定しない場合は、テーブル内の全データが配列として返されます。

// すべてのユーザーを取得
const allUsers = await prisma.user.findMany()

条件を指定して取得

whereオプションを使用すると、指定した条件に一致するレコードだけを取得できます。この例では、publishedがtrueの投稿のみを検索しています。

// 公開済みの投稿のみ取得
const publishedPosts = await prisma.post.findMany({
  where: {
    published: true,
  },
})

特定のフィールドのみ選択

selectオプションを使用すると、必要なフィールドだけを取得できます。不要なデータを取得しないため、処理効率の向上につながります。

// 名前とメールのみ取得
const users = await prisma.user.findMany({
  select: {
    name: true,
    email: true,
  },
})

単一レコードの取得

findUnique()メソッドは、主キーや一意制約のある値を指定して、1件のレコードを取得します。この例では、idが1のユーザーを検索しています。

// IDでユーザーを検索
const user = await prisma.user.findUnique({
  where: {
    id: 1,
  },
})

リレーションを含めて取得

includeオプションを使用すると、関連するテーブルのデータも同時に取得できます。この例では、ユーザー情報と、そのユーザーが投稿したデータをまとめて取得しています。

// ユーザーとその投稿を取得
const userWithPosts = await prisma.user.findUnique({
  where: {
    id: 1,
  },
  include: {
    posts: true,
  },
})

ソートとページネーション

orderByを使用すると取得結果を並び替えることができ、takeで取得件数を制限できます。また、cursor・skip・takeを組み合わせることで、カーソルベースのページネーションを実装し、効率よくデータを分割して取得できます。

// 作成日降順で10件取得
const recentPosts = await prisma.post.findMany({
  orderBy: {
    createdAt: 'desc',
  },
  take: 10, // 取得件数
})

// ページネーション(カーソルベース)
const secondPage = await prisma.post.findMany({
  cursor: {
    id: 10, // 最後に取得したレコードのID
  },
  skip: 1, // カーソルのレコードをスキップ
  take: 5, // 1ページあたりの件数
})

UPDATE操作 – データの更新

単一レコードの更新

update()メソッドは、指定した1件のレコードを更新します。whereで更新対象を指定し、dataに変更したい内容を設定します。

// ユーザー情報を更新
const updatedUser = await prisma.user.update({
  where: {
    id: 1,
  },
  data: {
    name: '山田太郎(更新)',
  },
})

複数レコードの更新

updateMany()メソッドは、条件に一致する複数のレコードをまとめて更新します。この例では、未公開の投稿を一括で公開状態に変更しています。

// すべての未公開の投稿を公開状態に
const updatedPosts = await prisma.post.updateMany({
  where: {
    published: false,
  },
  data: {
    published: true,
  },
})

カウンタのインクリメント

incrementを使用すると、数値フィールドの値を指定した分だけ増やすことができます。同様に、decrementを使えば値を減らすことも可能です。

// 投稿の閲覧数を増やす
await prisma.post.update({
  where: {
    id: 1,
  },
  data: {
    viewCount: {
      increment: 1, // 1増やす
    },
  },
})

リレーションの更新

リレーションを利用すると、親データと関連する子データを同時に更新できます。この例では、ユーザー名を変更するとともに、未公開の投稿をまとめて公開状態へ更新しています。

// ユーザーとその投稿を更新
await prisma.user.update({
  where: {
    id: 1,
  },
  data: {
    name: '新しい名前',
    posts: {
      updateMany: {
        where: {
          published: false,
        },
        data: {
          published: true,
        },
      },
    },
  },
})

DELETE操作 – データの削除

単一レコードの削除

delete()メソッドは、指定した1件のレコードを削除します。whereで削除対象を指定して実行します。

// IDが1のユーザーを削除
await prisma.user.delete({
  where: {
    id: 1,
  },
})

複数レコードの削除

deleteMany()メソッドは、条件に一致する複数のレコードを一括で削除します。この例では、公開済みの投稿をまとめて削除しています。

// 公開済みの投稿をすべて削除
await prisma.post.deleteMany({
  where: {
    published: true,
  },
})

全レコードの削除

deleteMany()を条件なしで実行すると、指定したテーブル内のすべてのレコードを削除できます。

// すべての投稿を削除
await prisma.post.deleteMany()

トランザクション処理

$transaction()を使用すると、複数のデータベース操作を1つの処理として実行できます。途中でエラーが発生した場合は、すべての変更が取り消されるため、データの整合性を保つことができます。

// トランザクションを使用したユーザー移行
const transfer = await prisma.$transaction([
  prisma.user.delete({
    where: { id: 1 },
  }),
  prisma.user.create({
    data: {
      name: '新しいユーザー',
      email: 'new@example.com',
    },
  }),
])

エラーハンドリング

Prismaの処理はtry-catchで囲むことで、発生したエラーを適切に処理できます。この例では、一意制約違反(P2002)を検出し、メールアドレスの重複を判定しています。

try {
  const user = await prisma.user.create({
    data: {
      email: 'existing@example.com', // 既に存在する場合
    },
  })
} catch (error) {
  if (error instanceof Prisma.PrismaClientKnownRequestError) {
    if (error.code === 'P2002') {
      console.error('一意制約違反: このメールアドレスは既に使用されています')
    }
  }
  throw error
}

高度なクエリテクニック

集計関数

aggregate()メソッドを使用すると、レコード数や平均値、最大値などの集計結果を取得できます。データの分析や統計情報を表示したい場合に便利です。

// 投稿数の集計
const postStats = await prisma.post.aggregate({
  _count: true, // 総数
  _avg: {
    viewCount: true, // 平均閲覧数
  },
  _max: {
    viewCount: true, // 最大閲覧数
  },
})

全文検索(フルテキストサーチ)

searchを使用すると、指定したキーワードを含むデータを検索できます。この例では、投稿内容から「Prisma ORM」を含むレコードを取得しています。

// 投稿内容で検索
const searchResults = await prisma.post.findMany({
  where: {
    content: {
      search: 'Prisma ORM', // 検索キーワード
    },
  },
})

生SQLクエリ

$queryRawを使用すると、Prismaでは表現しにくい複雑なSQLを直接実行できます。柔軟な検索や高度なデータ操作を行いたい場合に利用します。

const result = await prisma.$queryRaw`
  SELECT * FROM "User" WHERE "name" LIKE ${'%山田%'}
`

Next.js APIルートでの使用例

実際にNext.jsのAPIルートでPrismaを使用する例を示します。Next.jsのAPI RouteでPrismaを利用し、ユーザーデータの取得と登録を行うAPIを実装しています。

GETリクエストではfindMany()で全ユーザーを取得し、POSTリクエストではcreate()を使って新しいユーザーを登録します。それ以外のHTTPメソッドが送信された場合は、405 Method Not Allowedを返し、利用可能なメソッドを通知しています。

pages/api/users.js:

import prisma from '../../lib/prisma'

export default async function handler(req, res) {
  switch (req.method) {
    case 'GET':
      const users = await prisma.user.findMany()
      res.status(200).json(users)
      break
    case 'POST':
      const newUser = await prisma.user.create({
        data: req.body,
      })
      res.status(201).json(newUser)
      break
    default:
      res.setHeader('Allow', ['GET', 'POST'])
      res.status(405).end(`Method ${req.method} Not Allowed`)
  }
}

パフォーマンス最適化のヒント

必要なフィールドのみ選択

selectを使用すると、必要なフィールドだけを取得できます。取得するデータ量を減らせるため、処理速度の向上や通信量の削減につながります。

   await prisma.user.findMany({
     select: { id: true, name: true },
   })

バッチ処理の活用

関連データを個別に取得するとN+1問題が発生し、処理効率が低下します。includeを利用して関連データを一括取得することで、データベースへのアクセス回数を減らし、パフォーマンスを向上できます。

   // 悪い例: N+1問題
   for (const user of users) {
     const posts = await prisma.post.findMany({
       where: { authorId: user.id },
     })
   }

   // 良い例: 一括取得
   const usersWithPosts = await prisma.user.findMany({
     include: { posts: true },
   })

インデックスの活用

検索頻度の高いフィールドにインデックスを設定すると、データ検索を高速化できます。この例では、nameフィールドにインデックスを作成し、検索性能を向上させています。

   model User {
     id    Int     @id
     email String  @unique
     name  String

     @@index([name]) // 検索頻度の高いフィールドにインデックス
   }

テストの書き方

Prisma操作のテスト例が以下になります。このコードは、Jestを使用してPrismaのCRUD処理をテストする例です。

beforeAll()でテスト実行前にユーザーデータを削除し、test()では新しいユーザーを作成して名前が正しく登録されたかをexpect()で検証しています。最後にafterAll()で$disconnect()を実行し、データベースとの接続を終了しています。

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

describe('User CRUD', () => {
  beforeAll(async () => {
    await prisma.user.deleteMany()
  })

  test('create user', async () => {
    const user = await prisma.user.create({
      data: { name: 'Test User', email: 'test@example.com' },
    })
    expect(user.name).toBe('Test User')
  })

  afterAll(async () => {
    await prisma.$disconnect()
  })
})

プロダクション環境での注意点

接続プーリングの設定

DATABASE_URLにconnection_limitを指定すると、データベースへの同時接続数を制限できます。接続数を適切に管理することで、サーバーの負荷を抑え、安定した運用につながります。

   DATABASE_URL="postgresql://user:password@localhost:5432/db?connection_limit=5"

長時間実行クエリの回避

AbortControllerを利用すると、一定時間を超えたクエリを中断できます。タイムアウトを設定することで、長時間実行される処理によるパフォーマンス低下を防ぐことができます。

   // タイムアウト設定
   const controller = new AbortController()
   setTimeout(() => controller.abort(), 5000)

   await prisma.$queryRaw`SELECT pg_sleep(10)`, {
     signal: controller.signal,
   })

ロギングの設定

PrismaClientのlogオプションを設定すると、実行されたクエリや情報、警告、エラーなどをログとして出力できます。開発時のデバッグやトラブルシューティングに役立ちます。

   const prisma = new PrismaClient({
     log: ['query', 'info', 'warn', 'error'],
   })

まとめ

このガイドでは、Next.jsアプリケーションでPrismaを使用してCRUD操作を行う方法を詳細に解説しました。Prismaの強力なクエリAPIを活用すれば、複雑なデータベース操作も簡単に実装できます。

次のステップとして、これらのCRUD操作を組み込んだREST APIの実装方法を学びましょう。PrismaとNext.js APIルートを組み合わせることで、完全なバックエンドシステムを構築できます。