TypeORMで本番運用を見据えたマイグレーション

TypeScript で Node.js やる場合の O/R Mapper の選択肢は少ない。

* [TypeORM](http://typeorm.io/)
* [Sequelize](http://docs.sequelizejs.com/) + [sequelize-typescript](https://github.com/RobinBuschmann/sequelize-typescript)
* [wetland](https://wetland.spoonx.org/)

メジャーっぽかったのと [Nest.js](https://docs.nestjs.com/) で標準対応してるので TypeORM を使うことにしてみた。機能的には充分だが、本番環境での運用を想定した使いやすさには至ってないように感じた。

## ✍️ TypeORM の機能

* Entity クラスをつくってプロパティにデコレータを付けるだけでテーブル定義できる
* Ruby on Rails の ActiveRecord に近い感覚で使うこともできる
  * [@PrimaryGeneratedColumn](http://typeorm.io/#/decorator-reference/primarygeneratedcolumn) で `AUTO_INCREMENT` なプライマリキー
  * [@CreateDateColumn](http://typeorm.io/#/decorator-reference/createdatecolumn) で `INSERT` 時に Date カラムを自動更新
  * [@UpdateDateColumn](http://typeorm.io/#/decorator-reference/updatedatecolumn) で `UPDATE` 時に Date カラムを自動更新
  * ActiveRecord の Model と同じように [Entity クラスに READ/WRITE メソッドを付加](http://typeorm.io/#/active-record-data-mapper)できる
* 実行された SQL の出力など柔軟な [Logging 設定](http://typeorm.io/#/logging)ができる
* [マルチデータベース対応](http://typeorm.io/#/multiple-connections)
  * レプリケーションとして master/slave の指定もできる
* [外部キー制約の活用](http://typeorm.io/#/relations)
* [CLI ツール](http://typeorm.io/#/using-cli)による Entity クラスの生成や Migration ファイルの生成

## 😇 ここがダメだよ TypeORM

使いにくいと感じたところ。

### CREATE TABLE をマイグレーションで管理しにくい

TypeORM では CLI ツールが提供されており、マイグレーションを管理する仕組みがある。

```bash
$ yarn global add typeorm
$ typeorm -v
0.2.7
```

CLI ツールで `sync` すると自分が定義した Entity クラスがすべて `CREATE TABLE` されて DB 反映される。  

```bash
$ typeorm schema:sync
```

ただしこの実行内容は `migrations` テーブルで管理されないため、再実行するたびに `ALTER TABLE` が実行されてしまう。本番環境でこのコマンドはちょっと使えない。  
[マイグレーションファイル側で `CREATE TABLE` を記述](http://typeorm.io/#/migrations/using-migration-api-to-write-migrations)することもできるが、それだとテーブル定義が Entity クラスとの二重管理になってしまうし、そもそも文法覚えるのもだるい。  
ORM という性質上、テーブルの中身と Entity クラスを自動マッピングしてくれる役割なわけで、Entity クラスを作らないという選択肢はないしね。

### CLI ツールで気軽に全テーブル削除できる

```bash
$ typeorm schema:drop
```

これだけで全テーブルが問答無用に削除される。明らかにテスト環境用のコマンドだけど、ターミナルのタブを別環境と間違えてしまい、うっかり本番環境で実行したりしそうで怖い。こういうのはデフォルトで `--dry-run` になってほしいが、DryRun のオプション自体がない。

## 🚑 本番運用するための対応策

マイグレーションについては `CREATE TABLE` 含めてマイグレーションファイルで管理したい。  
少しトリッキーかもだが下記方法で実現できた。

### `sync` はローカル環境だけで行う

`sync` すると CREATE 文がコンソールに出力されるのでコピーしておき、その内容をマイグレーションファイルにペーストする。

```bash
$ typeorm migration:create -n CreateUser
```

```javascript
async up(queryRunner: QueryRunner): Promise<any> {
  await queryRunner.query('CREATE TABLE `users` (`id` int NOT NULL AUTO_INCREMENT, `created_at` datetime(0) NOT NULL DEFAULT NOW(), `updated_at` datetime(0) NOT NULL DEFAULT NOW(), PRIMARY KEY (`id`)) ENGINE=InnoDB');
}

async down(queryRunner: QueryRunner): Promise<any> { 
  await queryRunner.query('DROP TABLE IF EXISTS `typeorm_test`.`users`');
}
```

### Entity クラスで `synchronize: false` にする

```javascript
@Entity({name: 'users', synchronize: false})
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @CreateDateColumn({name: 'created_at', precision: 0, default: () => 'NOW()'})
  createdAt: Date;

  @UpdateDateColumn({name: 'updated_at', precision: 0, default: () => 'NOW()'})
  updatedAt: Date;
}
```

こうしておけば以後 `sync` コマンドの対象外となり、本番環境で `schema:sync` と `schema:drop` を使う必要がなくなる。  
マイグレーション実行は設定ファイルで `migrationsRun: true` にしておけばサーバ起動時に実行される。トランザクション使ってテーブル生成やテーブル変更が行われるので、難しいこと考えなくても新しいサーバをデプロイするだけでよくなる。

### package.json

サーバ起動は `yarn start` するだけでよい。`prestart` から自動実行される。

```json
"scripts": {
  "prestart": "rm -rf dist && tsc",
  "start": "node dist/main.js"
}
```


## 🗽 まとめ

TypeORM は必要な機能すべて揃ってはいるが、ホスピタリティというか開発者が便利につかえるというところまでは手が回ってないという感じ。  
[Issue](https://github.com/typeorm/typeorm/issues) も500件くらい溜まっていて、その半分はレスがついていない。需要と比べて開発者がかなり不足しているようだ。