Prismaにおけるtransaction処理

Prisma: transaction

2022.09.29

prismaでトランザクション処理をする方法をまとめました。

トランザクションとは

トランザクションは、データベースをある一貫した状態から別の一貫した状態へ変更するアクションを1つに束ねたものであるとだけ書くのも不親切なので理解の試金石として例を考えてみます。
Authorテーブルとそれに紐づくBookテーブルを同時に作る状況を考えてみます。

Author
id string
name string
Book
id string
authorId string
title string

BookとAuthorを同時に作る状況を考えます。

TypeScript
const prisma = new PrismaClient()
const author = await prisma.author.create({
  data: {
    name: 'Aurelius',
  }
})

cont authorId = author.id

const book = await prisma.book.create({
  data: {
    authorId: authorId,
    title: 'Ta eis heauton',
  }
})

これを実行した場合constがcontになっているためauthorを作成した直後にエラーが発生してしまい、著書のない作家が生まれてしまいます。かわいそうですね。
これを防ぐのがトランザクションです。Prismaには3種類のトランザクションの書き方があるようなのでこれを紹介します。


Nested writes

親を作るときに子も一緒に作ってしまう基本的な書き方です。これをトランザクションとして考えていなかったのですが気軽にかけて便利ですね。

TypeScript
const prisma = new PrismaClient()
const author = await prisma.author.create({
  data: {
    name: 'Aurelius',
    books: {
      create: [
        {
           title: 'Ta eis heauton',
         }
      ]
    }
  }
})
メリット デメリット
気軽にかける 親と子を作る間に複雑な処理が入る場合は扱えない

Sequential operations

他の2つに比べると使用頻度は低い気がします。 ただ親子関係にない2つのテーブルを作る場合は上のNested writesを使えないので良いかもしれません。下の例はcreateManyで事足りますが他のテーブルを扱い、かつロジックが複雑でない場合はこちらを選択すると良いかもしれません。

TypeScript
const prisma = new PrismaClient()
const author1 = prisma.author.create({
  data: {
  name: 'Aurelius',
  books: {
    create: [
      {
        title: 'Ta eis heauton',
      }
    ]
  }
}
const author2 = prisma.author.create({
  data: {
  name: 'Caesar',
  books: {
    create: [
      {
        title: 'Commentarii de bello Gallico',
      }
    ]
  }
}

await prisma.$transaction([author1, author2])
メリット デメリット
Nested writesでは扱えない親子関係にないレコードも扱える 親と子を作る間に複雑な処理が入る場合は扱えない

Interactive transactions

一番表現力の高い書き方です。Preview featureなのでschema.prismaを変更する必要がありますが、テーブルを更新する以外の場所で起きたエラーも検知してrollbackしてくれます。

TypeScript
const prisma = new PrismaClient()
await prisma.$transaction(
  async (prisma) => {
    const author = await prisma.author.create({
      data: {
        name: 'Aurelius',
      }
    })
    // 複雑な処理
    const epicurean = await checkAbstinence(prisma, author)
    if (epicurean) {
      throw new Error()
    } 

    await prisma.book.create({
      data: {
        title: 'Ta eis heauton',
        authorId: author.id
      }
    })
  }
)
メリット デメリット
prismaによるDBの更新以外のロジック部分で出たエラーに対してもrollbackが走る 古いバージョンでは使えない

最後に

以上がPrismaにおける3種類のtransaction処理の書き方です。
最後のInteractive transactionはかなり使いやすそうでした。ただ普段自分が使っているNest.jsでPrismaを扱う場合、ビジネスロジックを扱うクラスが保持するPrisma Clientを使うのが推奨されているため, transaction apiで受け取ったprisma clientを使い回すこの方法とあまり相性が良くないように感じました。


参考サイト

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