⚙️

TypeORM 0.3.10→0.3.20で大幅改善!relationLoadStrategy: query の問題が解決

2025/02/20に公開

最初に

この記事では、TypeORMのバージョンを0.3.10から0.3.20にアップデートした際に発生した主な変化について解説します。

2025年2月現在、TypeORMの最新バージョンは0.3.20であり、2024年1月以降アップデートは行われていないようです。

バージョンを上げたことで起きた大きな変化

0.3.20のリリースノートで注目すべきポイントとして、以下の内容が挙げられます。

hangup when load relations with relationLoadStrategy: query

リレーションをrelationLoadStrategy: queryでロードすると、ハングアップ(フリーズ)する可能性がある。

ハングアップの原因

relationLoadStrategy: queryでは、リレーションごとに個別のSQLクエリが発行される仕組みです。このため、リレーションの数が増えるとクエリ数が膨大になり、データベース接続プールが枯渇することで性能問題やハングアップを引き起こします。

0.3.20ではこの問題が解消され、性能が大幅に向上しています。

relationLoadStrategyとは?

TypeORMのバージョン0.3.0から導入された設定で、リレーションのロード方法を指定できます。主に以下の2つがあります。

1. relationLoadStrategy: query

  • 仕組み:各リレーションについて個別にSQLクエリを発行。
  • 特徴:シンプルなリレーション構造では効率的。リレーションが複雑になるとSQLの発行数が増え、性能問題を引き起こしやすい。

2. relationLoadStrategy: join

  • 仕組み:JOINを使用してリレーションを1つのSQLクエリで取得。
  • 特徴:クエリ発行数を抑えることが可能。リレーションが深い場合やデータ量が多い場合、JOIN結果が大きくなりパフォーマンスに影響することもある。

検証してみた

実際にrelationLoadStrategyを設定して発行されるSQLを検証してみました。

relationLoadStrategy: queryの場合

UserとSchoolが1対多(OneToMany)のリレーションで、以下のようなクエリを実行します。

userRepository.find({
  where: { email: email },
  relations: {
    school: true,
  },
  relationLoadStrategy: "query"
});

発行されるSQLは以下の通りです。

-- Userを検索
SELECT
  "User"."id",
  "User"."first_name",
  "User"."last_name",
  "User"."school_id"
FROM
  "users" "User"
WHERE
  "User"."email" = $1;

-- Schoolを取得
SELECT
  "School"."id" AS "School_id",
  "School"."name" AS "School_name",
  "School"."created_at" AS "School_created_at",
  "School"."updated_at" AS "School_updated_at"
FROM
  "schools" "School"
   INNER JOIN "users" "User" ON "User"."school_id" = "School"."id"
   AND "User"."deleted_at" IS NULL
WHERE
  "User"."id" IN ($1) -- PARAMETERS: ["20c12a52-a64b-018f-8809-55314b80ff0f"]

  • クエリ数: 2つのSQLが発行されています。リレーションごとに個別のクエリが発行されるため、リレーションが多いほどクエリ数が増えます。そのため、relationsで指定したテーブルの数だけクエリが発行されるということです。

relationLoadStrategy: joinの場合

同じ条件でrelationLoadStrategy: joinを設定すると、以下のようなクエリが発行されます。

    userRepository.find({
      where: { email: email },
      relations: {
        school: true,
      },
      relationLoadStrategy: "join"
    });
    SELECT
      "User"."id",
      "User"."first_name",
      "User"."last_name",
      "School"."id" AS "School_id",
      "School"."name" AS "School_name"
    FROM
      "users" "User"
    LEFT JOIN "schools" "School"
      ON "School"."id" = "User"."school_id"
    WHERE
      "User"."email" = $1;
    
  • クエリ数: JOINによってリレーションを結合し、SQLクエリを1本にまとめます。

結果

設定 クエリ数 特徴
relationLoadStrategy:query リレーションごとに1本 クエリ数が多くなるため、複雑なリレーションでは非効率
relationLoadStrategy:join 1本 クエリ数は1本になるが、結果セットが大きくなると負荷が増加する可能性。

ハングアップの原因を検証

実際に、リレーションを多く持つクエリを実行して、接続プールの動作を確認しました。

async testConnectionPool(email: string): Promise<any> {
  const sql = async () =>
    await this.postgresDataSource.getRepository(User).find({
      where: {
        id: "XXXXXXXXXXX"
      },
      relations: {
        school: {
          departments: {
            classes: {
              students: true
            },
          },
          affiliatedOrganizations: {
            organizationDetails: true
          },
        },
        projects: {
          tasks: {
            taskAssignees: true,
            taskCategories: {
              categoryDetails: true
            },
          },
        },
        resourceGroup: {
          resources: {
            resourceAttributes: true,
            resourceTags: {
              tagDetails: true
            },
          },
          accessPermissions: {
            permissionDetails: true
          },
        },
        relationLoadStrategy: "query"
      },
    });

  const log = async () => {
    await new Promise((resolve) => {
      let count = 0;
      const interval = setInterval(() => {
        const pool = (this.postgresDataSource.driver as any).master as Pool;

        console.log("Total Clients:", pool.totalCount);
        console.log("Idle Clients:", pool.idleCount);
        console.log("Waiting Requests:", pool.waitingCount);

        count++;
        if (count >= 10) {
          clearInterval(interval);
          resolve("Completed");
        }
      }, 100);
    });
  };

  await Promise.all([log(), sql()]);
}

  • TypeORMのバージョン0.3.10の場合
Total Clients: 19
Idle Clients: 19
Waiting Requests: 0

クライアント数が多く、接続プールが逼迫する可能性。

  • TypeORMのバージョン0.3.20の場合
Total Clients: 3
Idle Clients: 3
Waiting Requests: 0

バージョンアップにより接続プールの負荷が軽減され、効率的に動作。

結論

  • TypeORM 0.3.10の課題
    relationLoadStrategy: queryでリレーションを多用すると、SQL発行数が増加し、接続プールが枯渇する恐れがあります。
  • TypeORM 0.3.20の改善
    relationLoadStrategy: queryの性能が向上し、接続プールの負荷が軽減されました。

補足

複雑なリレーションを使用している場合は、TypeORM 0.3.20以降へのアップデートを推奨します。ただし、正規化は進んでおり、リレーションがそこまで深くないシステムの場合、そもそも影響が小さいケースもあります。

  • 影響を受けやすいケース:リレーションのネストが深い・取得するテーブル数が多い
  • 影響が小さいケース:リレーションが単純・取得データが少ない

また、リレーションの数が多い場合は、relationLoadStrategy: joinの利用を検討することでクエリ数を抑え、パフォーマンスをさらに向上させることが可能です。

最後に

最後まで、目を通していただきありがとうございました!アスエネでは、このように自分の強みを活かしながら、自分の目標目掛けてチャレンジできる魅力的な環境があります。全方位で採用強化中なので、ご興味がある方は、ぜひこちらの採用サイトからご連絡ください!

(SNSからカジュアルにご連絡いただくのも大歓迎です!)

https://corp.asuene.com/recruit

Discussion