LoginSignup
10
4

More than 3 years have passed since last update.

TypeORM x AuroraDB(MySQL), transaction の正しい扱い方

Last updated at Posted at 2020-09-18

TypeORM x AuroraDB(MySQL)

TypeORM つかってて、以下のように怒られる人むけの研究記録。
AuroraDB(MySQL)限定の話かどうかはわかりません。

  • Query runner already released. Cannot run queries anymore.
  • MissingRequiredParameter: Missing required key 'transactionId' in params
  • Transaction <base64string> is not found

環境

  • typeorm 0.2.26
  • typeorm-aurora-data-driver 1.4.0

この環境におけるTransactionの経験則

経験則1: (当たり前だけど)閉じたtransactionは利用できない

let tx:any;

await getConnection().transaction(async tx => {
  await tx.save(MyEntity, entity);
}); // transaction() が返った時点で commit され、txは閉じている

// Error: "Query runner already released. Cannot run queries anymore."
await tx.save(MyEntity, entity);

経験則2: 暗黙の transaction が存在する

TypeORMにおける単純なEntityのsaveを考える。

const entity1 = new MyEntity();
const entity2 = new MyEntity();

await entity1.save();
await entity2.save();

一見、トランザクションなど存在しないが、
aurora-data-api-driver 内部にログを仕込んでみると以下のようになる。

TX START { transactionId: 'bnOf7HfepOTZplVAduk=' }
TX QUERY { transactionId: 'bnOf7HfepOTZplVAduk=', query: 'INSERT INTO ...' }
TX QUERY { transactionId: 'bnOf7HfepOTZplVAduk=', query: 'SELECT `MyEntity` ...' }
TX COMMIT { transactionId: 'bnOf7HfepOTZplVAduk=' }

TX START { transactionId: 'KCdxQMJb1xyWXZMcbYI=' }
TX QUERY { transactionId: 'KCdxQMJb1xyWXZMcbYI=', query: 'INSERT INTO ...' }
TX QUERY { transactionId: 'KCdxQMJb1xyWXZMcbYI=', query: 'SELECT `MyEntity` ...' }
TX COMMIT { transactionId: 'KCdxQMJb1xyWXZMcbYI=' }

実はtransactionが暗黙的に生成されてた。
具体的にどの処理で、までは調べてない。
Write系は基本transactionと理解しておけば事故が減ると思う。

ほとんどは暗黙のtransaction + 経験則3 が複合して怒られているエラー。

経験則3: transactionの並列や入れ子はエラーの原因となる

厳密なパターンまで言語化できていないが、
以下のような場合に(きっと処理の順番やタイミング次第で)エラーになることがある。 

  • MissingRequiredParameter: Missing required key 'transactionId' in params
  • Transaction <base64string> is not found

のエラーが出た場合、経験則3を疑う。

  • 並列の例
// ❌ だめな(ことがある)例
await Promise.all([
  entity1.save(), // 暗黙的にtx1が生成
  entity2.save(), // 暗黙的にtx2が生成
]); // tx1とtx2が並列に走る

// ✅ うまくいく例
await getConnection().transaction(async (tx) => {
  await Promise.all([
    tx.save(MyEntity, entity1),
    tx.save(MyEntity, entity2),
  ]); // 単一txで並列に処理するならok
});
  • 入れ子の例
// ❌ だめな(ことがある)例
await getConnection().transaction(async (tx1) => { // tx1 が生成
  await tx1.save(MyEntity, entity1);

  // 例えばこの中で await entity2.save(); が行われていると
  // 暗黙的に tx2 が生成
  await something();
});

// ✅ うまくいく例1
await getConnection().transaction(async (tx) => {
  await tx.save(MyEntity, entity1);

  // 例えばこんなふうに、単一txで処理されるよう工夫する
  const entity2 = something();
  await tx.save(MyEntity, entity2);
});

// ✅ うまくいく例2
await getConnection().transaction(async (tx) => {
  await tx.save(MyEntity, entity1);
});
await something(); // 例えばこんなふうに、txが入れ子にならないよう、外に出す

経験則4: @Transaction() はtransactionを引き継げない

@Transaction() でデコることによって、functionをtransactionalにできる。
例えばこんな感じだね

@Transaction()
async function processA(@TransactionManager tx: EntityManager) {
  const entity1 = new MyEntity();
  await tx.save(entity1);
}

@Transaction()
async function processB(@TransactionManager tx: EntityManager) {
  const entity2 = new MyEntity();
  await tx.save(entity2);
}

// 実行時、勝手にtxが注入され、それぞれ transaction として動作する
await processA();
await processB();

ここで、processAとprocessBを同一transaction内で行うにはどうしたらよいか?
答え: できない (できるなら教えて!!)

// ❌ できそうだけどできない例
getConnection().transaction(async tx1 => {
  // みんなtx1を使って処理してね〜

  await processA(tx1); // 問答無用でtx2が生成される

  await processB(tx1); // 問答無用でtx3が生成される
});

まとめ

  • 本環境における TypeORM の write 処理は基本的に transaction である。
  • transactionを扱うときには、他のtransactionは閉じていなければならない。
  • @Transaction()は思った通りtransactionを引き継げない。

と理解して実装しておけば、エラーの発生や意図しない挙動は起きなくなるはずだ。

10
4
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4