はじめに
TypeScriptでバックエンドを書くなら、ORMはTypeORMがおすすめです。
が、日本語で導入からmigrationの世代管理まで説明している記事がなかったので起こしました。
導入
npm init
して express.js
と typescript
環境だけ突っ込んだ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
を追加。
{
"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を継承してモデルクラスを作ります。
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当てたほうがいいです)
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接続情報を渡してあげるだけです。
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のまま使っちゃうほうが
混乱が少なくていいかと思います。
{
// 注意: 本番環境にトランスパイル後の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/