TypeORMで多対多の自己結合アソシエーション定義
はじめに
こんにちは、今回はTypeORMを使って、多対多の自己結合アソシエーション定義を行う方法について解説します。
多対多の自己結合とは
多対多の自己結合とは、同テーブル同士の結合を中間テーブルを介して結合し、結合した双方のテーブルのデータを取得することができるようにすることを指しています。
例を挙げるとするとわかりやすいのは、X(旧Twitter)のフォロー機能が該当します。
下記のようなテーブル構造を想定してみます。
usersテーブル
id | name |
---|---|
1 | ユーザーA |
2 | ユーザーB |
3 | ユーザーC |
4 | ユーザーD |
followsテーブル
id | follower_id | followed_id |
---|---|---|
1 | 1 | 2 |
2 | 1 | 3 |
3 | 2 | 1 |
4 | 3 | 1 |
5 | 4 | 1 |
上記のテーブル構造では、follows
テーブルが中間テーブルとして機能し、follower_id
とfollowed_id
がそれぞれusers
テーブルの主キーであるid
を参照しています。
もし、上記のようなテーブル構造においてユーザーAがフォローしているユーザーとフォローされているユーザーを取得したい場合、どのように実装するのが良いでしょうか?
1つの方法として、生のSQLを使いJOINを駆使して頑張る方法もあるかと思いますが、今回はTypeORMのアソシエーション定義を使ってより簡単に関連付けの実装を行ってみたいと思います。
実装
早速ですが、本題のアソシエーション定義を見ていきます。
まずは、usersテーブルの実体をUser
エンティティとして定義します。
// user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// フォローしているユーザー
@ManyToMany(() => User, user => user.followers)
@JoinTable({
name: 'follows',
joinColumn: { name: 'follower_id'},
inverseJoinColumn: { name: 'followed_id'}
})
following: User[];
// フォローされているユーザー
@ManyToMany(() => User, user => user.following)
@JoinTable({
name: 'follows',
joinColumn: { name: 'followed_id'},
inverseJoinColumn: { name: 'follower_id'}
})
followers: User[];
}
続いて、 Follow
エンティティを定義します。
// follow.entity.ts
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Follow {
@PrimaryGeneratedColumn()
id: number;
@Column()
followerId: number;
@Column()
followedId: number;
}
以上で、多対多の自己結合アソシエーション定義が完了しました。
ややこしいのは@JoinTable
の設定ですが、簡単にそれぞれを解説すると
-
name
中間テーブルの名前を入れます。 -
joinColumn.name
中間テーブルを結合する際に結合に使うキー名を指定します。following
を取得する場合は、follower_id
を結合のキーに使用する形となります。 -
inverseJoinColumn.name
上記の反対で、自己結合される側のテーブルと中間テーブルを結合する際に結合に使うキー名を指定します。following
を取得する場合は、followed_id
を結合のキーに使用する形となります。
続いて、データベースからUser
のエンティティを取得するためのリポジトリを作成します。
// user.repository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
@Injectable()
export class UserRepository{
constructor(
@InjectRepository(User)
private repository: Repository<User>,
) {}
// フォローしているユーザーとフォローされているユーザーを一括取得
async getFollowingAndFollowers(userId: number): Promise<User> {
return this.repository.findOne({
where: { id: userId },
relations: ['following', 'followers'], // UserEntityに定義したアソシエーション名を指定
});
}
}
上記のように、getFollowingAndFollowers
のfindOne
メソッドで、relations
にアソシエーション名を指定することで、指定したユーザーとそれに紐づくフォロー/フォロワー関係にあるユーザーを一括で取得することができます。
試しに、上記のメソッドに対してユーザーAのid: 1
を引数に入れて返却値を見てみましょう。
実行結果
User {
id: 1,
name: 'ユーザーA',
following: [
{ id: 2, name: 'ユーザーB'},
{ id: 3, name: 'ユーザーC'}
],
followers: [
{ id: 2, name: 'ユーザーB'},
{ id: 3, name: 'ユーザーC'},
{ id: 4, name: 'ユーザーD'}
]
}
ユーザーAがフォローしているユーザー2名とフォローされているユーザー3名が取得できていることが確認できますね。適切なアソシエーション定義ができていると言えそうです。
まとめ
今回は、TypeORMを使って多対多の自己結合アソシエーションを定義する方法について解説しました。TypeORMを使うことで、複雑なSQLを書かなくてもEntityにアソシエーションを定義するだけで、簡単に多対多の自己結合を実現できるのは嬉しいですね。
是非、同様のケースに出会した際は参考にしてみてください。
最後に
弊社では、NestJSへのアーキテクチャ移行等にも取り組んでいます。
カジュアルな面談でも構いませんので、ご興味のある方は是非ご応募お待ちしております!
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion