Edited at

Typescript + TypeORMセットアップ & migration世代管理 & expressへの組み込み


はじめに

TypeScriptでバックエンドを書くなら、ORMはTypeORMがおすすめです。

が、日本語で導入からmigrationの世代管理まで説明している記事がなかったので起こしました。


導入

npm init して express.jstypescript 環境だけ突っ込んだPJだと仮定します。

馴染みのない人はTypeScriptでExpress.js開発するときにやることまとめ (docker/lint/format/tsのまま実行/autoreload)も合わせてどうぞ。

postgresqlとの組み合わせ例ですが、mysqlでも殆ど変わりません。


パッケージ追加

まずは必要なライブラリのインストール。

npm install typeorm

npm install pg # MySQLの場合はmysql
npm install reflect-metadata
npm install @types/pg --save-dev


設定

TypeORMはDecorator機能を使用するため、 tsconfig.json に設定を追加


{
"compilerOptions": {
...
"emitDecoratorMetadata": true,
"experimentalDecorators": true
},
...
}

また、 "target": "es5" だとエラーが起こるので、es6以上を指定します。


ディレクトリ作成

ormconfigで使用するディレクトリを作成しておきます。

ActiveRecord慣れしている人はentitiesはmodelsという名前にするのもいいですね。


  • src/entities

  • src/db/migrations

  • src/db/subscribers


TypeORMの設定

PJ直下に ormconfig.json を追加。


ormconfig.json

{

"type": "postgres",
"host": "db", // 接続するDBホスト名
"port": 5432,
"username": "foo", // DBユーザ名
"password": "bar", // DBパスワード
"database": "test_db", // DB名
// 注意" これがtrueだと、モデル定義を変更すると即DB反映されます。
// 個人PJならいいですが、普通はmigrationファイルで世代管理すると思うのでfalseにします。
"synchronize": false,
"logging": false,
"entities": ["src/entities/**/*.ts"],
"migrations": ["src/db/migrations/**/*.ts"],
"subscribers": ["src/db/subscribers/**/*.ts"],
"cli": {
"entitiesDir": "src/entities",
"migrationsDir": "src/db/migrations",
"subscribersDir": "src/db/subscribers"
}
}

JSON以外にも、「jsファイル」「環境変数」「ymlファイル」「xmlファイル」が使え、設定方法はかなり柔軟です。

特にjsファイルで書くとos.env.NODE_ENV等の環境変数を使ったり、条件による分岐ができるので嬉しいですね。

踏み込んだ設定が必要になった場合、各設定の書き方は これ 、設定出来る項目はこれを参考にしてください。


migration作成

セットアップ自体はここまでで終わりです。

ここからは実際にモデルとテーブルの作成を世代管理しながら回していく方法の紹介です。

ActiveRecord方式の説明をしますが、Repositoryパターンでも継承するクラスが

BaseEntityではなくBaseであること以外はほとんど同じです。

(事前準備)

postgresqlで接続しようとしているdbを作成しておきます。ここでは仮にtest_dbとします。

create database test_db

Railsではmigrationファイル作成→モデルが自動的に出来上がるという順番だと思いますが、

TypeORMはモデルを作成→migrationのgenerateコマンドで現在のDB状態とモデル定義を照らし合わせ、

migrationファイルを作成という順番になっています。

(既存のモデルも、定義を変更すればgenerateコマンドで差分を拾ってくれます)

個人的には、モデルを常に正義とするTypeORM型式のほうが好きです。

では早速。


モデル定義作成

BaseEntityを継承してモデルクラスを作ります。


src/entities/User.ts

import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm'

@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
public id: number

@Column()
public name: string = ''

@Column()
public age: number = 0
}

export default User



migrationファイル作成

TypeORMのcliで、migrationファイルを作成します。

「現在のDBスキーマ」と「modelsフォルダ内のモデル定義」を比較し、差分を作って自動的にmigrationを作成してくれます。

-n 引数でマイグレーションに名前をつけます。アッパーキャメルケースで書きましょう。

# tsファイルを直実行 

ts-node $(npm bin)/typeorm migration:generate -n Initialize
# ts-nodeがグローバルインストールされていない場合は`npm run ts-node`をpackage.jsonに書いた上でこう
npm run ts-node $(npm bin)/typeorm migration:generate -- -n Initialize

# jsコンパイルしたファイルを実行するならこう。この方式で行く場合はormconfigの各種パスがトランスパイル後のものになっている必要がある。
$(npm bin)/typeorm migration:generate -n Initialize

