Prisma Client の使い方:基本クエリから応用まで

データベース操作の新しい時代へようこそ。あなたが今まで感じていた「なぜ SQL は複雑なのか」「なぜ型エラーが頻発するのか」といった悩みを、Prisma Client が一挙に解決してくれるでしょう。
現代の Web アプリケーション開発において、データベースとの連携は避けて通れない道です。しかし、従来のアプローチでは開発者を悩ませる多くの課題がありました。Prisma Client は、そんな課題を解決し、開発体験を劇的に向上させる革新的なツールなのです。
この記事では、Prisma Client の基本的な使い方から実践的な応用まで、段階的に学んでいただけるよう構成しています。あなたの開発スキルが一段階上がることをお約束いたします。
背景
従来の ORM/データベースアクセスの課題
これまでの Web アプリケーション開発では、データベースとの連携において多くの困難がありました。生の SQL を書く場合、複雑なクエリになると可読性が著しく低下し、メンテナンスが困難になります。
また、従来の ORM を使用する場合でも、以下のような問題が頻繁に発生していました。
# | 課題 | 具体的な問題 |
---|---|---|
1 | 型安全性の不足 | 実行時まで型エラーが発見されない |
2 | SQL インジェクション | セキュリティリスクの増大 |
3 | N+1 問題 | パフォーマンスの劣化 |
4 | 設定の複雑さ | 初期設定に時間がかかる |
Prisma Client が解決する問題
Prisma Client は、これらの課題を根本的に解決します。特に注目すべきは、型安全性と開発者体験の向上です。
コンパイル時に型チェックが行われるため、データベーススキーマとコードの不整合を事前に検出できます。これにより、本番環境でのトラブルを大幅に減らすことができるのです。
課題
型安全性の欠如
従来のデータベースアクセスでは、以下のようなコードが一般的でした。
javascript// 従来のアプローチ - 型安全ではない
const user = await db.query(
'SELECT * FROM users WHERE id = ?',
[userId]
);
console.log(user.nmae); // タイポがあってもコンパイル時に検出されない
このコードの問題点は、user.nmae
のタイポがコンパイル時に検出されないことです。実行時になって初めてエラーが発生し、本番環境でのトラブルの原因となっていました。
複雑なクエリの記述
リレーションを含む複雑なクエリを書く際、SQL の記述が非常に煩雑になります。
sql-- 複雑なJOINクエリの例
SELECT u.*, p.title, p.content, c.name as category_name
FROM users u
LEFT JOIN posts p ON u.id = p.author_id
LEFT JOIN categories c ON p.category_id = c.id
WHERE u.active = 1
ORDER BY p.created_at DESC
LIMIT 10;
このようなクエリは可読性が低く、メンテナンスが困難です。また、スキーマが変更された際の修正漏れも発生しやすくなります。
パフォーマンスの最適化
N+1 問題は、特に深刻なパフォーマンス課題でした。
javascript// N+1問題の例
const users = await User.findAll();
for (const user of users) {
// 各ユーザーに対してクエリが実行される(N+1問題)
const posts = await Post.findAll({
where: { authorId: user.id },
});
user.posts = posts;
}
解決策
Prisma Client の特徴と利点
Prisma Client は、これらの課題を以下の特徴で解決します。
# | 特徴 | 利点 |
---|---|---|
1 | 型安全な自動生成 | コンパイル時の型チェック |
2 | 直感的な API | 学習コストの低減 |
3 | 自動最適化 | N+1 問題の自動解決 |
4 | リレーション操作 | 複雑なクエリの簡素化 |
型安全なデータベースアクセス
Prisma Client を使用すると、以下のように型安全なコードが書けます。
typescript// Prisma Client - 型安全なアプローチ
const user = await prisma.user.findUnique({
where: { id: userId },
});
console.log(user?.name); // 型チェックにより、タイポがコンパイル時に検出される
この例では、user
オブジェクトの型が自動的に推論され、存在しないプロパティにアクセスしようとするとコンパイルエラーが発生します。
具体例
基本的な CRUD 操作
セットアップとインストール
まず、プロジェクトに Prisma を導入しましょう。以下のコマンドで必要なパッケージをインストールします。
bash# Prismaのインストール
yarn add prisma @prisma/client
# Prisma CLIの初期化
yarn prisma init
次に、schema.prisma
ファイルでデータベースモデルを定義します。
prisma// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // または "mysql", "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
スキーマを定義した後、以下のコマンドで Prisma Client を生成します。
bash# データベースマイグレーション
yarn prisma migrate dev --name init
# Prisma Clientの生成
yarn prisma generate
Create(作成)操作
新しいデータを作成する際の Prisma Client の使い方をご紹介します。
typescript// ユーザーの作成
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const createUser = async () => {
try {
const user = await prisma.user.create({
data: {
email: 'john@example.com',
name: 'John Doe',
},
});
console.log('ユーザーが正常に作成されました:', user);
return user;
} catch (error) {
console.error('ユーザー作成エラー:', error);
throw error;
}
};
関連データと一緒に作成する場合は、ネストした create 操作を使用します。
typescript// ユーザーと投稿を同時に作成
const createUserWithPost = async () => {
try {
const userWithPost = await prisma.user.create({
data: {
email: 'jane@example.com',
name: 'Jane Smith',
posts: {
create: [
{
title: '初投稿',
content: 'Prisma Clientを使ってみました!',
published: true,
},
],
},
},
include: {
posts: true, // 作成された投稿も含めて返す
},
});
return userWithPost;
} catch (error) {
if (error.code === 'P2002') {
console.error('メールアドレスが既に使用されています');
}
throw error;
}
};
Read(読み取り)操作
データの取得方法について詳しく見ていきましょう。
typescript// 単一のユーザーを取得
const getUser = async (userId: number) => {
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
posts: {
where: { published: true }, // 公開済みの投稿のみ
orderBy: { createdAt: 'desc' },
},
},
});
if (!user) {
throw new Error(
`ユーザーID ${userId} が見つかりません`
);
}
return user;
};
複数のレコードを取得する場合の例です。
typescript// 複数のユーザーを条件付きで取得
const getUsers = async (
page: number = 1,
limit: number = 10
) => {
const skip = (page - 1) * limit;
const users = await prisma.user.findMany({
where: {
posts: {
some: { published: true }, // 公開済み投稿があるユーザーのみ
},
},
include: {
_count: {
select: { posts: true }, // 投稿数を含める
},
},
orderBy: { createdAt: 'desc' },
skip,
take: limit,
});
return users;
};
Update(更新)操作
データの更新処理を実装してみましょう。
typescript// 単一のユーザー情報を更新
const updateUser = async (
userId: number,
data: { name?: string; email?: string }
) => {
try {
const updatedUser = await prisma.user.update({
where: { id: userId },
data,
include: {
posts: { take: 5 }, // 最新の5投稿を含める
},
});
return updatedUser;
} catch (error) {
if (error.code === 'P2025') {
throw new Error('更新対象のユーザーが見つかりません');
}
if (error.code === 'P2002') {
throw new Error(
'指定されたメールアドレスは既に使用されています'
);
}
throw error;
}
};
複数のレコードを一括更新する場合の例です。
typescript// 複数の投稿を一括更新
const publishPosts = async (authorId: number) => {
const result = await prisma.post.updateMany({
where: {
authorId,
published: false,
},
data: {
published: true,
},
});
console.log(`${result.count}件の投稿が公開されました`);
return result;
};
Delete(削除)操作
安全なデータ削除の実装方法をご紹介します。
typescript// 単一の投稿を削除
const deletePost = async (postId: number) => {
try {
const deletedPost = await prisma.post.delete({
where: { id: postId },
});
console.log('投稿が削除されました:', deletedPost.title);
return deletedPost;
} catch (error) {
if (error.code === 'P2025') {
throw new Error('削除対象の投稿が見つかりません');
}
throw error;
}
};
関連データと一緒に削除する場合の例です。
typescript// ユーザーとその投稿を全て削除
const deleteUserWithPosts = async (userId: number) => {
// トランザクションを使用して安全に削除
const result = await prisma.$transaction(async (tx) => {
// まず投稿を削除
const deletedPosts = await tx.post.deleteMany({
where: { authorId: userId },
});
// その後ユーザーを削除
const deletedUser = await tx.user.delete({
where: { id: userId },
});
return {
deletedUser,
deletedPostsCount: deletedPosts.count,
};
});
return result;
};
中級クエリ
リレーション操作
Prisma Client の真の力は、リレーション操作で発揮されます。複雑なデータ構造も直感的に扱えるのです。
typescript// 深いネストのリレーションデータを取得
const getUserWithDetailedPosts = async (userId: number) => {
const user = await prisma.user.findUnique({
where: { id: userId },
include: {
posts: {
include: {
comments: {
include: {
author: {
select: { name: true, email: true }, // 必要なフィールドのみ選択
},
},
},
_count: {
select: { comments: true, likes: true },
},
},
},
},
});
return user;
};
リレーションの作成と更新を同時に行う例です。
typescript// 既存のユーザーに新しい投稿を関連付け
const addPostToUser = async (
userId: number,
postData: { title: string; content: string }
) => {
const updatedUser = await prisma.user.update({
where: { id: userId },
data: {
posts: {
create: {
title: postData.title,
content: postData.content,
published: false,
},
},
},
include: {
posts: {
orderBy: { createdAt: 'desc' },
take: 1, // 最新の投稿のみ
},
},
});
return updatedUser;
};
フィルタリングとソート
高度な検索機能を実装してみましょう。
typescript// 複雑な条件でのフィルタリング
const searchPosts = async (searchParams: {
keyword?: string;
authorName?: string;
published?: boolean;
dateFrom?: Date;
dateTo?: Date;
}) => {
const {
keyword,
authorName,
published,
dateFrom,
dateTo,
} = searchParams;
const posts = await prisma.post.findMany({
where: {
AND: [
// キーワード検索(タイトルまたは内容)
keyword
? {
OR: [
{
title: {
contains: keyword,
mode: 'insensitive',
},
},
{
content: {
contains: keyword,
mode: 'insensitive',
},
},
],
}
: {},
// 著者名での検索
authorName
? {
author: {
name: {
contains: authorName,
mode: 'insensitive',
},
},
}
: {},
// 公開状態での絞り込み
published !== undefined ? { published } : {},
// 日付範囲での絞り込み
dateFrom ? { createdAt: { gte: dateFrom } } : {},
dateTo ? { createdAt: { lte: dateTo } } : {},
],
},
include: {
author: {
select: { name: true },
},
_count: {
select: { comments: true },
},
},
orderBy: [
{ published: 'desc' }, // 公開済みを優先
{ createdAt: 'desc' }, // 新しい順
],
});
return posts;
};
ページネーション
効率的なページネーション機能を実装します。
typescript// カーソルベースのページネーション
const getPostsWithCursorPagination = async (
cursor?: number,
limit: number = 10
) => {
const posts = await prisma.post.findMany({
take: limit + 1, // 次のページがあるかチェック用に+1
cursor: cursor ? { id: cursor } : undefined,
skip: cursor ? 1 : 0, // カーソルレコード自体をスキップ
where: { published: true },
include: {
author: {
select: { name: true },
},
},
orderBy: { createdAt: 'desc' },
});
const hasNextPage = posts.length > limit;
const postsToReturn = hasNextPage
? posts.slice(0, -1)
: posts;
const nextCursor = hasNextPage
? postsToReturn[postsToReturn.length - 1].id
: null;
return {
posts: postsToReturn,
hasNextPage,
nextCursor,
};
};
オフセットベースのページネーションも実装できます。
typescript// オフセットベースのページネーション(総数付き)
const getPostsWithOffsetPagination = async (
page: number = 1,
limit: number = 10
) => {
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
prisma.post.findMany({
skip,
take: limit,
where: { published: true },
include: {
author: {
select: { name: true },
},
},
orderBy: { createdAt: 'desc' },
}),
prisma.post.count({
where: { published: true },
}),
]);
const totalPages = Math.ceil(total / limit);
return {
posts,
pagination: {
currentPage: page,
totalPages,
totalItems: total,
itemsPerPage: limit,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
},
};
};
応用クエリ
トランザクション
データの整合性を保つトランザクション処理を実装しましょう。
typescript// 複雑なビジネスロジックをトランザクションで処理
const transferUserPosts = async (
fromUserId: number,
toUserId: number
) => {
try {
const result = await prisma.$transaction(async (tx) => {
// 移譲元ユーザーの確認
const fromUser = await tx.user.findUnique({
where: { id: fromUserId },
include: { _count: { select: { posts: true } } },
});
if (!fromUser) {
throw new Error('移譲元ユーザーが見つかりません');
}
// 移譲先ユーザーの確認
const toUser = await tx.user.findUnique({
where: { id: toUserId },
});
if (!toUser) {
throw new Error('移譲先ユーザーが見つかりません');
}
// 投稿の移譲
const updatedPosts = await tx.post.updateMany({
where: { authorId: fromUserId },
data: { authorId: toUserId },
});
// ログの作成
await tx.transferLog.create({
data: {
fromUserId,
toUserId,
postsCount: updatedPosts.count,
transferredAt: new Date(),
},
});
return {
transferredPosts: updatedPosts.count,
fromUser: fromUser.name,
toUser: toUser.name,
};
});
return result;
} catch (error) {
console.error('投稿移譲エラー:', error);
throw error;
}
};
集計関数
データの分析に便利な集計機能を活用しましょう。
typescript// ユーザー統計情報の取得
const getUserStatistics = async () => {
const stats = await prisma.user.aggregate({
_count: {
id: true,
},
_avg: {
posts: {
_count: true,
},
},
_max: {
createdAt: true,
},
_min: {
createdAt: true,
},
});
// 月別のユーザー登録数
const monthlyStats = await prisma.$queryRaw`
SELECT
DATE_TRUNC('month', "createdAt") as month,
COUNT(*) as user_count
FROM "User"
GROUP BY DATE_TRUNC('month', "createdAt")
ORDER BY month DESC
LIMIT 12
`;
return {
totalUsers: stats._count.id,
averagePostsPerUser: stats._avg.posts?._count || 0,
oldestUser: stats._min.createdAt,
newestUser: stats._max.createdAt,
monthlyRegistrations: monthlyStats,
};
};
グループ化された集計も簡単に実行できます。
typescript// 投稿の統計情報(著者別)
const getPostStatsByAuthor = async () => {
const authorStats = await prisma.post.groupBy({
by: ['authorId'],
_count: {
id: true,
},
_avg: {
viewCount: true,
},
having: {
id: {
_count: {
gt: 5, // 5投稿以上の著者のみ
},
},
},
orderBy: {
_count: {
id: 'desc',
},
},
});
// 著者情報を追加
const enrichedStats = await Promise.all(
authorStats.map(async (stat) => {
const author = await prisma.user.findUnique({
where: { id: stat.authorId },
select: { name: true, email: true },
});
return {
author,
postCount: stat._count.id,
averageViews: stat._avg.viewCount,
};
})
);
return enrichedStats;
};
Raw クエリ
Prisma Client では、必要に応じて生の SQL クエリも実行できます。
typescript// 複雑な分析クエリの実行
const getAdvancedAnalytics = async (
startDate: Date,
endDate: Date
) => {
try {
const result = await prisma.$queryRaw`
WITH post_metrics AS (
SELECT
p.id,
p.title,
p."authorId",
u.name as author_name,
p."createdAt",
COUNT(c.id) as comment_count,
COUNT(l.id) as like_count
FROM "Post" p
LEFT JOIN "User" u ON p."authorId" = u.id
LEFT JOIN "Comment" c ON p.id = c."postId"
LEFT JOIN "Like" l ON p.id = l."postId"
WHERE p."createdAt" BETWEEN ${startDate} AND ${endDate}
GROUP BY p.id, p.title, p."authorId", u.name, p."createdAt"
)
SELECT
author_name,
COUNT(*) as post_count,
AVG(comment_count) as avg_comments,
AVG(like_count) as avg_likes,
MAX(comment_count + like_count) as max_engagement
FROM post_metrics
GROUP BY author_name
ORDER BY avg_likes DESC
LIMIT 10
`;
return result;
} catch (error) {
console.error('分析クエリエラー:', error);
throw error;
}
};
型安全な Raw クエリの実行方法もあります。
typescript// 型安全なRawクエリ
interface PostAnalytics {
authorName: string;
postCount: bigint;
avgComments: number;
avgLikes: number;
}
const getTypedAnalytics = async (): Promise<
PostAnalytics[]
> => {
const result = await prisma.$queryRaw<PostAnalytics[]>`
SELECT
u.name as "authorName",
COUNT(p.id) as "postCount",
AVG(COALESCE(comment_stats.comment_count, 0)) as "avgComments",
AVG(COALESCE(like_stats.like_count, 0)) as "avgLikes"
FROM "User" u
LEFT JOIN "Post" p ON u.id = p."authorId"
LEFT JOIN (
SELECT "postId", COUNT(*) as comment_count
FROM "Comment"
GROUP BY "postId"
) comment_stats ON p.id = comment_stats."postId"
LEFT JOIN (
SELECT "postId", COUNT(*) as like_count
FROM "Like"
GROUP BY "postId"
) like_stats ON p.id = like_stats."postId"
WHERE p.published = true
GROUP BY u.id, u.name
HAVING COUNT(p.id) > 0
ORDER BY "avgLikes" DESC
`;
return result;
};
バッチ処理
大量のデータを効率的に処理するバッチ操作を実装しましょう。
typescript// 大量のユーザーデータを一括作成
const createUsersInBatch = async (
userData: Array<{ email: string; name: string }>
) => {
const batchSize = 1000; // 1000件ずつ処理
const results = [];
for (let i = 0; i < userData.length; i += batchSize) {
const batch = userData.slice(i, i + batchSize);
try {
const batchResult = await prisma.user.createMany({
data: batch,
skipDuplicates: true, // 重複をスキップ
});
results.push(batchResult);
console.log(
`バッチ ${Math.floor(i / batchSize) + 1}: ${
batchResult.count
}件作成`
);
// メモリを解放するため少し待機
await new Promise((resolve) =>
setTimeout(resolve, 100)
);
} catch (error) {
console.error(
`バッチ ${Math.floor(i / batchSize) + 1} でエラー:`,
error
);
throw error;
}
}
const totalCreated = results.reduce(
(sum, result) => sum + result.count,
0
);
return { totalCreated, batchCount: results.length };
};
データの一括更新処理も効率的に実行できます。
typescript// 条件に基づく一括更新
const updateInactiveUsers = async () => {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
// 30日間ログインしていないユーザーを非アクティブに
const result = await prisma.user.updateMany({
where: {
lastLoginAt: {
lt: thirtyDaysAgo,
},
active: true,
},
data: {
active: false,
deactivatedAt: new Date(),
},
});
console.log(
`${result.count}人のユーザーが非アクティブになりました`
);
// 非アクティブユーザーの投稿も非公開に
const postResult = await prisma.post.updateMany({
where: {
author: {
active: false,
},
published: true,
},
data: {
published: false,
},
});
return {
deactivatedUsers: result.count,
unpublishedPosts: postResult.count,
};
};
まとめ
Prisma Client は、現代の Web アプリケーション開発において必須のツールといっても過言ではありません。型安全性、直感的な API、そして強力な機能により、開発者の生産性を大幅に向上させてくれます。
この記事で学んだ内容を振り返ってみましょう。
# | 学習内容 | 得られる価値 |
---|---|---|
1 | 基本的な CRUD 操作 | 安全で効率的なデータ操作 |
2 | リレーション操作 | 複雑なデータ構造の簡単な扱い |
3 | 高度なクエリ | 柔軟で強力なデータ取得 |
4 | トランザクション | データ整合性の保証 |
5 | 集計と Raw クエリ | 分析機能とパフォーマンス最適化 |
Prisma Client を使いこなすことで、あなたの開発スキルは確実に向上します。エラーハンドリングも含めた実践的なコードを書けるようになり、保守性の高いアプリケーションを構築できるようになるでしょう。
今日から始めて、Prisma Client の威力を実感してください。きっと、従来のデータベース操作には戻れなくなるはずです。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来