🏢

Prismaでのトランザクションとロールバック

2021/09/12に公開1

Prismaでtransactionとrollbackをする3つの方法をまとめました。
prismaのversionが2.29.0以前だとtransactionが少し厳しかったのですが、2.29.0以上のバージョンにすると、Long-running transactionが可能になり、非常に扱いやすくなった印象です。本稿では、Prismaの公式ドキュメントを内容を引用し、日本語訳にして説明しています。

transaction APIその1

Long-running transactionで可能となったトランザクションの書き方です。具体例で把握したほうが理解しやすいはずなので、以下のシナリオを実施するためのトランザクションの処理を書いていきます。

シナリオ:

オンライン・バンキング・システムを構築しているとします。実行するアクションのひとつに、ある人から別の人への送金があります。

以下の例では、AliceとBobがそれぞれ100ドルを持っています。二人が所持金以上の金額を送金しようとすると、その送金は拒否されます。

Aliceは100ドル分の送金を1回行うことができると予想されますが、もう一方の送金は拒否されます。この場合、Aliceは0ドル、Bobは200ドルを持っていることになります。

例:

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()

async function transfer(from: string, to: string, amount: number) {
  return await prisma.$transaction(async (prisma) => {
    // 1. 送金者から金額を減らします
    const sender = await prisma.account.update({
      data: {
        balance: {
          decrement: amount,
        },
      },
      where: {
        email: from,
      },
    })
    // 2. 送金者の残高が0以下になっていないことを確認します。
    if (sender.balance < 0) {
      throw new Error(`${from} doesn't have enough to send ${amount}`)
    }
    // 3. 受け取り人の残高を金額分増やす
    const recipient = prisma.account.update({
      data: {
        balance: {
          increment: amount,
        },
      },
      where: {
        email: to,
      },
    })
    return recipient
  })
}

async function main() {
  // 送金が成功するケース
  // 送金前:
  // Alice → 100ドル
  // Bob → 100ドル
  // 送金後
  // Alice → 0ドル
  // Bob → 200ドル
  await transfer('alice@prisma.io', 'bob@prisma.io', 100)
  // Aliceの残高が不足してるので、送金に失敗するケース
  // 送金前:
  // Alice → 0ドル
  // Bob → 200ドル
  await transfer('alice@prisma.io', 'bob@prisma.io', 100)
}

main()
  .catch(console.error)
  .finally(() => {
    prisma.$disconnect()
  })

transaction APIその2

prismaが2.29.0以前だと前述のLong-running transactionが使えなく、transaction APIでは以下の書き方しかできずに、非常に苦しかったです。

postsの獲得も、totalPostsの獲得も両方成功することが保証される書き方です。
どちらか一方でも失敗した場合は、failします。

const [posts, totalPosts] = await prisma.$transaction([
  prisma.post.findMany({ where: { title: { contains: 'prisma' } } }),
  prisma.post.count(),
])

Nested writes

こちらはPrismaらしい書き方なのですが、 createやupdateする時に、子や孫要素までも一度にcreateやupdateできます。親要素のcreateに成功しても、子要素で失敗した場合、全体でfailするので、実質的にtransactionとrollbackのような処理になるといった方法です。

入れ子になった書き込みでは、1つのPrismaクライアントAPIコールで、複数の関連するレコードに触れる複数の操作を行うことができます。例えば、投稿と一緒にユーザーを作成したり、請求書と一緒に注文を更新したりします。Prisma Clientは、すべての操作が全体として成功または失敗することを保証します。

次の例は、createを使った入れ子の書き込みを示しています。

// 1つのトランザクションで、あるuserとそのuserに紐づくpostを2つ作成する処理
const newUser: User = await prisma.user.create({
  data: {
    email: 'alice@prisma.io',
    posts: {
      create: [
        { title: 'Join the Prisma Slack on https://slack.prisma.io' },
        { title: 'Follow @prisma on Twitter' },
      ],
    },
  },
})

次の例では、updateを使用した入れ子式の書き込みを示しています。

// 1つのトランザクションでpostのauthorを変更する処理
const updatedPost: Post = await prisma.post.update({
  where: { id: 42 },
  data: {
    author: {
      connect: { email: 'alice@prisma.io' },
    },
  },
})

参考

https://www.prisma.io/docs/guides/performance-and-optimization/prisma-client-transactions-guide

https://www.prisma.io/docs/concepts/components/prisma-client/transactions

Discussion

ゆうたゆうた

どちらか一方でも失敗した場合は、failします。

fail とはどういう意味ですか?
クエリの結果としてnullが代入されるという意味ですか?
ロールバックはされますか?