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

# 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は利用できない

```ts
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を考える。

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

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

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

```text
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を疑う。


- 並列の例

```ts
// ❌ だめな(ことがある)例
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
});
```

- 入れ子の例

```ts
// ❌ だめな(ことがある)例
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にできる。
例えばこんな感じだね

```ts
@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を引き継げない。**

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