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を引き継げない。
と理解して実装しておけば、エラーの発生や意図しない挙動は起きなくなるはずだ。