概要
Node.jsでMySQLのORMを管理をする場合に最初に思いつくライブラリとしてはSequelizeがありますが、ある程度かっちり開発する人にとって、MySQLのマイグレーションやエンティティを折角コードで管理するのであれば、TypeScriptへ対応は必須かと思います。しかしSequelizeはver5でTSにネイティブ対応したものの、元々の設計から互換性はかなり厳しい様です。(参考: Sequelize から TypeORM に移行してみた)
そこで、TypeORMが選択肢に上がります。2020/05月時点でスター数が18.9kを超えており非常に期待されているTSベースのORMライブラリです。TypeORMは各種DBに対応していますが、本投稿ではMySQL
をTypeORMで触る方法を説明します。
サンプルコードはこちらに公開しています。
https://github.com/hedrall/typeorm-sample
セットアップ
まずは必要なツールをインストールする
(gitのサンプルを確認してみてください。)
npm i -g typeorm mysql reflect-metadata
tsconfig.jsonには下記の記述が必須になります。
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
ルートディレクトリにapp.tsを作成します。
import "reflect-metadata";
接続設定を記述したormconfig.jsを作成する
module.exports = [
{
name: 'default', // 標準で読み込まれる設定
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'docker',
password: 'docker',
database: 'test',
synchronize: false,
logging: false,
connectTimeout: 30 * 1000,
acquireTimeout: 30 * 1000,
entities: [__dirname + '/dist/entity/**/*.js'],
migrations: [__dirname + '/dist/migration/**/*.js'],
// 今回subscriberは扱いません。
// subscribers: [__dirname + '/dist/subscriber/**/*.js'],
cli: {
entitiesDir: 'src/entity',
migrationsDir: 'src/migration',
// subscribersDir: 'src/subscriber'
}
},
];
ポイントとして、entities, migrationsにはテーブル定義やマイグレーション定義を格納する場所ですが、参照先を設定する必要があります。今回はTSCを実行した時に~/src => ~/distに結果が吐き出される設定にしており、typeormのcliコマンド自体は当然nodeで動くので、dist内の.js
ファイルを参照するようにしています。一方cliがファイルを生成する場合の格納先はsrc配下なので、src/entityなどを設定します。
一応 ts-nodeを使用して./node_modules/typeorm/cli.js
を実行する方法もありますが、ORMライブラリを使用したコードのデプロイ時は.jsファイルを参照するはずなので、個人的にはこの方法がおすすめです。
ローカル確認用にMySQLのコンテナを起動する
こんな感じでdocker-compose.ymlを書きました。
version: '3'
services:
mysql:
image: mysql:5.7
container_name: mysql_host
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test
MYSQL_USER: docker
MYSQL_PASSWORD: docker
TZ: 'Asia/Tokyo'
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
ports:
- 3306:3306
コンテナを立ち上げます。
docker-compose up -d
MySQLに入ってみます。
mysql -u docker -p -h 127.0.0.1 # パスワードもdocker
初期状態ではtestというDBを作成しています。
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| test |
+--------------------+
2 rows in set (0.00 sec)
エンティティ(テーブル定義)を作成
test_userというテーブルを作成してみます。カラムはusernameとemailとageだけでusernameとemailの両方でprimaryにします。
import {
Column,
CreateDateColumn,
Entity,
PrimaryColumn,
UpdateDateColumn
} from 'typeorm';
@Entity()
export class TestUser {
// ユーザ名, VARCHAR(20)型
// デコレータの引数で型を定義できる
// string型の変数に関してデフォルトではvarchar(255)型になるため、lengthをつけて宣言することもできる
@PrimaryColumn( { type: 'varchar', length: 20 } )
username: string;
// emailアドレス, VARCHAR(50)型
@PrimaryColumn( { type: 'varchar', length: 50 } )
email: string;
// emailアドレス, INT(11)型
@Column( { nullable: true } )
age?: number;
// レコードの作成時間, DATETIME(6)型
@CreateDateColumn()
created_at?: string;
// レコードの更新時間, DATETIME(6)型
@UpdateDateColumn()
updated_at?: string;
// 例えば自動生成列(Generated Column)の定義も可能, VARCHAR(10)型
@Column( {
type: 'varchar',
length: 10,
generatedType: 'STORED',
asExpression: 'date_format(created_at,\'%Y%m%d\')'
} )
create_date?: string;
constructor( options: TestUser ) {
Object.assign( this, options );
}
}
マイグレーションを作成
TypeORMのマイグレーションファイルはエンティティの定義とDBでの現状との差分から自動的にSQLを生成可能です。実行するには
rm -rf dist && tsc
typeorm migration:generate -n CreateTestUser
を実行します。
するとmigrationファイルが自動生成されます。
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateTestUser1588663729665 implements MigrationInterface {
name = 'CreateTestUser1588663729665';
public async up( queryRunner: QueryRunner ): Promise<void> {
await queryRunner.query(
'CREATE TABLE `test_user` (`username` varchar(20) NOT NULL, `email` varchar(50) NOT NULL, `age` int NULL, `created_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `updated_at` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), `create_date` varchar(10) AS (date_format(created_at,\'%Y%m%d\')) STORED NOT NULL, PRIMARY KEY (`username`, `email`)) ENGINE=InnoDB',
undefined
);
}
public async down( queryRunner: QueryRunner ): Promise<void> {
await queryRunner.query( 'DROP TABLE `test_user`', undefined );
}
}
この様に単純に差分をSQLに起こしてくれます。
続いてマイグレーションを実行します。
rm -rf dist && tsc
typeorm migration:run
ちなみにrunコマンドでは、全ての未実施のmigrationが一度に実行されます。また、downの操作を行うには
typeorm migration:revert
ですが、最後のmigrationファイルのdownのみが実行されます。なので、一個づつupする、全てdownするという操作がありませんが、仕様の様です。
https://typeorm.io/#/migrations/running-and-reverting-migrations
DBを操作する
DBにデータを挿入してみます。
CRUD操作にはクエリビルダーを使用してSQLの様に操作を指定する方法と、組み込みのORMモデル(manager)を使用する方法があります。下記ではmanagerを使用してデータを挿入します。
import { createConnection } from 'typeorm';
import { TestUser } from './entity/TestUser';
( async () => {
// MySQLと接続
const connection = await createConnection( 'default' );
// データを挿入する
await connection.manager.save( TestUser, [
{
username: 'user1',
email: 'email1@example.com',
age: 20
},
{
username: 'user2',
email: 'email2@example.com',
}
] );
await connection.close();
} )().catch( e => console.log( e ) );
tsc
node dist/insert.js
再度実行しても重複列は自動的にスキップされます。
続いてSELECTで取得してみましょう。
import { createConnection } from 'typeorm';
import { TestUser } from './entity/TestUser';
( async () => {
// MySQLと接続
const connection = await createConnection( 'default' );
// データを取得する
const result = await connection.manager.find( TestUser, { age: 20 } );
console.table( result );
await connection.close();
} )().catch( e => console.log( e ) );
$ tsc
$ node dist/select.js
┌─────────┬──────────┬──────────────────────┬─────┬──────────────────────────┬──────────────────────────┬─────────────┐
│ (index) │ username │ email │ age │ created_at │ updated_at │ create_date │
├─────────┼──────────┼──────────────────────┼─────┼──────────────────────────┼──────────────────────────┼─────────────┤
│ 0 │ 'user1' │ 'email1@example.com' │ 20 │ 2020-05-05T07:44:29.166Z │ 2020-05-05T07:44:29.166Z │ '20200505' │
└─────────┴──────────┴──────────────────────┴─────┴──────────────────────────┴──────────────────────────┴─────────────┘
TIPS
- データをインサートする場合はsaveを使用すると、primari-key制約で重複する行はupdate(値が変化しなければ何もしない) 、それ以外はinsertをしてくれるが、複数のキーを使用している場合には全てinsertの処理がされる様子。gitでも同様のエラーが議論されてました。 その場合は自分で重複を排除する必要があります。
- 重複判定には
connection.manager.getId
を使用すると、上の例ではuser1-email1@example.com
の様なIDが返却されます。selectの結果ではdatetime型の列は Date オブジェクトで帰りますので、string変換する場合に予期せぬ形式になる可能性があるので、注意してください。 - GCPのCloudFunctionsで使用する場合はTCP接続ができないので、ormconfig.jsに下記を追加してください。
extra: {
socketPath: '/cloudsql/DBの接続名'
}
以上
参考になれば幸いです。