尋常でないもふもふ

a software engineer blog

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

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

メジャーっぽかったのと Nest.js で標準対応してるので TypeORM を使うことにしてみたが、どうも本番環境を想定した作りには至ってないように感じた。

✍️ TypeORM の機能

😇 ここがダメだよ 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:syncschema: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 サーバは RubyRails、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 ならプラグインをインストールするだけ。

jnst.hateblo.jp

$ 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 を使っていた気がする。