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" というテーブルが追加されます。
反映された処理はこのテーブルに記録されているため、下記のような操作をすると、最後の処理が無視されます。
- マイグレーションファイルによってテーブルを作る。
- PgAdmin などを使って直接 1.のテーブルを削除する。
- "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"`); } }