ゼロからPrismaを導入して、Prismaの基本をマスターしよう | 技術ブログ
はじめに前提環境Prismaの導入下準備DB関連設定テーブルの生成データベースの操作下準備データの登録データの取得データの更新データの削除まとめはじめにPrismaはNode.jsとTypeSc...
https://www.wantedly.com/companies/jointcrew/post_articles/936508
はじめに
1対1のリレーション
データモデルの定義
1対多のリレーション
データモデルの定義
リレーション関連のDB操作
事前準備
リレーション定義されたデータの取得
データの同時登録
Cascadeによるデータの削除
まとめ
前回はPrismaの基本を学ぶ記事を書きました。
今回はPrismaのDB操作の応用編シリーズの第1回目として、リレーション関連について解説します。
今後は複雑なクエリや、データ整合性・パフォーマンスについても順次説明していく予定です。
なお、本記事は前回の記事と同じ環境で動かすことを想定しています。
schema.prismaにUserとProfileのモデルを定義します。
1人のユーザーに対してプロフィール情報は1つだけなので、1対1のリレーションとなります。
// 他の定義は省略
model User {
id Int @id @default(autoincrement())
email String @unique
name String
profile Profile?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Profile {
id Int @id @default(autoincrement())
bio String?
birthDate DateTime?
phoneNumber String?
address String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
リレーション定義におけるポイントは2つです。
関係フィールドの定義
Userモデルに定義したprofile、Profileモデルに定義したuserが関係フィールドです。
これらはテーブル生成時、カラムとして設定されるものではありません。
Prisma上でリレーションを定義するための情報です。
フィールド名は任意で、型としてリレーションを設定したいモデル名を指定します。
@relationの定義
リレーションの参照元の関係フィールドにrelation属性を指定します。
これにより、テーブル同士の依存関係を定義します。
fields
外部キーの参照元となるカラムを指定します。
references
外部キーの参照先となるカラムを指定します。
onDelete
参照先のデータが削除されたときの挙動を指定します。
ここでは「Cascade」を指定することで、参照先のデータが削除された場合、関連するデータも自動的に削除されるようにしています。
他にもいくつか設定可能なオプションがありますが、今回は上記の3つを使用します。
ブログなどの投稿を想定して、Postモデルを定義してみましょう。
schema.prismaにPostモデルを定義、Userモデルに対して定義を追加します。
1人のユーザーは複数の投稿ができるので、1対多のリレーションとなります。
// 他の定義は省略
model User {
id Int @id @default(autoincrement())
email String @unique
name String
profile Profile?
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], onDelete: Cascade)
authorId Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
基本的には1対1のリレーションと変わりませんが、1つだけ違いがあります。
関係フィールドの定義
1対多のリレーションの場合、関係フィールドの型部分を配列として定義します。
これにより、1つのユーザーデータに対して複数のポストデータを紐づけるよう定義できます。
ここまで完了すれば、リレーションの定義は完了です。
以下のコマンドを実行し、テーブルとソースの生成を行います。
npx prisma generate dev --name relations
1対1のリレーションを定義したUserテーブルとProfileテーブルを使って、DB操作方法を紹介します。
基本的に1対1であっても、1対多であってもコードの書き方は変わりません。
ユーザーデータの登録
登録処理は前回も行ったので特段の解説はしません。
なお、前回の記事の「データベースの操作 > 下準備」をベースにしている想定のため、本筋とは関係ない処理は記載を省略します。
/* index.ts */
// 本来は登録時のHTTPメソッドはPOSTを使うが、簡易化のためGETで定義
app.get("/create-user", async (req, res) => {
// クエリパラメータから値を取得
const { email, name } = req.query;
const newUser = await prisma.user.create({
data: {
email: String(email),
name: String(name),
},
});
res.send(`【User created】 id: ${newUser.id} name: ${newUser.name}`);
});
処理を実装したらサーバーを起動して、以下のURLにアクセスします。
http://localhost:3000/create-user?name=John&email=john@example.com
データが登録できました。
idはプロフィール登録に使うので控えておきます。
プロフィールデータの登録
/* index.ts */
// 本来は登録時のHTTPメソッドはPOSTを使うが、簡易化のためGETで定義
app.get("/create-profile", async (req, res) => {
// クエリパラメータから値を取得
const { userId, bio, birthDate } = req.query;
const newProfile = await prisma.profile.create({
data: {
userId: Number(userId),
bio: String(bio),
birthDate: new Date(String(birthDate)),
},
});
res.send(
`【Profile created】 id: ${newProfile.id} userId:${newProfile.userId} bio:${newProfile.bio} birthDate:${newProfile.birthDate}`
);
});
処理を実装したらサーバーを再起動して、以下のURLにアクセスします。
http://localhost:3000/create-profile?userId=2&bio=biotest&birthDate=1990-03-01
こちらもデータ登録できました。
では次に登録したデータを確認しましょう。
Prismaでは、リレーションが定義されているデータを簡単に取得することができます。
/* index.ts */
app.get("/select-user-with-profile", async (req, res) => {
const { userId } = req.query;
const user = await prisma.user.findUnique({
where: { id: Number(userId) },
include: {
profile: true,
},
});
res.send(`<pre>User with Profile selected:${JSON.stringify(user, null, 2)}<pre>`);
});
ポイントは1つです。
includeの指定
オプションとして、includeを指定します。
キー名に関係フィールドを指定し、値としてtrueを設定します。
これにより、Userテーブルと関連するProfileテーブルのデータを一気に取得することができます。
以下のURLにアクセスしましょう。
http://localhost:3000/select-user-with-profile?userId=2
Profileテーブルのデータも取得できていることがわかります。
SQLではJOIN句を書いて結合条件を書くなどしますが、Prismaでは簡単な記述でデータを取得することができます。
また、以下のようにProfileテーブルからUserテーブルを指定することもできます。
/* index.ts */
app.get("/select-profile-with-user", async (req, res) => {
const { userId } = req.query;
const user = await prisma.profile.findUnique({
where: { userId: Number(userId) },
include: {
user: true,
},
});
res.send(
`<pre>Profile with user selected:${JSON.stringify(user, null, 2)}<pre>`
);
});
事前準備ではユーザー登録→プロフィール登録の順で実施しましたが、いっぺんにデータを作ることもできます。
/* index.ts */
// 本来は登録時のHTTPメソッドはPOSTを使うが、簡易化のためGETで定義
app.get("/create-user-with-profile", async (req, res) => {
// クエリパラメータから値を取得
const { email, name, bio, birthDate } = req.query;
const newUser = await prisma.user.create({
data: {
email: String(email),
name: String(name),
profile: {
create: {
bio: String(bio),
birthDate: new Date(String(birthDate)),
},
},
},
include: {
profile: true,
},
});
res.send(
`【User with Profile created】 id: ${newUser.id} name: ${newUser.name} bio:${newUser.profile?.bio} birthDate:${newUser.profile?.birthDate}`
);
});
ポイントは2つです。
ネストしたcreate
リレーションのあるテーブルにデータを登録する際は、関係フィールドを指定します。
登録するデータは関係フィールドにネストしたcreateに指定します。
他のネストさせるオプションとしてconnect、connectOrCreateを指定できます。
includeの指定
データ取得のときと同じように、includeを指定できます。
ここでincludeを指定することで、createメソッドの戻り値にリレーション関係のあるデータも含まれるようになります。
データを取得する場合は、「newUser.profile.bio」のように「戻り値.関係フィールド.カラム名」で取得できます。
以下のURLにアクセスします。
http://localhost:3000/create-user-with-profile?name=Tom&email=tom@example.com&bio=anotherbio&birthDate=1992-04-01
ユーザーデータ、プロフィールデータの両方を登録できました。
リレーションが定義されているテーブルにおいて、参照先のデータが削除されたときに、関連データを削除してくれるのがCascadeオプションです。
モデル定義時、ProfileテーブルにCascadeを指定したので、その挙動を確認しましょう。
まず、以下の処理を作成し、既存データを確認します。
/* index.ts */
app.get("/select-profile", async (req, res) => {
const { id } = req.query;
const user = await prisma.profile.findUnique({
where: { id: Number(id) },
include: {
user: true,
},
});
const message = user
? `<pre>Profile selected:${JSON.stringify(user, null, 2)}<pre>`
: `id:${id} data not found`;
res.send(message);
});
以下のURLにアクセスします。
http://localhost:3000/select-profile?id=1
事前準備で登録したデータが確認できました。
では、Userテーブルを削除します。
/* index.ts */
// 本来は登録時のHTTPメソッドはDELETEを使うが、簡易化のためGETで定義
app.get("/delete-user", async (req, res) => {
const { id } = req.query;
await prisma.user.delete({
where: { id: Number(id) },
});
res.send("User deleted");
});
以下のURLにアクセスします。
http://localhost:3000/delete-user?id=2
※ここで指定するidは事前準備でプロフィール登録をするために使用したid
ユーザーが削除できたら、プロフィールデータの確認のため、再び以下のURLにアクセスします。
http://localhost:3000/select-profile?id=1
削除したのはUserテーブルのデータですが、Profileテーブルもデータが無くなっています。
これにより、データの不整合を防ぐことができます。
Prismaを使ったリレーション定義について紹介しました。
実際の業務では関連する複数のテーブルからデータを持ってくることもよくあるので、Prismaでの書き方をしっかり覚えておきたいですね。
他にもリレーション関連でいろいろな操作があるのですが、複雑になりすぎるのでそれはまた別の機会に紹介できればと思います。