Sequelize から TypeORM に移行してみた
https://3.bp.blogspot.com/-4vIwXFWmSdU/W6DTI_sgmQI/AAAAAAABO5o/KzvDeFitJfsuSmoF2wDn2ZEyEM_l8AqYQCLcBGAs/s800/computer_hatsumei_syarin.png
課題点
三行まとめ
まとめると以下のような3つの課題点を感じていました。
現時点での Sequelize 単体ではこれまでの実装と互換性を意識してのためか TypeScript と相性がいいとは言いづらい
sequelize-typescript を使うことで緩和されるが機能の対応付けが必要になり学習コストが高い
Sequelize が更新されたとき、sequelize-typescript の追従を待つ必要がある場合もある
こうした課題を解決する手段として、ORM ライブラリの移行を決断しました。詳細はそれぞれ下記にまとめます。
Sequelize の課題点
まず Sequelize は Node.js の ORM ライブラリとしては老舗の1つで、Node.js のこれまでの歴史で大きくその価値を発揮してきたことを忘れてはいけません。今でも純粋に Node.js で Sequelize を使用することは手軽な手段の1つであり、有効に活用できる手札であると考えています。 Sequezlie v5 では昨今の流れを受けてか TypeScript のサポートが追加され、公式による型定義の提供が始まりました。一方で、TypeScript を活用しているプロジェクトにおいては、公式で TypeScript の型の運用はすぐには導入しづらい (原文: As Sequelize heavily relies on runtime property assignments, TypeScript won't be very useful out of the box.) とマニュアルに記載されているように、手堅く運用するには記載するコードが冗長になってしまう傾向にあります。 具体的には、モデルの「定義」と「実装」が分離しており、実行時に定義と実装を紐付ける必要があります。(コードは公式を参照のこと。)これもマニュアルに記載されていることではありますが、Sequelize の実装がコード実行時の状態に強く依存する実装になっていることが背景にあるようです。
sequelize-typescript の課題点
SequelizeとTypeScriptを組み合わせたときの課題を解決する手段として、こちらのスライド でご紹介したように sequelize-typescript というサポートライブラリを使用する方法を提案しています。sequelize-typescript は デコレータとメタデータを活用してクラスとクラスに定義したメソッドを実行時に Sequelize で活用できる形にマッピングするような動きをし、Sequelize単体で使用した際に発生する型と実装の分離を解決し冗長な実装を回避することができるようになります。 しかしながら、sequelize-typescript を使用すると冗長なコード自体は避けられるようになりますが、公式のマニュアルにある記載とsequelize-typescript により提供されるデコレータの機能を脳内で対応付けしつつ実装を進めていく必要があるという別の課題が発生します。ほとんどの場合、sequelize-typescript の README に記載されているので困ることはありませんがそれでも、チームでの開発で新規に参入したメンバーにそれを求めることはなかなかハードであり、学習コストが高いと判断しています。
また、Sequelize のバージョンアップによっては非互換の変更が入ることもあり、sequelize-typescript の更新が追従するまでは Sequelize 本体の更新を待つほかない状態になります。単なる機能更新の場合は問題とはなりませんが脆弱性の対応などが絡んでくると厄介な問題になり得ます。
TypeORM に移行するモチベーション
TypeORM は、Sequelize と同じく複数のデータベースの種類に対応した ORMライブラリですが、Seuqelize + sequelize-typescript な構成に比較して以下のような特徴を持っており、先述のような課題点を解決できるメリットが有ると判断しました。
TypeScript で書かれることを前提に設計されている
モデルはクラスとデコレータによる実装を行えるため、今までの実装体験を継承できる
TypeORM のマニュアルを参照するのみで実装を進められるため学習コストを抑えられる
一方で、挙げなければいけないデメリットは以下のとおりでした。
移行コストそのものは無視できない
機能そのものの変化を起こす実装の変更ではないので、特にチーム開発において移行を推し進めるには得られるメリットと発生するデメリットを理解した上で、共通認識を育てておく必要がある
英語、日本語問わず、Sequelize に比較して情報は少なめ
TypeORM の方が後発であるので情報の量は自ずと少なくなってくるのは致し方なし
日本語の情報が見れることは便利だが、一次情報である公式のマニュアルを参照する癖をつけておくことは大事
オールインワンフレームワークである Nest.js の採用があるなどの事例が増えてくることが見込まれる 移行までのロードマップ
一度に既存の Sequelize のモデルをすべて TypeORM へ移行することはほとんどの場合、無理ゲーになることが想像できます。
そのため、Sequelize から TypeORM へ移行を進めるにあたり、下記のような基本方針で段階的に進めることとしました。
1. 移行が完全に完了するまでは Sequelize と TypeORM は並行稼動する
2. これまで培ってきた Sequelize のマイグレーションは DB から SQL でスナップショットを撮って削除
3. 新規に作成するモデルの実装やマイグレーションは TypeORM で行う
4. 実装の比較的薄く手軽なものから「期限を設定して」TypeORM への移行を行う
無限に時間を使うわけにも行かないのでモデルや機能単位で移行をスケジュールしておくのが大事
5. すべてのモデルが TypeORM に移行したあとは Sequelize に関連するパッケージを削除し後片付けする
基本方針なので、様々な背景により特別に勘案するようなシーンはあると思いますが臨機応変に対応するのが良いでしょう。
注意すべき点は並行稼動中に対象のデータベースへの接続が単純に倍になる点です。Sequelizeの内部からコネクションをどうにか引っ張り出して、TypeORM にわたすようなやり方もあるかもしれませんがここでは純粋なやり方だけ示します。
移行の流れ
TypeORM の設定
TypeORM の実装やCLIから使用する接続情報を格納するファイル ormconfig を配置します。
JSON や JavaScript、TypeScript の他 YAML や XML でも記載できるようです。下記は PostgreSQL に接続するサンプルです。
code:ormconfig.ts
module.exports = {
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'USERNAME',
password: 'PASSWORD',
database:'DATABASE',
synchronize: false, // これを false にしないと実行時に実装に合うように自動マイグレーションされるので注意
logging: false,
cli: {
entitiesDir: 'src/entities',
migrationsDir: 'src/migrations',
subscribersDir: 'src/subscribers'
}
}
軽く紹介すると、Entity は テーブルに対応するクラス、Migration は テーブルを変更するスクリプト、Subscriber は Entity の処理をフックして処理を行うクラスを実装します。機会があったら別のタイミングで詳しくご紹介したいと思います。
なぜ Model ではなく、Entity なのかというと TypeORM が ActiveRecord パターン と Data Mapper パターンの両方をサポートしており、プロジェクトの規模や方針に応じて使い分けられるためです。そのため Entity は Model 相当にもなり得、DAO として使用することもできます。公式マニュアルにどう使い分けるといいか書いてあるので参考にすると良いでしょう。
CLI の準備
ormconfig を TypeScript で記載して実行する場合は下記のように ts-node と tsconfig-path を併用することが必要になります。ついては npm script に typeorm-cli を簡単に実行できるよう、コマンドを追加しておくのが良いでしょう。tsconfig.json で "module": "commonjs" となっていない場合は ts-node のオプションで設定を行うか専用の tsconfig.json を作成し、指定する必要があります。
code:package.json
{
"scripts": {
"typeorm": "ts-node -r tsconfig-paths/register $(npm bin)/typeorm"
}
}
こうすると以下のように実行できます。
code:sh
npm run typeorm --help
Entity の定義
ざっとは下記のような定義を行います。sequelize-typescript のときのようなデコレータをプロパティ/メンバーに設定して行きます。Active Record パターンを使用する場合はクラスは BaseEntity を継承する必要があります。 Data Mapper パターンの場合は単純なクラスで良いので何も継承する必要はありません。コンストラクタでのプロパティの初期化は行っても問題なさそうでしたが、ここではマニュアルに沿って初期化はあえて行わないようにしています。
code:entities/sample.ts
import {
Entity,
Column,
PrimaryGeneratedColumn,
BaseEntity,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm'
@Entity('users')
export default class User extends BaseEntity {
@PrimaryGeneratedColumn('uuid')
public id!: string
@Column({ nullable: false })
protected name!: string
@CreateDateColumn({ name: 'created_at', nullable: false })
public createdAt!: Date
@UpdateDateColumn({ name: 'updated_at' })
public updatedAt!: Date | null
}
ロジックの移行
Active Record パターンの場合は、クラスに直接メソッドを実装していきます。
Data Mapper パターンの場合は、Repository クラスを用意しそちらにクエリや実装を追加していきます。
データベースに接続する
TypeORM でデータベースに接続するには createConnection を実行します。
下記の例では先に作成した ormconfig から設定情報を取得するようにしています。
entities と subscribers を import したクラスを直接指定していますが、ormconfig 同様テキストで指定することもできます。
code:connection.ts
import { createConnection, Connection } from 'typeorm'
import OrmConfig = require('./ormconfig')
import User from './entities/user'
import UserSubscriber from './subscribers/user'
const connection = await createConnection({
type: OrmConfig.type,
host: OrmConfig.host,
port: OrmConfig.port,
username: OrmConfig.username,
password: OrmConfig.password,
database: OrmConfig.database,
})
connection を生成したあとは Entity や Repository を通じてデータベースの操作が可能になります。
既存実装の置き換え
ここまで来たらあとは、置き換え先の Entity を使用するように既存の実装を置き換えていくだけになります。
注意するのは、Sequelize の BelognsTo が使用されているテーブルの置き換えを行う場合は、BelongsToによるSequelizeのモデルの参照を一度廃止して TypeORM の Entity を使用してレコードを引くようにするか、もしくは思い切って一度に移行するかのどちらかを選択する必要があるという点です。影響範囲が大きくなるので後者はおすすめしませんが、実装が極端に薄い等の場合はそういうやり方もありではないかと思います。
あるいはこのタイミングで interface を用意するなどで、実装を疎結合にできるようなポイントがないか改めて考えてみるのも良いかもしれません。
まとめ
Sequelize + sequelize-typescript から TypeORM への移行方法とそれを実現するための方針例を示しました。
Sequelize 自体は素晴らしいライブラリですが、TypeScript の特性十分に活かしつつ ORM ライブラリを使用したいという状況においては最適解とならないケースもあるかと思います。sequelize-typescript で、機能は必要十分となりましたが、学習コストが高くなってしまう側面を否めないことも述べました。
そうした背景から今回は TypeORM を選択するに至ったわけですが、自身やチームの熟練度や状況に応じて適切な選択をしていきたいと思います。
これを書いているときには、実のところまだ Sequelize から TypeORM への移行は完了していませんが、今後すべての Sequelize の実装を TypeORM に載せ替えて、今回上げたような課題を解決し、よりより開発体験を継続して保っていけるように色々整備していきたいなと考えています。