背景
TypeORMで最も時間がかかった多対多テーブルの実装方法について、一通りの実装例を以下に記載する。
こういう人向け
- 筆者自身。こういう風にコーディングしましたと自他に後で見せるため。
- 上記公式ドキュメントを見たりしてそのまま実装しようとしたら「えっ…中間テーブルの消し方…どうなってんの!?」と思った方
- TypeORMにて多対多テーブルをどんな風に他の人が組んでるのか知りたい方
前提
- TypeORMのEntityをテーブルの設計書代わりとしたい(≒Entityファイルを見れば、どういったデータベース定義か自明となるようにしたい)
- 上記より、フレームワーク側が勝手に定義・作成するテーブルは許容しづらい。ヒトの定義でコントロールしたい。
- その上で、typescriptとtypeormが生み出す「静的型付け」「Entityにもとづくテーブル自動生成」を行いたい
- DBはmysql、typeormバージョンは
^0.2.37
実装例
この方の記事が分かりやすかったので、上記の「ユーザ」多対多「コース」を題材とする。
user
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, JoinColumn } from "typeorm";
import { date_dictionary } from "./datadirectory"
import { user_course } from "./user_course";
// ユーザテーブル。
@Entity({ name: "t_user",synchronize:true })
export class t_user extends BaseEntity {
// primary keyやtype、サイズ、コメントも設定できる。
@PrimaryGeneratedColumn({ name: "id", type: "bigint", comment: '自動採番ID' })
id!: number;
@Column({ nullable: false, type: "varchar", length: 12, comment: "ユーザ名" })
userName!: string;
// 共通カラム使用。デフォルトだとカラム名が連結される(dateTypeUpdatorみたいになる)ので、prefix:falseにしている
@Column(() => date_dictionary, { prefix: false })
dateType!: date_dictionary
// 中間テーブルを対象に、一対多でタイプを宣言する。
@JoinColumn()
user_course!: user_course[]
}
course
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, JoinColumn } from "typeorm";
import { date_dictionary } from "./datadirectory"
import { user_course } from "./user_course";
// コードマスタ定義。
@Entity({ name: "t_course",synchronize:true })
export class t_course extends BaseEntity {
// primary keyやtype、サイズ、コメントも設定できる。
@PrimaryGeneratedColumn({ name: "id", type: "bigint", comment: '自動採番ID' })
id!: number;
@Column({ nullable: false, type: "varchar", length: 12, comment: "コース名" })
courseName!: string;
// 共通カラム使用。デフォルトだとカラム名が連結される(dateTypeUpdatorみたいになる)ので、prefix:falseにしている
@Column(() => date_dictionary, { prefix: false })
dateType!: date_dictionary
@JoinColumn()
user_course!: user_course[]
}
中間テーブルuser_course
import { Entity, Column, BaseEntity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm";
import { date_dictionary } from "./datadirectory"
import { t_user } from "./t_user";
import { t_course } from "./t_course";
// 多対多テーブルのユーザ・コーステーブルの中間テーブル。
@Entity({ name: "user_course",synchronize:true })
export class user_course extends BaseEntity {
// Foreign keyのための設定。
@ManyToOne(()=> t_user, user => user.id,{})
// JoinColumnでnameをしておかないとuserIdIdみたいな新規カラムを生み出してくる。
@JoinColumn({ name: "userId" })
// primary keyやtype、サイズ、コメントも設定できる。
@PrimaryColumn({ nullable: false, type: "bigint", comment: "ユーザID" })
userId!: number;
@ManyToOne(()=> t_course, course => course.id)
@JoinColumn({ name: "courseId" })
@PrimaryColumn({ nullable: false, type: "bigint", comment: "コースID" })
courseId!: number;
@Column({ nullable: false, type: "int", comment: "コース進捗度" })
progressNum!: number;
// 共通カラム使用。デフォルトだとカラム名が連結される(dateTypeUpdatorみたいになる)ので、prefix:falseにしている
@Column(() => date_dictionary, { prefix: false })
dateType!: date_dictionary
// ユーザ・コースを、このテーブル視点から多対一で結ぶ。
@JoinColumn()
user!:t_user
@JoinColumn()
course!:t_course
}
上記Entity+「synchronize: true」によって自動生成される中間テーブルuser_course
CREATE TABLE `user_course` (
`userId` bigint(20) NOT NULL COMMENT 'ユーザID',
`courseId` bigint(20) NOT NULL COMMENT 'コースID',
`creator` varchar(12) DEFAULT NULL COMMENT '作成者',
`createdAt` datetime(6) DEFAULT current_timestamp(6) COMMENT '作成日時',
`updator` varchar(12) DEFAULT NULL COMMENT '更新者',
`updatedAt` datetime(6) DEFAULT current_timestamp(6) ON UPDATE current_timestamp(6) COMMENT '更新日時',
`progressNum` int(11) NOT NULL COMMENT 'コース進捗度',
PRIMARY KEY (`userId`,`courseId`),
KEY `FK_67a940b1d7b3cc2f0e99ab6d23b` (`courseId`),
CONSTRAINT `FK_63b2ec4f34c89d4b1219f85a806` FOREIGN KEY (`userId`) REFERENCES `t_user` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT `FK_67a940b1d7b3cc2f0e99ab6d23b` FOREIGN KEY (`courseId`) REFERENCES `t_course` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8
ManyToOneによって外部キーもしっかり宣言されている。
あまり関係ない共通部品
import { Column, UpdateDateColumn, CreateDateColumn } from "typeorm";
// @Entityが無い、テーブルとしては登録されないクラス。共通カラムを別々の場所で使う時に使える。
export class date_dictionary {
@Column({ nullable: true, type: "varchar", length: 12, comment: "作成者" })
creator!: string;
@CreateDateColumn({ nullable: true, type: "datetime", comment: "作成日時" })
createdAt!: Date;
@Column({ nullable: true, type: "varchar", length: 12, comment: "更新者" })
updator!: string;
@UpdateDateColumn({ nullable: true, type: "datetime", comment: "更新日時" })
updatedAt!: Date;
}
select join
join系のメソッドを色々試したが、「innerJoinAndMapMany」系が一番使いやすかった。
// ユーサ~中間テーブル~コースをjoinして取得する
let manymanyResult = await con.createQueryBuilder(t_user, "user")
.innerJoinAndMapMany("user.user_course",user_course,"user_course", "user.id = user_course.userId ")
.leftJoinAndMapMany("user_course.course",t_course,"course", "course.id = user_course.courseId AND user.id = user_course.userId")
.getMany();
console.dir(manymanyResult,{depth:null})
INNER JOINs entity's table, SELECTs the data returned by a join and MAPs all that data to some entity's property. This is extremely useful when you want to select some data and map it to some virtual property. It will assume that there are multiple rows of selecting data, and mapped result will be an array. You also need to specify an alias of the joined data. Optionally, you can add condition and parameters used in condition.
↓deepl翻訳
エンティティのテーブルをINNER JOINし、結合によって返されたデータをSELECTし、そのすべてのデータをあるエンティティのプロパティにマッピングします。これは、あるデータを選択し、それをある仮想プロパティにマッピングしたい場合に非常に便利です。複数行のデータが選択されていることを想定し、マッピングされた結果は配列になります。また、結合したデータのエイリアスを指定する必要があります。オプションとして、条件や条件に使用するパラメータを追加することができます。
JoinAndMapMany系:("エイリアス.joinするエイリアス", joinするEntity,"joinするエイリアス", "join条件",{param])
一応上記で下記のように上手くいった。
manymanyResult取得結果
[
t_user {
id: '1',
userName: 'user1',
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:20:25.507Z,
updator: null,
updatedAt: 2021-09-09T00:20:25.507Z
},
user_course: [
user_course {
userId: '1',
courseId: '1',
progressNum: 1,
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:20:48.112Z,
updator: null,
updatedAt: 2021-09-09T01:02:27.733Z
},
course: [
t_course {
id: '1',
courseName: 'Aコース',
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:19:55.973Z,
updator: null,
updatedAt: 2021-09-09T00:43:39.479Z
}
}
]
},
user_course {
userId: '1',
courseId: '2',
progressNum: 2,
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:21:00.955Z,
updator: null,
updatedAt: 2021-09-09T01:02:28.717Z
},
course: [
t_course {
id: '2',
courseName: 'Bコース',
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:20:06.046Z,
updator: null,
updatedAt: 2021-09-09T00:43:43.822Z
}
}
]
},
user_course {
userId: '1',
courseId: '3',
progressNum: 0,
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:44:07.213Z,
updator: 'test',
updatedAt: 2021-09-09T05:32:08.983Z
},
course: [
t_course {
id: '3',
courseName: 'Cコース',
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:43:34.510Z,
updator: null,
updatedAt: 2021-09-09T00:43:34.510Z
}
}
]
}
]
},
t_user {
id: '2',
userName: 'user2',
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:20:33.497Z,
updator: null,
updatedAt: 2021-09-09T00:20:33.497Z
},
user_course: [
user_course {
userId: '2',
courseId: '2',
progressNum: 1,
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:21:11.953Z,
updator: null,
updatedAt: 2021-09-09T01:02:29.373Z
},
course: [
t_course {
id: '2',
courseName: 'Bコース',
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:20:06.046Z,
updator: null,
updatedAt: 2021-09-09T00:43:43.822Z
}
}
]
},
user_course {
userId: '2',
courseId: '3',
progressNum: 1,
dateType: date_dictionary {
creator: '',
createdAt: 2021-09-09T00:43:58.108Z,
updator: '',
updatedAt: 2021-09-09T01:02:29.877Z
},
course: [
t_course {
id: '3',
courseName: 'Cコース',
dateType: date_dictionary {
creator: null,
createdAt: 2021-09-09T00:43:34.510Z,
updator: null,
updatedAt: 2021-09-09T00:43:34.510Z
}
}
]
}
]
}
]
ユーザが2人いて、それぞれ受けているコースが異なる。これらを中間テーブルに入れたコース進捗度(progressNum)含めて取得することができる。
t_user[]
として取得できるため、見た目以上に入力補完が容易。
中間テーブルのEntityを既に用意しているため、insert・update・deleteなりも好きにできる。
終わりに
TypeORMは公式ドキュメントに書かれていないオプションやメソッド、実装の仕方が意外とやりやすかったりする。
公式ドキュメントのようなシンプルな書き方で上手く行けばいいが、内部でやっていることが多すぎてエラーややれないこと、一目見て分からないことが増えていくため、結局ヒトが裏側に気をつけることが必要になってしまう。
ただ静的型付けとEntityにもとづくテーブル自動生成は良いと考える。