Prismaの一括アップデート: Nest/Write や Interactive Transaction を使う方法

📕 新コースを公開しました。→クーポン掲載ページ

Prisma使ってますか??

僕も今関わっているプロジェクトで Prisma.js を使用しています。

その中で一括アップデート(バルクアップデート)行う際に、TransactionNest/Write な方法を見つけたのでここにまとめておきます。

一括の update に関する内容を書いていますが、createや他の状況にも適用できるものです。

仮定

仮にスキーマが次のような場合を想定します。

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

model User {
  id       Int       @id @default(autoincrement())
  email    String    @unique
  articles Article[]
}

model Article {
  id      Int    @id @default(autoincrement())
  title   String
  user    User   @relation(fields: [user_id], references: [id])
  user_id Int
}

あらかじめ下記のようなデータがあると仮定します。

image.png
image.png

JSONで表現すると次のような感じのデータです。

{
  "user": [
    {
      "id": 1,
      "email": "example@example.com"
    }
  ],
  "article": [
    {
      "id": 1,
      "title": "title 1",
      "user_id": 1
    },
    {
      "id": 2,
      "title": "title 2",
      "user_id": 1
    }
  ]
}

以下では基本的に Articletitile をどうやって更新するか、に焦点を当てます。

ArticleIDが事前にわかるケース: 思いつきやすい方法

事前に article のidがわかっていて、複数のarticleを更新する方法を見ていきます。

まず最も思いつきやすい方法を書きます。

ただしこれは使わずに次以降のものを使用すべきです。

// タイトルに「A」の文字を付与します。

// (1)
await prisma.article.update({
  where: {
    id: 1,
  },
  data: {
    title: `title 1: A`,
  },
});

// (2)
await prisma.article.update({
  where: {
    id: 2,
  },
  data: {
    title: `title 2: A`,
  },
});
image.png

この場合の問題は (1) の後にエラーが発生した場合です。

その場合、Article1のみ更新され、Article2は更新されていないという整合性に関わる問題が発生します。そのため一括アップデート(バルクアップデート)ではこのやり方は控えた方が無難です。

💡 ちなみに updateMany の場合はそもそも今回のケースには使用できません。updateMany はある条件に合致したレコードを全て同じように更新するものだからです。今回はそれぞれのArticleをそれぞれ違う値として更新する必要があるからです。

ArticleIDが事前にわかるケース: Transaction

先ほどの問題を Transaction を使って解決します。

Transactionは複数のprismaの処理を配列に渡せばいいだけです。とてもシンプルに書けます。

Transactionを使うと、全体として成功か失敗かのどちらかということが保証されます。

つまり、Article1だけタイトルが更新されて、Article2は更新されていない、ということにはならないので安心です。

// タイトルに「B」の文字を付与します。

const [article1, article2] = await prisma.$transaction([
  prisma.article.update({
    where: {
      id: 1,
    },
    data: {
      title: `title 1: AB`,
    },
  }),
  prisma.article.update({
    where: {
      id: 2,
    },
    data: {
      title: `title 2: AB`,
    },
  }),
]);
image.png

ArticleIDが事前にわかるケース: Nest/Write

userに紐づくものとして user から article を一括更新できます。

ドキュメントの例では単一の更新例しか載っていませんが、配列で渡せばバルクアップデートが可能です。

またこのようにネスト形式で書いた場合は、先述のものと同じ Transaction として処理されるので、全体として成功または失敗のどちらかということが保証されています。

// タイトルに「C」の文字を付与します。

const user = await prisma.user.update({
  where: {
    id: 1,
  },
  data: {
    articles: {
      update: [
        {
          where: {
            id: 1,
          },
          data: {
            title: `title 1: ABC`,
          },
        },
        {
          where: {
            id: 2,
          },
          data: {
            title: `title 2: ABC`,
          },
        },
      ],
    },
  },
});
image.png

ArticleIDが不明のケース: 思いつきやすい方法

