vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

TypeORMに触れてみる 2

はじめに

今回はマイグレーションと外部キーの設定について。

Migration

まずマイグレーションから。 TypeORM のコマンドでファイルを作ります。

npx typeorm migration:create -n AddUpdateDate

生成されたファイルは src/migration に出力されます。

1591186678545-AddUpdateDate.ts

import {MigrationInterface, QueryRunner} from "typeorm";
export class AddUpdateDate1591186678545 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
    }
    public async down(queryRunner: QueryRunner): Promise<void> {
    }
}

そのままだと何も実行されないので、処理を追加します。 今回は "updateDate" カラムを "SampleUser" に追加します。

1591186678545-AddUpdateDate.ts

import {MigrationInterface, QueryRunner} from "typeorm";
export class AddUpdateDate1591186678545 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
      await queryRunner.query(`ALTER TABLE "SampleUser" ADD COLUMN "updateDate" date DEFAULT current_timestamp NOT NULL`);
    }
    public async down(queryRunner: QueryRunner): Promise<void> {
      await queryRunner.query(`ALTER TABLE "SampleUser" DROP COLUMN "updateDate"`);
    }
}

重要(多分)な点として、 SQL ではデータ型で何もしないと Nullable になる、ということです。
Entity クラスはデフォルトで Not Null なので、実行するとエラーになって??になる、という。

マイグレーション実行(失敗)

"migration:run" コマンドを実行すると処理が反映されるはず。

npx typeorm migration:run

しかし実際はエラーになります。

import {Entity, PrimaryGeneratedColumn, Column} from "typeorm";
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at wrapSafe (internal/modules/cjs/loader.js:1101:16)
    at Module._compile (internal/modules/cjs/loader.js:1149:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1205:10)
    at Module.load (internal/modules/cjs/loader.js:1034:32)
    at Function.Module._load (internal/modules/cjs/loader.js:923:14)
    at Module.require (internal/modules/cjs/loader.js:1074:19)
    at require (internal/modules/cjs/helpers.js:72:18)
    at Function.PlatformTools.load (C:\Users\example\Documents\workspace\gen-typeorm-sample\node_modules\typeorm\platform\PlatformTools.js:114:28)
    at C:\Users\example\Documents\workspace\gen-typeorm-sample\node_modules\typeorm\util\DirectoryExportedClassesLoader.js:39:69
    at Array.map (<anonymous>)

マイグレーションを実行する(成功)

これは、今回 "ts-node" を使用していることが原因のようです。
下記のように実行すれば OK です。

npx ts-node ./node_modules/typeorm/cli.js migration:run

また、処理を取り消したい場合は下記のようにすれば OK です。

npx ts-node ./node_modules/typeorm/cli.js migration:revert

"migration:revert" を実行すると、最後に実行した処理が取り戻されます。

"migration" テーブル

"migration:run" を最初に実行すると、"migration" というテーブルが追加されます。 f:id:mslGt:20200607163135j:plain

反映された処理はこのテーブルに記録されているため、下記のような操作をすると、最後の処理が無視されます。

  1. マイグレーションファイルによってテーブルを作る。
  2. PgAdmin などを使って直接 1.のテーブルを削除する。
  3. "migration:run" で 1.を再実行する。

いつ新しい ID が発行されるか

たとえば2つのテーブル(テーブル A 、B とする)に一度に追加する場合。
B が A の ID を参照しているとします。
これを実現しようとすると、 A にデータを追加したあと、その ID を B に渡す必要があります。

では、A の ID はいつ発行されるでしょうか。
というのを試してみます。

index.ts

import "reflect-metadata";
import {createConnection } from "typeorm";
import { SampleUser } from "./entity/sample-user";

createConnection().then(async connection => {
   const queryRunner = connection.createQueryRunner();
   await queryRunner.startTransaction();
   try{
      const thirdUser = new SampleUser();
      thirdUser.firstName = 'Hello4';
      thirdUser.lastName = 'World4';
      thirdUser.age = 43;

      console.log("Before saving");
      console.log(thirdUser);

      await queryRunner.manager.save(thirdUser);
   
      console.log("Before committing");
      console.log(thirdUser);
      
      queryRunner.commitTransaction();
   
      console.log("After");
      console.log(thirdUser);
    }catch(error) {
        await queryRunner.rollbackTransaction();
    }
    finally {
        await queryRunner.release();
    }
}).catch(error => console.log(error));

