🤔

Prisma2でのtransactionをActiveRecord風に書きたい問題

2021/10/17に公開

ActiverRecordやEloquentに慣れていると、トランザクションを👇にように書きたいものです。
mysql2の例です。

import * as mysql2 from 'mysql2/promise';

const db = await mysql.createConnection({
  host: 'localhost',
  user: 'root',
  database: 'test'
});

await db.begin();

if(//何らかのロジック処理) {
 
   //何らかのデータベースアクセス
 
   await db.commit();

} else {

   await db.rollback();

}

これがどうもPrismaだと現バージョン3.2.1では無理のようです。
一応公式にはトランザクションのやり方がいくつか乗っています。(transactions
Primse.Allで配列に入れたデーターアクセス処理を実行し、どれかが失敗したら、他のアクセスもロールバックしてくれるとか。


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

その配列の中にprisma以外のビジネスロジックを入れたら、ダメでしたのと(間違った可能性もある)、一番目で成功した値を2番目のPromiseで使いたい時にややこしくなってきます。

もう一つはリレーション持っているテーブルで同時にデータを入れるとか


// Create a new user with two posts in a
// single transaction
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' },
      ],
    },
  },
})

これも同じく私が直面している問題を解決できません。

ただ、バージョン 2.29.0 から「Interactive Transactions」というプレビュー機能が実装されたみたいで、👇にように書けるとのことです。

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. Decrement amount from the sender.
    const sender = await prisma.account.update({
      data: {
        balance: {
          decrement: amount,
        },
      },
      where: {
        email: from,
      },
    })
    // 2. Verify that the sender's balance didn't go below zero.
    if (sender.balance < 0) {
      throw new Error(`${from} doesn't have enough to send ${amount}`)
    }
    // 3. Increment the recipient's balance by amount
    const recipient = prisma.account.update({
      data: {
        balance: {
          increment: amount,
        },
      },
      where: {
        email: to,
      },
    })
    return recipient
  })
}

async function main() {
  // This transfer is successful
  await transfer('alice@prisma.io', 'bob@prisma.io', 100)
  // This transfer fails because Alice doesn't have enough funds in her account
  await transfer('alice@prisma.io', 'bob@prisma.io', 100)
}

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


一応async functionの中で例外が発生した場合、rollbackが自動で走ってくれるみたいです、これでなんとかなりそうです。

プレビュー機能なので、有効にするにはprisma.schemaファイルに記述を加える必要があります。

prisma.schema

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

あまり関係ないですが、コントローラーからモデルの関数でトランザクション実行しながらビジネスロジックや他の処理をトランザクションの間にはさみたい時用に自分でラッパー作って実装したので、書き残しておきたいと思います。

userModel.ts

//...省略...

async createUser(
    user: User,
    callback?: (newUser?: User) => Promise<void>,
  ): Promise<User> {
  
    const createNewUser = (db?: PrismaTransactionCallBackInstance) => {
      const connection = db || this.db;
      return connection.user.create({
        data: {
          ...user
        },
        select: {
          email: true,
          id: true,
        },
      });
    };

    try {
      if (callback && typeof callback === 'function') {
        const resultUser = await this.db.$transaction(async (db) => {
          const result = await createNewUser(db);
          await callback(result);

          return result;
        });
      } else {
        const resultUser = await createNewUser();
      }

      return resultUser;
    } catch (error) {
      throw error;
    }
  }
  
  //...省略...

authController.ts
 //...省略...
	
  async signUp() {
    try {
      //承認用のランダムの文字列作成
      const confirmationCode = this.authService.generateConfirmationCode();

      //userオブジェクト作成
      const user = new User({//user情報登録});

      //usecaseの呼び出し
      await this.userModel.createUser(user, async (newUser: User) => {
        //承認メールの送信
        await this.mailer.sendVerification({
          email: newUser.email,
          confirmationCode,
        });
      });

      return 'success';
    } catch (error) {
      throw error;
    }
  }
	
 //...省略...

これでメール送信をモデルの中に入れずに実行てきる。

Discussion