そうすると、ormconfigで指定したディレクトリ(記事ではsrc/db/migrations)に、タイムスタンプ+マイグレーション名のファイルが出来上がります。

ちなみに、migration:generateではなくmigration:createコマンドを使用すれば空のファイルが作成できます。

(PJでLinter/Formatterを使っている場合は、自動生成されるコードがそのルール通りではない

可能性が高いので、ちゃんとLinter/Formatter当てたほうがいいです)


src/db/migrations/Initialize1540352770065.ts

import {MigrationInterface, QueryRunner} from "typeorm";

export class Initialize1543643478560 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`CREATE TABLE "user" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "age" integer NOT NULL, CONSTRAINT "PK_cace4a159ff9f2512dd42373760" PRIMARY KEY ("id"))`);
}

public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.query(`DROP TABLE "user"`);
}
}


(追記)

デフォルトでTypeORMが吐き出すテーブル名やカラム名、リレーションの名前規約があまりイケていません

とりあえず試してみるという人はそのままでOKですが、ガッツリTypeORM使う人向けに

それらを修正する記事も書きました。

TypeORMのmigrationで作成されるテーブル名をカスタマイズする


migration適用

先程作成したmigrationを実際にDBに適用します。

# tsファイルを直実行。作成したmigrationの`up`関数が実行される。

ts-node $(npm bin)/typeorm migration:run

# jsコンパイルしたファイルを実行するならこう。
$(npm bin)/typeorm migration:run

また、TypeORMは世代管理のためにDBにmigrationsテーブルを作成します。

このテーブルを見るとどの世代までのmigrationが当たっているのかを確認できます。

(テーブル名はormconfigファイルで変更できます)

もしも世代を下げる必要があれば、以下コマンドで可能です。

# 作成したmigrationの`down`関数が実行される。

ts-node $(npm bin)/typeorm migration:revert


expressサーバーへの組み込み

expresss自体に何かをする必要はありません。

サーバ立ち上げ前に、TypeORMにDB接続情報を渡してあげるだけです。


src/index.ts


import * as express from 'express'
import { User } from './entities/User'
import { getConnectionOptions, createConnection, BaseEntity } from 'typeorm'

let app = async () => {
const app = express()

// --- TypeORMの設定
const connectionOptions = await getConnectionOptions()
const connection = await createConnection(connectionOptions)
// ActiveRecordパターンでTypeORMを使用する場合
BaseEntity.useConnection(connection)

app.get('/', async (req, res) => {
const user = new User()
user.name = 'Qiita'
user.age = 25
await user.save()
const users = await User.find()
res.send(users)
})
app.listen(3000, () => console.log('Example app listening on port 3000!'))
}

app()


上記を ts-node src/index.ts で立ち上げれば、 localhost:3000/にアクセスするたびにユーザーが増えて

ユーザーテーブルにあるユーザー一覧が返るはずです。

(追記)

User.find()は引数なしでDBレコード全部返ります。引数にオプションオブジェクトを渡すことで色々できます。

例: find({ where: { name: 'Qiita'} }) レコードの引き方は豊富なオプションが有るので以下参照。

Repositoryパターンの例しか出てきませんが、ARパターンでも似たような感覚でオプション指定できます。

http://typeorm.io/#/find-options


その後

あとは普段のexpress.js開発のとおりです。


本番環境について

jsにトランスパイルしてnodeコマンドで実行する場合は各種パスの指定を

トランスパイル後のものにする必要があります。

(よって、トランスパイルは元のディレクトリ構造をキープするように吐き出したほうが幸せになれます)

追記:

ts-nodeの本番環境運用も試しており、結論から言うと起動時間が若干長いという以外の

オーバーヘッドは殆ど無いので、TypeORMを使う場合はts-nodeのまま使っちゃうほうが

混乱が少なくていいかと思います。


ormconfig.json

{

// 注意: 本番環境にトランスパイル後のjsファイルを使用する場合、パス指定は工夫が必要
// (entities, migrations, subscribersはトランスパイル後のパスを指定する)
"entities": ["src/entities/**/*.ts"],
"migrations": ["src/db/migrations/**/*.ts"],
"subscribers": ["src/db/subscribers/**/*.ts"],
"cli": {
"entitiesDir": "src/entities",
"migrationsDir": "src/db/migrations",
"subscribersDir": "src/db/subscribers"
}
}


Ref

英語であれば、公式リファレンスがそこそこ充実しています。

http://typeorm.io/