typeormで多対多(@ManyToMany)を実装する
概要
過去に typeorm の交差(中間)テーブルを手動で実装する方法について以下にまとめました。
今回は、typeorm で交差(中間)テーブルを自動生成する方法についてまとめます。
Cascade を指定しないとうまく動作しなかったりするので、そちらについてもまとめたいと考えています。
ソースコードは以下です。
下準備
環境
macOS に node v14.14.0 と docker compose を用いて検証をしました。
node のインストール方法は、以下の記事を参照してください。
ER 図
使用する DB は、以下の図です。
User
に対してFruit
とBasket
が一対多、Fruit
とBasket
が多対多になっています。
ER 図
実装
User エンティティ
User
のエンティティは以下のソースコードです。
@OneToMany
で一対多を実現しています。
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Basket } from "./Basket";
import { Fruit } from "./Fruit";
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
firstName: string;
@Column()
lastName: string;
@Column()
age: number;
@OneToMany(() => Fruit, (fruit) => fruit.user)
fruits: Fruit[];
@OneToMany(() => Basket, (basket) => basket.user)
baskets: Basket[];
}
Fruit エンティティと Basket エンティティ
次に、Fruit
とBasket
のエンティティは以下になります。
User
とはOneToMany
で一対多のリレーションを実現しています。
Fruit
とBasket
はManyToMany
で多対多を生成できます。
また、onDelete: "CASCADE"
で、片方が削除されたときに交差(中間)テーブルが自動で更新できるようになっています。
import {
Column,
Entity,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
PrimaryGeneratedColumn,
} from "typeorm";
import { Basket } from "./Basket";
import { User } from "./User";
@Entity()
export class Fruit {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
userId: number;
@ManyToOne(() => User, (user) => user.id, {
onDelete: "CASCADE",
nullable: false,
})
@JoinColumn({ name: "userId" })
user!: User;
@ManyToMany(() => Basket, (basket) => basket.fruits, {
onDelete: "CASCADE",
})
baskets: Basket[];
}
import {
Column,
Entity,
JoinColumn,
JoinTable,
ManyToMany,
ManyToOne,
PrimaryGeneratedColumn,
} from "typeorm";
import { User } from "./User";
import { Fruit } from "./Fruit";
@Entity()
export class Basket {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
userId: number;
@ManyToOne(() => User, {
onDelete: "CASCADE",
nullable: false,
})
@JoinColumn()
user!: User;
@ManyToMany(() => Fruit, (fruit) => fruit.baskets, {
onDelete: "CASCADE",
})
@JoinTable()
fruits: Fruit[];
}
動作確認
下準備
GitHub から clone してください。
git clone https://github.com/Msksgm/typeorm-with-cascade.git
cd typeorm-with-cascade
yarn
src/index.ts
には以下のように、一対多(User
対Fruit
、Basket
)と多対多(User
対Basket
)の動作確認ができるように実装しています。
import "reflect-metadata";
import { createConnection } from "typeorm";
import { User } from "./entity/User";
import { Basket } from "./entity/Basket";
createConnection()
.then(async (connection) => {
console.log("db connection is success");
// 一対多
const fruitByUser = await connection
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.fruits", "fruit")
.where("user.id=2")
.getOne();
console.log("UserとFruit");
console.log(fruitByUser);
const basketByUser = await connection
.getRepository(User)
.createQueryBuilder("user")
.leftJoinAndSelect("user.baskets", "basket")
.where("user.id=1")
.getOne();
console.log("UserとBasket");
console.log(basketByUser);
// 多対多
const fruitByBasket1 = await connection
.getRepository(Basket)
.createQueryBuilder("basket")
.where("basket.id=1")
.leftJoinAndSelect("basket.fruits", "fruit")
.getOne();
console.log("Basketに含まれるFruit1");
console.log(fruitByBasket1);
const fruitByBasket2 = await connection
.getRepository(Basket)
.createQueryBuilder("basket")
.where("basket.id=2")
.leftJoinAndSelect("basket.fruits", "fruit")
.getOne();
console.log("Basketに含まれるFruit2");
console.log(fruitByBasket2);
})
.catch((error) => console.log(error));
DB サーバの起動
DB の生成には、docker compose を使います。
今回あらかじめtypeorm-with-cascade/initdb.d/sample.sql
に SQL を記述しています。
typeorm-with-cascade/docker-compose.yml
で docker compose 実行時に、読み込むようにしています。
docker compose up -d
DB の中身を確認すると以下のようになっています。
確認。
mysql -u root --port 3306 -h 127.0.0.1 -D typeorm_db -e 'SHOW tables;' -p
Enter password:
結果。
+----------------------+
| Tables_in_typeorm_db |
+----------------------+
| basket |
| basket_fruits_fruit |
| fruit |
| user |
+----------------------+
確認。
mysql -u root --port 3306 -h 127.0.0.1 -D typeorm_db -e 'SELECT * FROM basket; SELECT * FROM basket_fruits_fruit; SELECT * FROM fruit; SELECT * FROM user;' -p
結果。
+----+---------------------------------------------+--------+
| id | name | userId |
+----+---------------------------------------------+--------+
| 1 | Timber's basket | 1 |
| 2 | Timber とティンバーのバスケット | 2 |
+----+---------------------------------------------+--------+
+----------+---------+
| basketId | fruitId |
+----------+---------+
| 1 | 1 |
| 1 | 2 |
| 2 | 1 |
| 2 | 2 |
| 2 | 3 |
| 2 | 4 |
+----------+---------+
+----+--------------------------+--------+
| id | name | userId |
+----+--------------------------+--------+
| 1 | orange | 1 |
| 2 | grape | 1 |
| 3 | グレープフルーツ | 2 |
| 4 | みかん | 2 |
+----+--------------------------+--------+
+----+-----------------+----------+-----+
| id | firstName | lastName | age |
+----+-----------------+----------+-----+
| 1 | Timber | Saw | 25 |
| 2 | ティンバー | ソウ | 25 |
+----+-----------------+----------+-----+
実行
yarn start
で実行します。
もしエラーが発生した場合は、DB サーバの起動でエラーが発生している可能性があるので、docker compose up
(-d を外す)でエラーがでているか確認してください。
yarn start
実行に成功すると、以下のように表示されます。
UserとFruit
User {
id: 2,
firstName: 'ティンバー',
lastName: 'ソウ',
age: 25,
fruits: [
Fruit { id: 3, name: 'グレープフルーツ', userId: 2 },
Fruit { id: 4, name: 'みかん', userId: 2 }
]
}
UserとBasket
User {
id: 1,
firstName: 'Timber',
lastName: 'Saw',
age: 25,
baskets: [ Basket { id: 1, name: "Timber's basket", userId: 1 } ]
}
Basketに含まれるFruit1
Basket {
id: 1,
name: "Timber's basket",
userId: 1,
fruits: [
Fruit { id: 1, name: 'orange', userId: 1 },
Fruit { id: 2, name: 'grape', userId: 1 }
]
}
Basketに含まれるFruit2
Basket {
id: 2,
name: 'Timber とティンバーのバスケット',
userId: 2,
fruits: [
Fruit { id: 1, name: 'orange', userId: 1 },
Fruit { id: 2, name: 'grape', userId: 1 },
Fruit { id: 3, name: 'グレープフルーツ', userId: 2 },
Fruit { id: 4, name: 'みかん', userId: 2 }
]
}
参考
Discussion