結果

Before saving
SampleUser { id: -1, firstName: 'Hello4', lastName: 'World4', age: 43 }
Before committing
SampleUser { id: 6, firstName: 'Hello4', lastName: 'World4', age: 43 }
After
SampleUser { id: 6, firstName: 'Hello4', lastName: 'World4', age: 43 }

ということで、 "queryRunner.manager.save" 実行後に ID が取得できます。

外部キー

"SampleUser" の "id" を外部キーとして設定するにはどうすれば良いでしょうか。

@ManyToOne(), @OneToMany(), @OneToOne(), @JoinColumn() を使って実現できます。

sample-user.ts

import {Entity, PrimaryGeneratedColumn, Column, OneToMany, UpdateDateColumn} from "typeorm";
import { Post } from "./post";

@Entity("SampleUser")
export class SampleUser {
    @PrimaryGeneratedColumn()
    id: number = -1;

    @Column({ type: 'text' })
    firstName: string = '';

    @Column({ type: 'text' })
    lastName: string = '';

    @Column()
    age: number = -1;

    @UpdateDateColumn({ type: 'timestamptz' })
    updateDate: Date = new Date();

    @OneToMany(type => Post, post => post.user)
    posts: Post[]|null = null;

}

post.ts

import {Entity, PrimaryGeneratedColumn, Column, ManyToOne, UpdateDateColumn, OneToOne, JoinColumn} from "typeorm";
import { SampleUser } from "./sample-user";
import { Category } from "./category";

@Entity("Post")
export class Post {
    @PrimaryGeneratedColumn()
    id: number = -1;

    @ManyToOne(type => SampleUser, user => user.posts)
    user: SampleUser = new SampleUser();

    @OneToOne(() => Category)
    @JoinColumn([{
        name: 'categoryId',
        referencedColumnName: 'id'
    }])
    category: Category = new Category();

    @Column({ type: 'text' })
    title: string = '';

    @Column({ type: 'text' })
    article: string = '';

    @UpdateDateColumn({ type: 'timestamptz' })
    updateDate: Date = new Date();
}

category.ts

import {Entity, PrimaryGeneratedColumn, Column, UpdateDateColumn} from "typeorm";

@Entity("Category")
export class Category {
    @PrimaryGeneratedColumn()
    id: number = -1;

    @Column({ type: 'text' })
    name: string = '';

    @UpdateDateColumn({ type: 'timestamptz' })
    updateDate: Date = new Date();
}

テーブル生成を synchronize: true ですべきかマイグレーションファイルですべきか

これまで、 ormconfig.json で "synchronize" を true に設定して実行していました。
そのため、実行したときにテーブルが存在していなければ自動で生成されていました。

しかし、テーブルの追加はマイグレーションファイルに "CREATE TABLE" を書いても実現できます。

1591186678544-CreateSampleUserTable.ts

import {MigrationInterface, QueryRunner} from "typeorm";

export class AddCreateSampleUserTable1591186678544 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`CREATE TABLE "SampleUser" ( id serial PRIMARY KEY, "firstName" text NOT NULL, "lastName" text NOT NULL, "age" integer NOT NULL)`);
    }
    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`DROP TABLE "SampleUser"`);
    }
}

どちらを使うべきでしょうか。

という答えはまだ見つけられていないのですが、少なくともマスタテーブル(基本的に更新せず、他のテーブルから参照するためのデータを持つテーブル)については "synchronize" では解決できないので、マイグレーションファイルからテーブル生成も行おうかと思います。

1591356501422-AddCategories.ts

import {MigrationInterface, QueryRunner} from "typeorm";
import { Category } from "../entity/category";
export class AddCategories1591356501422 implements MigrationInterface {
    public async up(queryRunner: QueryRunner): Promise<void> {
        const programming = new Category();
        programming.name = 'Programming';
        await queryRunner.manager.save(programming);
        const book = new Category();
        book.name = 'Book';
        await queryRunner.manager.save(book);
    }
    public async down(queryRunner: QueryRunner): Promise<void> {
        await queryRunner.query(`DELETE FROM "Category"`);
    }
}