0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TypeORMを使って適当なテーブルを簡単に作成しよう

Posted at

はじめに

こんにちは!ポーラ・オルビスホールディングスの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作りましょう。

docker-compose.yaml
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:

適当に繋いどきましょう。

data-source.ts
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も適当に。

package.json
{ 
...中略.... 
  "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やデフォルト値等もセットできます

ためしにテーブル作るとこんな感じ

User.entity.ts

@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 

そうするとこんなファイルができました

1751531581762-migrations.ts
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の紐付けをしてみましょう。

user.entity.ts
  @OneToMany(() => Sushi, sushi => sushi.user) // Sushiクラスのuser要素と紐付け
  sushi: Sushi[] // ユーザ(板前)が握った寿司のリスト 
sushi.entity.ts
@Entity() 
export class Sushi { 
  @PrimaryGeneratedColumn() 
  id: number; 
  
  @Column() 
  sushiName: string; 

  @ManyToOne(() => User, user => user.sushi) // Userクラスのsushi要素と紐付け
  user: User; // 寿司を握ったユーザ 
} 

いい感じ。
そうしたら先ほど実施したmigration:generateコマンドを再度実行してマイグレーションファイル作成してみましょう。

1751534125347-migrations.ts
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が紐づけられてますね。
外部キー制約も付いてますね、偉い。

多対多

今度は寿司から寿司セットを作りましょう。
ただの寿司セットだと多対多にならなさそうなんで、複数のセットに同じ寿司を入れるタイプの寿司屋にしましょう。
寿司セットは複数の寿司を持っており、寿司は複数の寿司セットに紐づきます。

sushiSet.entity.ts
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[]; // セットに入っている寿司
}
sushi.entity.ts

@ManyToMany(() => SushiSet, sushiSet => sushiSet.sushi) 
sushiSet: SushiSet[]; // 自分が含まれる寿司セット 

基本的にはManyToOne等と同じなんですが、JoinTableというアノテーションが必須です。
JoinTableをつけると中間テーブルが勝手にできます。JoinTableはデータの所有者側につけるのが一般的ぽいです。
今回のケースだと寿司セットが複数の寿司を持っているので、寿司セット側にJoinTableつけましょう。

これで再度migration:generateコマンドを実行するとこんな感じ。

1751627775359-migrations.ts
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まで作った話が書ければと思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?