2021年6月28日

プログラミング

Prismaとcascading deletes

目次

  1. はじめに
  2. Prismaの導入
  3. 親・子・孫のデータを削除する
  4. まとめ
  5. 参考

はじめに

この記事では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のネイティブでの対応が待ち遠しいですね。

ご覧いただきありがとうございました!

参考

Transactionについて(Prisma公式)

Array.prototype.reduce() (MDN)

Prismaのお仕事に関するご相談

Bageleeの運営会社、palanではPrismaに関するお仕事のご相談を無料で承っております。
zoomなどのオンラインミーティング、お電話、貴社への訪問、いずれも可能です。
ぜひお気軽にご相談ください。

無料相談フォームへ

0

0

AUTHOR

kobari

taishi kobari

フロントエンドの開発を主に担当してます。Blitz.js好きです。

アプリでもっと便利に!気になる記事をチェック!

記事のお気に入り登録やランキングが表示される昨日に対応!毎日の情報収集や調べ物にもっと身近なメディアになりました。

簡単に自分で作れるWebAR

「palanAR」はオンラインで簡単に作れるWebAR作成ツールです。WebARとはアプリを使用せずに、Webサイト上でARを体験できる新しい技術です。

palanARへ
palanar

palanはWebARの開発を
行っています

弊社では企画からサービスの公開終了まで一緒に関わらせていただきます。 企画からシステム開発、3DCG、デザインまで一貫して承ります。

webar_waterpark

palanでは一緒に働く仲間を募集しています

正社員や業務委託、アルバイトやインターンなど雇用形態にこだわらず、
ベテランの方から業界未経験の方まで様々なかたのお力をお借りしたいと考えております。

話を聞いてみたい

運営メンバー

eishis

Eishi Saito 総務

SIerやスタートアップ、フリーランスを経て2016年11月にpalan(旧eishis)を設立。 マーケター・ディレクター・エンジニアなど何でも屋。 COBOLからReactまで色んなことやります。

sasakki デザイナー

アメリカの大学を卒業後、日本、シンガポールでデザイナーとして活動。

yamakawa

やまかわたかし デザイナー

フロントエンドデザイナー。デザインからHTML / CSS、JSの実装を担当しています。最近はReactやReact Nativeをよく触っています。

Sayaka Osanai デザイナー

Sketchだいすきプロダクトデザイナー。シンプルだけどちょっとかわいいデザインが得意。 好きな食べものは生ハムとお寿司とカレーです。

はらた

はらた エンジニア

サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています

kobori

こぼり ともろう エンジニア

サーバーサイドエンジニア。SIerを経て2019年7月に入社。日々学習しながらRuby on Railsを使った開発を行っています。

sasai

ささい エンジニア

フロントエンドエンジニア WebGLとReactが強みと言えるように頑張ってます。

damien

Damien

WebAR/VRの企画・開発をやっています。森に住んでいます。

ゲスト bagelee

ゲスト bagelee

かっきー

かっきー

まりな

まりな

suzuki

suzuki

miyagi

ogawa

ogawa

雑食デザイナー。UI/UXデザインやコーディング、時々フロントエンドやってます。最近はARも。

いわもと

いわもと

デザイナーをしています。 好きな食べ物はラーメンです。

kobari

taishi kobari

フロントエンドの開発を主に担当してます。Blitz.js好きです。

shogokubota

kubota shogo

サーバーサイドエンジニア。Ruby on Railsを使った開発を行いつつ月500kmほど走っています!

nishi tomoya

aihara

aihara

グラフィックデザイナーから、フロントエンドエンジニアになりました。最近はWebAR/VRの開発や、Blender、Unityを触っています。モノづくりとワンコが好きです。

nagao

SIerを経てアプリのエンジニアに。xR業界に興味があり、unityを使って開発をしたりしています。

kainuma

Kainuma

サーバーサイドエンジニア Ruby on Railsを使った開発を行なっています

sugimoto

sugimoto

asama

ando

CONTACT PAGE TOP