🕊

TypeORMで多対多の自己結合アソシエーション定義

2024/02/27に公開

はじめに

こんにちは、今回は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_idfollowed_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に定義したアソシエーション名を指定
    });
  }
}

上記のように、getFollowingAndFollowersfindOneメソッドで、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へのアーキテクチャ移行等にも取り組んでいます。
カジュアルな面談でも構いませんので、ご興味のある方は是非ご応募お待ちしております!
https://spacemarket.co.jp/recruit/engineer/

スペースマーケット Engineer Blog

Discussion