はじめに
こんにちは!ポーラ・オルビスホールディングスのITプロダクト開発チームでエンジニアをしている大野です。
私の所属するITプロダクト開発チームでは、技術力向上のために好きな技術で何らか役にたつものを作るプロジェクトを行なっており 、今回NestJS + TypeORMでAPIを一本作成したので軽く使い方を書いておこうと思います。
ORMやマイグレーションツールの導入を考えてるエンジニアの方のためになれば幸いです。
TypeORMってなんだ?
TypeORMはts, jsで使用できるORMの一種です。
公式サイト: https://typeorm.io/
最近のjs,tsのORMといえばprismaってイメージなんですが、2024年くらいまではユーザ数的にはそんなに大きな差はなかったみたいですね。ちょっと意外。
https://npmtrends.com/prisma-vs-sequelize-vs-typeorm
事前準備
ORMなので、まずいじくる対象のDBがないと話にならないので適当にDB作りましょう。
services:
db:
platform: linux/x86_64
image: mysql:9.1.0
command: mysqld --datadir=./data:/var/lib/mysql --user=mysql_user
volumes:
- db-data:/var/lib/mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: hogehoge_root
MYSQL_DATABASE: testdatabase
MYSQL_USER: mysql_user
MYSQL_PASSWORD: hogehoge
cap_add:
- SYS_NICE
volumes:
db-data:
適当に繋いどきましょう。
import { DataSource } from 'typeorm'
// マイグレーション用のデータソース
export default new DataSource({
type: 'mysql',
host: process.env.DB_HOST || '127.0.0.1',
port: 3306,
username: process.env.DB_USER,
password: process.env.DB_PASS,
entities: ['./**/*.entity{.ts,.js}'],
migrations: ['./dist/**/*migrations*{.ts,.js}'],
database: 'testdatabase',
logging: true
})
あとは実行用のtypeorm-ts-node-commonjsコマンドいちいち打つの大変なので、
コマンドを適当に作成しておきましょう。
dependenciesも適当に。
{
...中略....
"scripts": {
"build": "npx tsc",
"typeorm": "typeorm-ts-node-commonjs -d ./data-source.ts"
},
"dependencies": {
"mysql2": "^3.11.5",
"typeorm": "^0.3.20",
"@types/node": "^20.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.3"
}
}
TypeORMのつかいかた
大体のものは公式見たほうが早いですが、一応軽くまとめておきます
https://orkhan.gitbook.io/typeorm/docs
基本的なつかいかた
まずはテーブル定義ですね
@Entity()
がついているClassを作成するとそれがテーブルになります、
制約等もClassにアノテーションをつけるといい感じに
カラムには@Column()
をつけましょう
notnullやデフォルト値等もセットできます
ためしにテーブル作るとこんな感じ
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id!: string
@Column() // デフォルトだとnot null制約付き
email: string
@Column({ nullable: true }) // null許容の場合はnullableをつける
age: number // 数字とか
@Column({ default: '名無しさん' }) // デフォルト値
name: string
@Column()
birthday: Date // dateも入れれる
}
@Column()
だけだと基本的にnotnull制約がつきます
notNull許容する場合はnullable: true
を指定しましょう
Dateとかnumberとかstringとかenumとか型もいろいろ使えます。詳しくは公式。
これで一旦typeORMのgenerateを起動してみましょう
npm run typeorm migration:generate migrations
そうするとこんなファイルができました
export class Migrations1751531581762 implements MigrationInterface {
name = 'Migrations1751531581762'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`user\` (\`id\` varchar(36) NOT NULL, \`email\` varchar(255) NOT NULL, \`age\` int NULL, \`name\` varchar(255) NOT NULL DEFAULT '名無しさん', \`birthday\` datetime NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE \`user\``);
}
}
これのup関数が作成時に実際に実行されるSQL、downがこのSQLを元に戻したい時に実行するSQLになります。
先ほど作成したクラスがテーブルとして作成されて、not null制約や型等も考慮したSQLが自動で作成されてますね!
このマイグレーションファイルを実際にDBに反映するのは以下コマンドで実行できます
npm run build
npm run typeorm migration:run
// > typeorm-ts-node-commonjs -d ./data-source.ts migration:run
migration:run すると先ほどのmigrationファイルのupに書かれたものが実行され、DBに実際に値がセットされます。
逆に元に戻したい場合はmigration:revertを実行しましょう。
リレーション
リレーション周りもtypeORMで設定できます。
1対1はあんまそんなに使わないと思うんで割愛。
1対多
特に意味はないですがUserは複数の寿司を持ちましょう。寿司好きなんで。
Userは板前ってことにしましょう。
1対多はOneToManyとManyToOneで設定します。
SushiオブジェクトのUserと、UserオブジェクトのSushiの紐付けをしてみましょう。
@OneToMany(() => Sushi, sushi => sushi.user) // Sushiクラスのuser要素と紐付け
sushi: Sushi[] // ユーザ(板前)が握った寿司のリスト
@Entity()
export class Sushi {
@PrimaryGeneratedColumn()
id: number;
@Column()
sushiName: string;
@ManyToOne(() => User, user => user.sushi) // Userクラスのsushi要素と紐付け
user: User; // 寿司を握ったユーザ
}
いい感じ。
そうしたら先ほど実施したmigration:generate
コマンドを再度実行してマイグレーションファイル作成してみましょう。
import { MigrationInterface, QueryRunner } from "typeorm";
export class Migrations1751534125347 implements MigrationInterface {
name = 'Migrations1751534125347'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`sushi\` (\`id\` int NOT NULL AUTO_INCREMENT, \`sushiName\` varchar(255) NOT NULL, \`userId\` varchar(36) NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`sushi\` ADD CONSTRAINT \`FK_c5d3e7a6ed3871fd262d790dc52\` FOREIGN KEY (\`userId\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`sushi\` DROP FOREIGN KEY \`FK_c5d3e7a6ed3871fd262d790dc52\``);
await queryRunner.query(`DROP TABLE \`sushi\``);
}
}
Sushiが作成されるSQLが作られ、userIdが紐づけられてますね。
外部キー制約も付いてますね、偉い。
多対多
今度は寿司から寿司セットを作りましょう。
ただの寿司セットだと多対多にならなさそうなんで、複数のセットに同じ寿司を入れるタイプの寿司屋にしましょう。
寿司セットは複数の寿司を持っており、寿司は複数の寿司セットに紐づきます。
import { Column, Entity, JoinTable, ManyToMany, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { User } from './user.entity';
import { Sushi } from './sushi.entity';
@Entity()
export class SushiSet {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@ManyToMany(() => Sushi, sushi => sushi.sushiSet)
@JoinTable()
sushi: Sushi[]; // セットに入っている寿司
}
@ManyToMany(() => SushiSet, sushiSet => sushiSet.sushi)
sushiSet: SushiSet[]; // 自分が含まれる寿司セット
基本的にはManyToOne等と同じなんですが、JoinTableというアノテーションが必須です。
JoinTableをつけると中間テーブルが勝手にできます。JoinTableはデータの所有者側につけるのが一般的ぽいです。
今回のケースだと寿司セットが複数の寿司を持っているので、寿司セット側にJoinTableつけましょう。
これで再度migration:generate
コマンドを実行するとこんな感じ。
import { MigrationInterface, QueryRunner } from "typeorm";
export class Migrations1751627775359 implements MigrationInterface {
name = 'Migrations1751627775359'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`sushi_set\` (\`id\` int NOT NULL AUTO_INCREMENT, \`name\` varchar(255) NOT NULL, PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`sushi_set_sushi_sushi\` (\`sushiSetId\` int NOT NULL, \`sushiId\` int NOT NULL, INDEX \`IDX_0dcdd129c864d9e65e615d87fd\` (\`sushiSetId\`), INDEX \`IDX_9acafa21a3f32c0317a865b9dc\` (\`sushiId\`), PRIMARY KEY (\`sushiSetId\`, \`sushiId\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`sushi_set_sushi_sushi\` ADD CONSTRAINT \`FK_0dcdd129c864d9e65e615d87fde\` FOREIGN KEY (\`sushiSetId\`) REFERENCES \`sushi_set\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
await queryRunner.query(`ALTER TABLE \`sushi_set_sushi_sushi\` ADD CONSTRAINT \`FK_9acafa21a3f32c0317a865b9dc8\` FOREIGN KEY (\`sushiId\`) REFERENCES \`sushi\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`sushi_set_sushi_sushi\` DROP FOREIGN KEY \`FK_9acafa21a3f32c0317a865b9dc8\``);
await queryRunner.query(`ALTER TABLE \`sushi_set_sushi_sushi\` DROP FOREIGN KEY \`FK_0dcdd129c864d9e65e615d87fde\``);
await queryRunner.query(`DROP INDEX \`IDX_9acafa21a3f32c0317a865b9dc\` ON \`sushi_set_sushi_sushi\``);
await queryRunner.query(`DROP INDEX \`IDX_0dcdd129c864d9e65e615d87fd\` ON \`sushi_set_sushi_sushi\``);
await queryRunner.query(`DROP TABLE \`sushi_set_sushi_sushi\``);
await queryRunner.query(`DROP TABLE \`sushi_set\``);
}
}
ちょっと見づらい長さになってきましたが、sushi_setテーブルだけでなく sushi_set_sushi_sushiテーブルも作成されてますね、
多対多の中間テーブルも勝手に作ってくれるの結構楽な気がします。
まとめ
TypeORMで一旦テーブル作成をやってみました。
結構楽にテーブル作成できるのでいい感じだなと思いました。
なんか型安全じゃないみたいな話もちらほら聞きますが、prismaよりはtypescriptライクに書けるので、TypescriptでORM使う時の選択肢にはなるのかもしれません。
次はこれとnestでAPIまで作った話が書ければと思います。