ここからはArticleのIDが事前にわからないケースを見ていきます。事前にわからないので、まずUserを取得し、そのUserに紐づくArticleを取り出してIDを取り出します。

まずは既に見たものと同じように、一番思いつきやすい方法を書いてみます。

  • Userを取得し、
  • その後 Article テーブルに対して個別に更新します。

この書き方も基本は使わず、後で記載する方法へ変更した方がいいです。

// タイトルに「D」の文字を付与します。

// (1)
const user = await prisma.user.findUnique({
  where: {
    id: 1,
  },
  include: {
    articles: true,
  },
});

if (!user) throw Error("error");

// (2)
await prisma.article.update({
  where: {
    id: user.articles[0].id,
  },
  data: {
    title: `title 1: ABCD`,
  },
});

// (3)
await prisma.article.update({
  where: {
    id: user.articles[1].id,
  },
  data: {
    title: `title 2: ABCD`,
  },
});
image.png

ArticleIDが不明のケース: Interactive Transaction

先の例では3回にわたってprismaを使用しています。

(1) は読み取りなので良いとしても、もし (2) の更新が終了した後に何かしらエラーなどが発生した場合、(3) のArticleに関しては更新されないという事態になります。

これを解決するには、既にみた Transaction の使い方ではなく Interactive Transaction を使用します。

今回の場合は (1) のUser情報を取得してからでないと、Article の ID がわからないので次の処理に進めない、という状況です。

このように全体を1つの Transaction とみなしつつ、中では逐次処理も行いたい時にはいい選択肢になります。

まずこの Interactive Transaction を使用するには schema の client を下記のように書き換えます。

generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["interactiveTransactions"]  // 追記
}

...

その後、該当コードを丸々prisma.transactionで囲います。

// タイトルに「E」の文字を付与します。

await prisma.$transaction(async (prisma) => {

  // (1)
  const user = await prisma.user.findUnique({
    where: {
      id: 1,
    },
    include: {
      articles: true,
    },
  });

  if (!user) throw Error("error");

  // (2)
  await prisma.article.update({
    where: {
      id: user.articles[0].id,
    },
    data: {
      title: `title 1: ABCDE`,
    },
  });

  // (3)
  await prisma.article.update({
    where: {
      id: user.articles[1].id,
    },
    data: {
      title: `title 2: ABCDE`,
    },
  });

});
image.png

Interaction Transaction の検証

ここで、わざと途中でエラーを発生させてみます。

(2) が終わった後に 強制的にエラーを発生させるコードを挟みます。

// タイトルに「F」の文字を付与します。

await prisma.$transaction(async (prisma) => {
  // (1)
  const user = await prisma.user.findUnique({
    where: {
      id: 1,
    },
    include: {
      articles: true,
    },
  });

  if (!user) throw Error("error");

  // (2)
  await prisma.article.update({
    where: {
      id: user.articles[0].id,
    },
    data: {
      title: `title 1: ABCDEF`,
    },
  });

  // エラー発生コードの追加
  if (1 + 1 === 2) throw Error("error");

  // (3)
  await prisma.article.update({
    where: {
      id: user.articles[1].id,
    },
    data: {
      title: `title 2: ABCDEF`,
    },
  });
});

この状況で期待されるのは、途中でエラーが発生した場合は、全体として書き込みを行わないことです。

つまり両方ともタイトルが変更されないことが望まれます。

結果は次のように、両方ともタイトルに変更はなく、Transaction としてうまく機能していることがわかります。

image.png

以上、prismaを使った様々なupdate方法についてでした。

🎓✍️コース一覧

プログラミング関係のビデオコースを提供しています。クーポンも発行していますので、ぜひ一度チェックしてみてください。

Twitter @takumafujimoto

記事を読んでいただきありがとうございます。ツイッターではプログラミング以外についてや、たまにクーポン情報もツイートしたり。。。ツイッターでもお待ちしてます。