2021年6月28日
プログラミング
Prismaとcascading deletes
はじめに
この記事ではPrismaを使ってcascading deletesを実装したいと思います!
Prismaとは
Prismaは、プログラミング言語とデータベースとの仲立ちをしてくれるORMと呼ばれるものの内、TypeScriptやマイグレーションに対応した比較的新しいツールです。
SQLを書かずに、型安全な書き方ができ、最近非常に注目されています。
cascading deletesとは
MySQLを始めとするリレーショナルデータベースでは、テーブルと呼ばれるデータセットが相互に関連付けられながら保存されています。
このとき参照元となるテーブルが削除されたとき、参照先のテーブルも自動的に削除される仕組みをcascading deletesと呼びます。
POINT!!
Prismaでは2021/06/21現在、cascading deletesが未対応なので、Transaction APIという仕組みを使って自分で実装する必要があります。
Prismaの導入
環境
- Node.js: v14.16.0
- Prisma: v2.24.1
- macOS: 11.2 (Big Sur)
- npm: v7.6.3
まずは公式に従ってターミナルから、下記のコマンドでPrismaのスターターキットをダウンロードします。
$ curl -L https://pris.ly/quickstart | tar -xz --strip=2 quickstart-master/typescript/starter
するとstarterというディレクトリが作られるので、下記のコマンドで移動し、依存パッケージをダウンロードします。
$ cd starter
$ npm install
これでセットアップは完了です!実際にstarterディレクトリをエディタから開いてみます。
一番よく使うのがscript.tsというファイルで、この中のmain関数の中にPrismaのコードを書いていきます。
// script.ts
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
// A `main` function so that you can use async/await
async function main() {
// ... you will write your Prisma Client queries here
}
main()
.catch((e) => {
throw e
})
.finally(async () => {
await prisma.$disconnect()
})
では試しにmain関数の中にコードを書いていきましょう!
main関数の中を下記のように書き換えて、
async function main() {
const allUsers = await prisma.user.findMany({
include: { posts: true },
})
console.dir(allUsers, { depth: null })
}
ターミナルから下記のコマンドでscript.tsを実行します。
$ npm run dev
するとmain関数の結果がターミナルに出力されると思います。
下記のように出力されればokです!
main関数の中に、Userテーブルから全てのユーザーの情報を取得するコードを書いたので、合計で2人のユーザーがデータベースに保存されていることがわかりました。
[
{ id: 1, email: 'sarah@prisma.io', name: 'Sarah', posts: [] },
{
id: 2,
email: 'maria@prisma.io',
name: 'Maria',
posts: [
{
id: 1,
title: 'Hello World',
content: null,
published: false,
authorId: 2
}
]
}
]
ポイント
実はこのスターターキットには、初期段階でUserとPostの2つのテーブルが用意されています。
先程確認した通り、UserテーブルにはSarahとMariaの2人のデータが、Postテーブルには”Hello World”というタイトルのついたデータが1つだけ保存されています。
この”Hello World”という記事はユーザーであるMariaが書いた記事なので、先程Userテーブルにアクセスしたときに、”Hello World”の記事の情報も抜き出すことができました。
“Hello World”のauthorIdに、Mariaのidである2が設定されています
ユーザーを削除する
データベースの中身がわかったところで、データベースの操作をしていきます!
まずはSarahのデータをUserテーブルから削除するため、main関数を下記のように書き換えます。
async function main() {
const deleteUser = await prisma.user.delete({ where: { id: 1 } })
console.dir(deleteUser, { depth: null })
}
userのうちidが1のユーザーを削除
そしてscript.tsを実行します。
$ npm run dev
削除されたデータとして、Sarahのデータが出力されていればokです!
{ id: 1, email: 'sarah@prisma.io', name: 'Sarah' }
cascading deletesを実装する
残るはMariaのデータですが、ここまででUserテーブルは下記のようになっています。
[
{
id: 2,
email: 'maria@prisma.io',
name: 'Maria',
posts: [
{
id: 1,
title: 'Hello World',
content: null,
published: false,
authorId: 2
}
]
}
]
先程と同じようにユーザーのデータだけ削除すると、”Hello World”の記事のデータが残ってしまうので、$transactionを使って、Mariaのユーザーデータと”Hello World”の記事データを一緒に削除します。
async function main() {
const deletePosts = prisma.post.deleteMany({ where: { authorId: 2 } })
const deleteUser = prisma.user.delete({ where: { id: 2 } })
const transaction = await prisma.$transaction([deletePosts, deleteUser])
console.dir(transaction, { depth: null })
}
ポイント
1行目でauthorIdが2のpostを全て削除、2行目でidが2のuserを削除、それぞれの返り値をtransactionで包むことで、2つの処理は両方とも成功するか、両方とも失敗するかのどちらかになることが保証されます。
1行目と2行目ではawaitを使いません
このコードを実行して、
$ npm run dev
成功していれば削除したデータが出力されます!
[ { count: 1 }, { id: 2, email: 'maria@prisma.io', name: 'Maria' } ]
親・子・孫のデータを削除する
ここまでで、ユーザーという親のデータを削除したときに、そのユーザーが書いた記事という子のデータも一緒に削除することができました。
今回はさらに孫のデータまで一緒に削除してみます。
準備
Companyという新しいテーブルを作ります。
schema.prismaというファイルの、model Userの行以下を下記のように書き換えます。
// prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
company Company @relation(fields: [companyId], references: [id])
companyId Int
}
model Company {
id Int @id @default(autoincrement())
name String
members User[]
}
ターミナルから下記のコマンドで、新しくなったschema.prismaでデータベースを初期化します。
$ npx prisma migrate dev --name init
データが失われます、と聞かれますがy(yes)を入力します。
We need to reset the SQLite database "dev.db" at "file:./dev.db".
Do you want to continue? All data will be lost. › (y/N)
こちらが出力されれば初期化完了です!
Your database is now in sync with your schema.
✔ Generated Prisma Client (2.25.0) to ./node_modules/@prisma/client
in 101ms
データベースが空の状態になったので、データを追加します。
main関数を下記に書き換えて、
async function main() {
const company = await prisma.company.create({
data: {
name: "palan",
members: {
create: [
{
name: "太郎さん",
email: "taro@prisma.io",
posts: { create: [{ title: "post_foo" }, { title: "post_bar" }] },
},
{
name: "花子さん",
email: "hanako@prisma.io",
posts: {
create: [{ title: "post_baz" }, { title: "post_quz" }],
},
},
],
},
},
include: { members: { include: { posts: true } } },
})
console.dir(company, { depth: null })
}
ターミナルから実行します。
$ npm run dev
追加されたデータが出力されればokです!
{
id: 1,
name: 'palan',
members: [
{
id: 1,
email: 'taro@prisma.io',
name: '太郎さん',
companyId: 1,
posts: [
{
id: 1,
title: 'post_foo',
content: null,
published: false,
authorId: 1
},
{
id: 2,
title: 'post_bar',
content: null,
published: false,
authorId: 1
}
]
},
{
id: 2,
email: 'hanako@prisma.io',
name: '花子さん',
companyId: 1,
posts: [
{
id: 3,
title: 'post_baz',
content: null,
published: false,
authorId: 2
},
{
id: 4,
title: 'post_quz',
content: null,
published: false,
authorId: 2
}
]
}
]
}
ポイント
Companyテーブルに”palan”という会社を追加しました。
同時に太郎さんと花子さんの2人を、会社のmembersとしてUserテーブルに追加しました。
さらに2人が書いた記事として、それぞれ2つの記事をPostテーブルに追加しました。
cascading deletesの実装(3世代ver)
さっそくCompanyテーブルの”palan”を削除します。
このとき、メンバーである太郎さん、花子さんと、2人が書いた記事も全て一緒に削除します。
main関数を下記に書き換えます。
async function main() {
const deleteUserAndPosts = (userId: number) => {
const deletePosts = prisma.post.deleteMany({
where: { authorId: userId },
})
const deleteUsers = prisma.user.delete({ where: { id: userId } })
return [deletePosts, deleteUsers]
}
const companyWithMembers = await prisma.company.findFirst({
select: { members: { select: { id: true } } },
}) //{ members: [ { id: 1 }, { id: 2 } ] }
if (!companyWithMembers) throw new Error()
const userTransaction = companyWithMembers.members.reduce(
(accumulator: any[], member) =>
accumulator.concat(deleteUserAndPosts(member.id)),
[]
)
const companyTransaction = await prisma.$transaction([
...userTransaction,
prisma.company.deleteMany(),
])
console.dir(companyTransaction, { depth: null })
}
まず冒頭では、先程実装したUser→Postのcascading deletesのロジックを関数化してます。
const deleteUserAndPosts = (userId: number) => {
const deletePosts = prisma.post.deleteMany({
where: { authorId: userId },
})
const deleteUsers = prisma.user.delete({ where: { id: userId } })
return [deletePosts, deleteUsers]
}
あとは”palan”に所属するユーザーがわかれば、そのuserIdに対してdeleteUserAndPostsを適用できます。
Companyテーブルのうち、”palan”に所属するメンバーを取得します。
const companyWithMembers = await prisma.company.findFirst({
select: { members: { select: { id: true } } },
}) //{ members: [ { id: 1 }, { id: 2 } ] }
if (!companyWithMembers) throw new Error()
companyWithMembersにはメンバー(太郎さん、花子さん)のuserIdが配列として入っているはずです。
companyWithMembersの要素にdeleteUserAndPostsを適用します。
const userTransaction = companyWithMembers.members.reduce(
(accumulator: any[], member) =>
accumulator.concat(deleteUserAndPosts(member.id)),
[]
)
ここでuserTransactionには4つのdelete操作が含まれているはずです(太郎さん、太郎さんの記事、花子さん、花子さんの記事)。
ポイント
reduceを使うことで、この4つの操作を1つの配列に収めることができています!
最後に、この4つの操作に”palan”という会社自体のdelete操作を加えて、合計5つの操作を$transactionで包みます!
const companyTransaction = await prisma.$transaction([
...userTransaction,
prisma.company.deleteMany(),
])
console.dir(companyTransaction, { depth: null })
これで完成です!
ターミナルから実行して、
$ npm run dev
削除されたデータとして、下記のように5つの操作が出力されればokです!
[
{ count: 2 }, // 太郎さんの記事削除
{ id: 1, email: 'taro@prisma.io', name: '太郎さん', companyId: 1 },
{ count: 2 }, // 花子さんの記事削除
{ id: 2, email: 'hanako@prisma.io', name: '花子さん', companyId: 1 },
{ count: 1 } // companyの削除
]
まとめ
Transaction APIを用いてcascading deletesを実装する方法をご紹介しました!
テーブル同士の関連が増えるほど保守が大変になってしまうというデメリットもあり、Prismaのネイティブでの対応が待ち遠しいですね。
ご覧いただきありがとうございました!
参考
Prismaのお仕事に関するご相談
Bageleeの運営会社、palanではPrismaに関するお仕事のご相談を無料で承っております。
zoomなどのオンラインミーティング、お電話、貴社への訪問、いずれも可能です。
ぜひお気軽にご相談ください。
この記事は
参考になりましたか?
0
0
関連記事
簡単に自分で作れるWebAR
「palanAR」はオンラインで簡単に作れるWebAR作成ツールです。WebARとはアプリを使用せずに、Webサイト上でARを体験できる新しい技術です。
palanARへpalanでは一緒に働く仲間を募集しています
正社員や業務委託、アルバイトやインターンなど雇用形態にこだわらず、
ベテランの方から業界未経験の方まで様々なかたのお力をお借りしたいと考えております。
運営メンバー
Eishi Saito 総務
SIerやスタートアップ、フリーランスを経て2016年11月にpalan(旧eishis)を設立。 マーケター・ディレクター・エンジニアなど何でも屋。 COBOLからReactまで色んなことやります。
sasakki デザイナー
アメリカの大学を卒業後、日本、シンガポールでデザイナーとして活動。
やまかわたかし デザイナー
フロントエンドデザイナー。デザインからHTML / CSS、JSの実装を担当しています。最近はReactやReact Nativeをよく触っています。
Sayaka Osanai デザイナー
Sketchだいすきプロダクトデザイナー。シンプルだけどちょっとかわいいデザインが得意。 好きな食べものは生ハムとお寿司とカレーです。
はらた エンジニア
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
こぼり ともろう エンジニア
サーバーサイドエンジニア。SIerを経て2019年7月に入社。日々学習しながらRuby on Railsを使った開発を行っています。
ささい エンジニア
フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。
Damien
WebAR/VRの企画・開発をやっています。森に住んでいます。
ゲスト bagelee
かっきー
まりな
suzuki
miyagi
ogawa
雑食デザイナー。UI/UXデザインやコーディング、時々フロントエンドやってます。最近はARも。
いわもと
デザイナーをしています。 好きな食べ物はラーメンです。
taishi kobari
フロントエンドの開発を主に担当してます。Blitz.js好きです。
kubota shogo
サーバーサイドエンジニア。Ruby on Railsを使った開発を行いつつ月500kmほど走っています!
nishi tomoya
aihara
グラフィックデザイナーから、フロントエンドエンジニアになりました。最近はWebAR/VRの開発や、Blender、Unityを触っています。モノづくりとワンコが好きです。
nagao
SIerを経てアプリのエンジニアに。xR業界に興味があり、unityを使って開発をしたりしています。
Kainuma
サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています
sugimoto
asama
ando
iwasawa ayane
oshimo
異業界からやってきたデザイナー。 palanARのUIをメインに担当してます。 これからたくさん吸収していきます!