はじめに
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ルートを組み合わせることで、完全なバックエンドシステムを構築できます。