TypeORMで本番運用を見据えたマイグレーション
TypeScript で Node.js やる場合の O/R Mapper の選択肢は少ない。
メジャーっぽかったのと Nest.js で標準対応してるので TypeORM を使うことにしてみたが、どうも本番環境を想定した作りには至ってないように感じた。
✍️ TypeORM の機能
- Entity クラスをつくってプロパティにデコレータを付けるだけでテーブル定義できる
- Rails の ActiveRecord に近い感覚で使うこともできる
- @PrimaryGeneratedColumn で
AUTO_INCREMENT
なプライマリキー - @CreateDateColumn で
INSERT
時に Date カラムを自動更新 - @UpdateDateColumn で
UPDATE
時に Date カラムを自動更新 - ActiveRecord の Model と同じように Entity クラスに READ/WRITE メソッドを付加できる
- @PrimaryGeneratedColumn で
- 実行された SQL の出力など柔軟な Logging 設定ができる
- マルチデータベース対応
- 外部キー制約の活用
- CLI ツールによる Entity クラスの生成や Migration ファイルの生成
😇 ここがダメだよ TypeORM
使いにくいと感じたところ。
CREATE TABLE をマイグレーションで管理しにくい
TypeORM では CLI ツールが提供されており、マイグレーションを管理する仕組みがある。
$ yarn global add typeorm $ typeorm -v 0.2.7
CLI ツールで sync
すると自分が定義した Entity クラスがすべて CREATE TABLE
されて DB 反映される。
$ typeorm schema:sync
ただしこの実行内容は migrations
テーブルのようなもので管理されないため、再実行するたびに ALTER TABLE
が実行されてしまう。本番環境でこのコマンドはちょっと使えない。
マイグレーションファイル側で CREATE TABLE
を記述することもできるが、それだとテーブル定義が Entity クラスとの二重管理になってしまうし、そもそも文法覚えるのがだるい。
ORM という性質上、テーブルの中身と Entity クラスを自動マッピングしてくれるわけで、Entity クラスを作らないという選択肢はないしね。
CLI ツールで気軽に全テーブル削除できる
$ typeorm schema:drop
これだけで全テーブルが問答無用に削除される。明らかにテスト環境用のコマンドだけど、ターミナルのタブを別環境と間違えてしまい、うっかり本番環境で実行したりしそうで怖い。こういうのはデフォルトで --dry-run
になってほしいが、DryRun のオプション自体がない。
🚑 本番運用のための対策
マイグレーションについては CREATE TABLE
含めてマイグレーションファイルで管理したい。
少しトリッキーかもだが下記方法で実現できた。
sync
はローカル環境だけで行う
sync
すると CREATE 文が出力されるのでコピーしておき、その内容をマイグレーションファイルにペーストする。
$ typeorm migration:create -n CreateUser
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
にする
@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
を使う必要がなくなる。
マイグレーションはサーバ起動前に typeorm migration:run
を実行すればトランザクション使ってテーブル生成やテーブル変更が行われるので、難しいこと考えなくても新しいサーバをデプロイするだけでよくなる。
package.json
サーバ起動は yarn start
するだけでよい。prestart
から自動実行される。
"scripts": { "prestart": "rm -rf dist && tsc && yarn db:migrate", "start": "node dist/main.js", "db:migrate": "typeorm migration:run", "db:rollback": "typeorm migration:revert", }
🗽 まとめ
TypeORM は必要な機能すべて揃ってはいるが、ホスピタリティというか開発者が便利につかえるというところまでは手が回ってないという感じ。
Issue も520件くらい溜まっていて、その半分はレスがついていない。需要と比べて開発者がかなり不足しているようだ。
JavaScriptの日付処理でLuxonを使う
今までは Moment.js 使ってたけどモダンに再設計された Luxon を使うことにした。
🤔 なぜ Luxon がつくられたか
Why does Luxon exist? に書いてある。
- 作者は Moment.js のメンテナー
- Moment.js を改善するアイデアを持っていたが良いコードベースではなかった
- より明示的な API にしたかった
moment(Number)
->DateTime.fromMillis(Number)
moment(Date)
->DateTime.fromJSDate(Date)
- 追加のデータファイルなしでタイムゾーンを扱いたかった
- Intl API を使って国際化の仕組みを再考したかった
⏰ Moment.js に劣っていること
- 最新ブラウザの機能を使うと古いブラウザのための互換性対応をしなければならずコードが煩雑になる
- 国際化された文字列をコードベースに保持してない故にブラウザが対応するまでその機能を待たなければいけない可能性
- Intl API のいくつかはブラウザ依存のため Luxon の動作もそれに依存する
特に Intl API は古いブラウザでは動作しないので、そこに対応する必要性の有無が Moment.js を使うか Luxon を使うかの分かれ道となっている。
Node.js で使うなら問題にならなそうだ。
🛠 インストール
$ yarn add luxon @types/luxon
🗞 TypeScript
import {DateTime} from 'luxon'; // now (string) DateTime.local().toString(); // => 2018-09-20T14:13:12.954+09:00 DateTime.utc().toString(); // => 2018-09-20T05:13:12.961Z // now (unixtime) DateTime.local().toMillis(); // => 1537420392961 Date.now(); // => 1537420392961 // format const dt = DateTime.fromISO('2020-07-24T20:00:00'); dt.toUTC().toString(); // => 2020-07-24T11:00:00.000Z dt.toString(); // => 2020-07-24T20:00:00.000+09:00 dt.toISO(); // => 2020-07-24T20:00:00.000+09:00 dt.toHTTP(); // => Fri, 24 Jul 2020 11:00:00 GMT dt.toFormat('yyyy-MM-dd HH:mm:ss z'); // => 2020-07-24 20:00:00 Asia/Tokyo dt.toSQL(); // => 2020-07-24 20:00:00.000 +09:00 dt.toSQLDate(); // => 2020-07-24 dt.toSQLTime(); // => 20:00:00.000 +09:00
RubyとPythonとNode.jsで開発してるやつちょっとこい
仕事上、メインの API サーバは Ruby の Rails、Lambda やマイクロサービス的なサーバは Node.js、一部 DevOps 関連のスクリプトを Python で書いてたりする。
各種プログラミング言語を使うときは言語自体のバージョン管理が必要となるが、env
系のバージョン管理ツールで統一するとコマンドがいっしょなので便利。
そして Mac でも Linux でも使える(これ超大事)。
Mac へのインストール方法
$ brew install rbenv $ brew install pyenv $ brew install nodenv
基本的なコマンド
インストール可能なバージョン一覧
$ rbenv install -l
インストール
$ rbenv install 2.5.1
インストール済のバージョン一覧
$ rbenv versions
アンインストール
$ rbenv uninstall 3.6.5
インストールしたバージョンをシステム上でグローバルに利用する
$ rbenv global 3.6.5
インストールしたバージョンを現在のディレクトリだけで利用する
$ rbenv local 3.6.5
カレントディレクトリに .ruby-version
ファイルが生成されるので rbenv
はこれを参照して指定のバージョンを利用する仕組み。
fish shell によるパス設定
bash を使ってる場合 ~/.bash_profile
を編集する『パスを通す設定』が煩わしい。でも fish shell ならプラグインをインストールするだけ。
$ fisher install pyenv $ fisher install rbenv $ fisher install oh-my-fish/plugin-nodenv
自分で設定ファイルを vi
とかで修正する必要がない。
Node.js のバージョン管理ツールいろいろ
nodenv
の存在に気づくまでは nodebrew を使っていた。
だいたい同じだけど、カレントディレクトリだけ特定のバージョンを利用する機能はない。
(あと nodebrew は指定バージョンのインストールで v10.11.0
みたいに v
を付けるが、nodenv 系は付けない点に注意。)
$ nodebrew ls-remote $ nodebrew install-binary v10 $ nodebrew ls $ nodebrew use v10 $ nodebrew uninstall v10 $ nodebrew selfupdate
nodenv
と同じ env 系のバージョン管理ツールとして ndenv というものもあるが、こっちはもうメンテナンスされてないので使わない。
nodeenv という一文字違いの紛らわしいものもあるが、まったくの別系統のようだ。ちなみに Node.js を最初に使い始めた 5 年くらい前は nvm を使っていた気がする。