LoginSignup
11
9

More than 3 years have passed since last update.

TypeORMを使用して、TypeScriptでMySQLのマイグレーション、接続を管理する

Last updated at Posted at 2020-05-05

概要

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には下記の記述が必須になります。

tsconfig.json
"emitDecoratorMetadata": true,
"experimentalDecorators": true,

ルートディレクトリにapp.tsを作成します。

~/app.ts
import "reflect-metadata";

接続設定を記述したormconfig.jsを作成する

~/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を書きました。

~/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にします。

~/src/entity/TestUser.ts
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ファイルが自動生成されます。

~/src/migrate/1588663729665-CreateTestUser.ts
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

そうすると、SQL通りにテーブルの作成が確認できました。
スクリーンショット 2020-05-05 16.33.05.png

ちなみに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を使用してデータを挿入します。

~/src/insert.ts
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

スクリーンショット 2020-05-05 16.44.43.png

再度実行しても重複列は自動的にスキップされます。

続いてSELECTで取得してみましょう。

~/src/select.ts
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の接続名'
}

以上

参考になれば幸いです。

11
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
9