👬

TypeORMでアソシエーションを使用する

2023/03/31に公開

こんにちは、株式会社スペースマーケットのエンジニアのtchmrです。

弊社のAPIはRailsがメインとなっていますが、段階的にNestJSにリプレイスしています。
NestJSのキャッチアップの中で学んだことを共有できればと思います。

記事のスコープ

TypeORMを用いて関連先のデータを含めて取得する方法について。
Railsにおけるhas_many, belongs_toのようなアソシエーションをNestJS + TypeORM環境においてどう実現するのかを見ていきます。

前提

お互いに関連を持つ以下の2つのテーブルを例に見ていきます。

  • users: 複数のphotosを持つ
  • photos: 一つのuserを持つ

アソシエーションの定義

@Entity()
export class User {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    name: string

    @OneToMany(() => Photo, (photo) => photo.user) // アソシエーション定義
    photos: Photo[]
}
@Entity()
export class Photo {
    @PrimaryGeneratedColumn()
    id: number

    @Column()
    url: string

    @ManyToOne(() => User, (user) => user.photos) // アソシエーション定義
    user: User
}

データ取得

以下の2つの方法で取得することができます。

find* + relationsオプション

find*メソッドにrelationsオプションを指定することで関連先の情報も併せて取得することができます。
TypeORMのREADMEにも記載されているとおり実態としてはLEFT JOINされています。
https://github.com/typeorm/typeorm/blob/master/docs/find-options.md

// photosを含むuserオブジェクトを取得
const userRepository = dataSource.getRepository(User)
const users = await userRepository.find({
    relations: {
        photos: true, // relationsオプションが必要
    },
})

// userを含むphotoオブジェクトを取得
const photoRepository = dataSource.getRepository(Photo)
const photos = await photoRepository.find({
    relations: {
        user: true, // relationsオプションが必要
    },
})

queryBuilder

queryBuilderを使うことでも関連先データを含めて取得することができます。
こちらの記法はLEFT JOINしていることが一目瞭然ですね。

// photosを含むuserオブジェクトを取得
const users = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.photos", "photo")
    .getMany()

// userを含むphotoオブジェクトを取得
const photos = await dataSource
    .getRepository(Photo)
    .createQueryBuilder("photo")
    .leftJoinAndSelect("photo.user", "user")
    .getMany()

取得データについて

上記で取得したデータについて見ていきます。

// 取得データを確認
console.log(users[0]);
// 出力結果
User {
  id: 1,
  name: 'TCHMR',
  photos: [
    Photo {
      id: 10,
      url: 'https://xxx.com',
    },
    Photo {
      id: 20,
      url: 'https://yyy.com',
    },
  ],
}

したがって、以下のように関連先の情報を参照することができます。

console.log(users[0].photos[0].url)
// 出力結果
'https://xxx.com'

queryBuilderパターンにおけるアソシエーション定義の意義

queryBuilderを用いるパターンでは、上記の通りrepository層で明示的にLEFT JOINしていることから@OneToManyといったアソシエーション定義はなくてもデータ取得できたりするのでは?と考えて、アソシエーション定義を削除して実行したところ以下のエラーが発生しました。

TypeORMError: Relation with property path photo in entity was not found.

ここで分かったことは2点です。

  1. queryBuilderを用いて明示的にleftJoinAndSelectを実行するとしてもアソシエーション定義は必要
  2. queryBuilderを用いる場合、実行時にエラーとなる

一方でfind* + relationsパターンの場合、アソシエーション定義がないとコンパイル時に型エラーが検出されます。

Type '{ photo: true; }' is not assignable to type 'FindOptionsRelationByString | FindOptionsRelations<User>'.

実行前に未然にバグを検出できるためより安全性が高い方法と言えるかと思います。

どちらの方法が良いかは多面的に判断する必要がありますが、エラーを未然に検出できるという点についてはfind*パターンにアドバンテージがあると言えるでしょう。

まとめ

以上、TypeORMで関連先のデータを参照する方法について見ていきました。
記述を少し追加する程度で関連先のデータを取得できることがわかり、この点についてはRailsからの移行もさほど違和感なくできそうな印象を持ちました。

さいごに

冒頭でも記載したとおり、弊社はRailsからNestJSへの移行を行っており、Railsの知見を活かしながらもNestJSの経験値を積むことができるフェーズです。
NestJSに興味がある、培ってきたRailsスキルを活かしたいといった方にとって特にマッチする環境をご提供することができるかと思っています。
もし少しでもご興味があればぜひ弊社の採用ページをご覧ください。

https://www.wantedly.com/projects/1113570

https://www.wantedly.com/projects/1113544

https://www.wantedly.com/projects/1061116

https://spacemarket.co.jp/recruit/engineer/

スペースマーケット Engineer Blog

Discussion