Dev Study
← 解説「リレーション — テーブル間の関連」に戻る

サンプルコードで身につける: リレーション — テーブル間の関連

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

11対多の最小定義

「1人のユーザーが複数の投稿を持つ」関係を、User 側 @OneToMany と Post 側 @ManyToOne で表す最小例です。多の側(Post)に外部キー列ができ、これがどの行とどの行が結びつくかを保持します。

import {
  Entity, PrimaryGeneratedColumn, Column,
  OneToMany, ManyToOne,
} from "typeorm";

@Entity()
export class User {
  @PrimaryGeneratedColumn() id: number;
  @OneToMany(() => Post, (post) => post.author) // 1 側
  posts: Post[];
}

@Entity()
export class Post {
  @PrimaryGeneratedColumn() id: number;
  @Column() title: string;
  @ManyToOne(() => User, (user) => user.posts) // 多 側(userId 列ができる)
  author: User;
}

2relations で関連先を一緒に読む

関連は既定では読み込まれず、find の relations オプションで明示したときだけ取得されます。必要なときだけ関連先を取りに行くという制御の基本を、ユーザーと投稿の例で示します。

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { User } from "./user.entity";

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepo: Repository<User>,
  ) {}

  findWithPosts(id: number) {
    // 既定では posts は読まれない。明示して初めて JOIN される
    return this.usersRepo.findOne({
      where: { id },
      relations: { posts: true },
    });
  }
}

3N+1 問題をクエリ回数で実感する

ユーザーごとに投稿を1件ずつ取りに行く素朴な実装が、なぜ件数に比例して遅くなるのかをクエリ回数のカウントで可視化した例です。1回の一覧取得のあとに N 回の個別取得が走る、という N+1 の正体が数字で見えます。

TypeScript

4関連をまとめて取って N+1 を解消する

N+1 を解消する基本対策を、著者を IN 句で一括取得する形でプレーン TypeScript に再現した例です。一覧1回 + 関連1回の計2クエリに収まり、件数が増えてもクエリ回数が一定に保たれることを数字で確認できます。

TypeScript

5ユーザーと注文の QueryBuilder JOIN

ユーザーと注文の1対多を QueryBuilder で JOIN し、関連先を1クエリでまとめて読む実務寄りの例です。leftJoinAndSelect を使うと N+1 を避けつつ、注文を持つユーザーだけ絞り込むといった条件も同じクエリに乗せられます。

import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { User } from "./user.entity";

@Injectable()
export class UsersService {
  constructor(
    @InjectRepository(User)
    private readonly usersRepo: Repository<User>,
  ) {}

  // ユーザーと注文を1クエリの JOIN でまとめて取得(N+1 を回避)
  findUsersWithOrders(minAmount: number) {
    return this.usersRepo
      .createQueryBuilder("user")
      .leftJoinAndSelect("user.orders", "order") // 関連を JOIN で同時取得
      .where("order.amount >= :minAmount", { minAmount })
      .orderBy("user.id", "ASC")
      .getMany();
  }
}