NestJSとTypeORM 2026年版:マイグレーション、リレーション、面接対策の完全ガイド

NestJSとTypeORMを組み合わせたバックエンド開発の実践的ガイド。マイグレーション管理、リレーション設計、トランザクション処理、そして技術面接で頻出する質問と回答を解説します。

NestJSとTypeORMのマイグレーションとリレーションのアーキテクチャ図

2026年現在、NestJSとTypeORMはNode.jsバックエンド開発において最も信頼性の高い組み合わせの一つとして広く採用されています。NestJSはモジュール設計とDependency Injectionによるスケーラブルなアーキテクチャを提供し、TypeORMはTypeScriptの型安全性を活かしたデータベース操作を実現します。しかし、本番環境でのマイグレーション運用、複雑なリレーション設計、トランザクション管理など、実務で直面する課題は少なくありません。

本記事では、TypeORMのマイグレーション戦略、主要なリレーションパターン、トランザクション処理の実装方法を具体的なコードとともに解説します。さらに、技術面接で頻繁に問われるNestJSとTypeORMに関する質問と、その模範回答も取り上げます。

TypeORM DataSource API について

TypeORMでは従来のormconfig.jsoncreateConnectionに代わり、DataSource APIが標準となっています。本記事のすべてのコード例は、現行のDataSource APIに基づいています。レガシーな設定方法を使用しているプロジェクトは、移行を検討してください。

NestJSにおけるTypeORMの設定

最初のステップとして、TypeORM CLIが使用するDataSource設定ファイルを作成します。この設定ファイルをNestJSのアプリケーションコンテキストから分離することで、CLIツールが独立してマイグレーション操作を実行できるようになります。

src/config/typeorm.config.tstypescript
import { DataSource } from 'typeorm';
import { config } from 'dotenv';

config(); // Load .env variables

export default new DataSource({
  type: 'postgres',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT || '5432'),
  username: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
  entities: ['src/**/*.entity.ts'],
  migrations: ['src/migrations/*.ts'],
  synchronize: false,
});

synchronize: falseの設定は非常に重要です。開発環境ではsynchronize: trueに設定してスキーマを自動同期させたくなりますが、本番環境でこれを有効にすると、カラムの削除やデータ型の変更が予告なく実行され、データ損失につながる可能性があります。本番環境では必ずマイグレーションを使用します。

AppModuleでは、ConfigServiceを通じて環境変数を注入し、TypeORMを非同期で設定します。

src/app.module.tstypescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (config: ConfigService) => ({
        type: 'postgres',
        host: config.get('DB_HOST'),
        port: config.getOrThrow<number>('DB_PORT'),
        username: config.get('DB_USER'),
        password: config.get('DB_PASSWORD'),
        database: config.get('DB_NAME'),
        autoLoadEntities: true, // Auto-register entities from feature modules
        synchronize: false,     // Always false in production
      }),
    }),
  ],
})
export class AppModule {}

config.get()config.getOrThrow()の違いに注目してください。getOrThrow()は環境変数が未定義の場合に例外をスローします。データベースポートのようなクリティカルな設定値に対してこのメソッドを使用することで、設定ミスをアプリケーション起動時に即座に検出できます。

マイグレーション:生成、実行、ロールバック

マイグレーションは、プロフェッショナルなデータベース管理の根幹です。スキーマの変更履歴をバージョン管理し、再現可能な形で適用・取り消しができます。以下のnpmスクリプトをpackage.jsonに定義すると、TypeORM CLIの操作が簡潔になります。

package.json (scripts section)json
{
  "migration:generate": "typeorm-ts-node-commonjs migration:generate src/migrations/$npm_config_name -d src/config/typeorm.config.ts",
  "migration:run": "typeorm-ts-node-commonjs migration:run -d src/config/typeorm.config.ts",
  "migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/config/typeorm.config.ts"
}

実際のワークフローは次のとおりです。まずEntityを変更し、npm run migration:generate --name=AddUserTableを実行すると、現在のデータベーススキーマとEntity定義の差分を検出してマイグレーションファイルが自動生成されます。npm run migration:runでマイグレーションを適用し、npm run migration:revertで直近のマイグレーションを元に戻すことができます。

プロジェクトでよく見られる誤りは、migration:generateが自動で差分を検出してくれるにもかかわらず、マイグレーションを手動で記述してしまうことです。手動マイグレーションが必要になるのは、カラム名の変更に伴うデータ移行のように、データの変換処理が含まれる場合に限られます。

生成されたマイグレーションの確認

カラム名の変更は、ALTER RENAMEではなくDROP + ADDとして生成される場合があります。これにより既存データが失われます。up()メソッド内のSQL文は必ず確認し、必要に応じて手動で修正してください。

Entityとリレーションの設計

TypeORMはデコレータを使用して、TypeScriptクラスとデータベース構造をマッピングします。主要なリレーションタイプは、OneToMany/ManyToOne、ManyToMany、OneToOneの3種類です。

OneToManyとManyToOne

ユーザーと注文の関係は、@OneToMany@ManyToOneの典型的な使用例です。

src/entities/user.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, OneToMany, CreateDateColumn } from 'typeorm';
import { Order } from './order.entity';

@Entity('users')
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ unique: true })
  email: string;

  @Column()
  name: string;

  @OneToMany(() => Order, (order) => order.user)
  orders: Order[];  // No DB column created here; relation lives on the Order side

  @CreateDateColumn()
  createdAt: Date;
}
src/entities/order.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';

@Entity('orders')
export class Order {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column('decimal', { precision: 10, scale: 2 })
  total: number;

  @Column({ default: 'pending' })
  status: string;

  @ManyToOne(() => User, (user) => user.orders, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })  // Explicit FK column name
  user: User;
}

ここで重要なのは、@OneToMany側はデータベースにカラムを生成しないという点です。外部キーは常に@ManyToOne側に配置されます。@JoinColumnデコレータで外部キーのカラム名を明示的に指定することで、データベースの可読性が向上し、命名の衝突を回避できます。onDelete: 'CASCADE'オプションにより、ユーザーが削除された際に関連する注文も自動的に削除されます。

ManyToMany

商品とカテゴリの関係には、ManyToManyリレーションが適しています。TypeORMは中間テーブルを自動的に生成します。

src/entities/product.entity.tstypescript
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable } from 'typeorm';
import { Category } from './category.entity';

@Entity('products')
export class Product {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;

  @Column('decimal', { precision: 10, scale: 2 })
  price: number;

  @ManyToMany(() => Category, (category) => category.products)
  @JoinTable({ name: 'product_categories' })  // Owns the junction table
  categories: Category[];
}

@JoinTableデコレータは、リレーションの所有側を示します。このデコレータを配置できるのは一方の側だけです。中間テーブルに並び順などの追加カラムが必要な場合は、別途Entityを作成し、2つの@ManyToOneリレーションで構成する必要があります。

Node.js / NestJSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Repositoryパターンとフィーチャーモジュール

NestJSはフィーチャーモジュールによるモジュール化を推進しています。各モジュールはTypeOrmModule.forFeature()でEntityを登録し、対応するRepositoryへのアクセスを得ます。

src/orders/orders.module.tstypescript
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from '../entities/order.entity';
import { OrdersService } from './orders.service';
import { OrdersController } from './orders.controller';

@Module({
  imports: [TypeOrmModule.forFeature([Order])],
  providers: [OrdersService],
  controllers: [OrdersController],
})
export class OrdersModule {}

Service内では、@InjectRepositoryによりRepositoryが注入されます。単純なクエリにはRepository APIが適しており、複雑なクエリにはQueryBuilderが使用されます。

src/orders/orders.service.tstypescript
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Order } from '../entities/order.entity';

@Injectable()
export class OrdersService {
  constructor(
    @InjectRepository(Order)
    private readonly orderRepo: Repository<Order>,
  ) {}

  async findByUser(userId: string): Promise<Order[]> {
    return this.orderRepo.find({
      where: { user: { id: userId } },
      relations: ['user'],          // Eager-load the user relation
      order: { createdAt: 'DESC' }, // Most recent first
    });
  }

  async findWithFilters(status: string, minTotal: number): Promise<Order[]> {
    // QueryBuilder for complex queries
    return this.orderRepo
      .createQueryBuilder('order')
      .leftJoinAndSelect('order.user', 'user')
      .where('order.status = :status', { status })
      .andWhere('order.total >= :minTotal', { minTotal })
      .orderBy('order.total', 'DESC')
      .getMany();
  }
}

findByUserメソッドでは、ネストされたWhere条件でリレーションをフィルタリングする方法を示しています。relations配列により、どのリレーションをロードするかを制御します。この指定がなければ、TypeORMは注文データのみを返し、ユーザー情報は含まれません。

findWithFiltersのQueryBuilderは、より細かな制御を提供します。leftJoinAndSelectはリレーションをロードして結果に含め、パラメータ化されたクエリによりSQLインジェクションを確実に防止します。

トランザクションの適切な実装

複数のデータベース操作をアトミックに実行する必要がある場合、トランザクションは不可欠です。TypeORMはDataSource.transaction()メソッドを提供しています。

src/orders/orders.service.ts (additional method)typescript
async createOrderWithItems(
  userId: string,
  items: { productId: string; quantity: number }[],
  dataSource: DataSource,
): Promise<Order> {
  return dataSource.transaction(async (manager) => {
    // All operations use the transactional manager
    const order = manager.create(Order, {
      user: { id: userId },
      total: 0,
      status: 'pending',
    });
    const savedOrder = await manager.save(order);

    let total = 0;
    for (const item of items) {
      const product = await manager.findOneByOrFail(Product, { id: item.productId });
      total += product.price * item.quantity;

      await manager.save(OrderItem, {
        order: savedOrder,
        product,
        quantity: item.quantity,
        unitPrice: product.price,
      });
    }

    savedOrder.total = total;
    return manager.save(savedOrder);
  });
}

トランザクション内では、すべての操作にmanagerを使用することが必須です。通常のRepositoryを使用した場合、その操作はトランザクションの外で実行されるため、後続のステップでエラーが発生してもロールバックされず、データの不整合が発生します。コールバック内でエラーが発生した場合、トランザクション全体が自動的にロールバックされます。

QueryRunner と transaction() の使い分け

DataSource.transaction()は、シンプルな複数ステップの操作に適しています。QueryRunnerは、コミット・ロールバックのタイミングやセーブポイントを手動で制御でき、バッチ処理における部分的なロールバックなど、より高度な制御が必要な場合に使用します。

NestJSとTypeORMの技術面接でよく問われる質問

技術面接でNestJSとTypeORMに関して問われる質問は、アーキテクチャの理解、実践的な問題解決能力、そしてよくある落とし穴の認識という3つの領域に分類されます。

本番環境でsynchronizeを無効にすべき理由は何ですか?

synchronize: trueを設定すると、アプリケーション起動時にEntity定義とデータベーススキーマが自動的に同期されます。この過程でカラムの削除、データ型の変更、テーブルの削除が記録なく実行される可能性があり、本番環境ではデータ損失のリスクがあります。マイグレーションを使用することで、スキーマ変更がバージョン管理され、再現可能かつ元に戻すことが可能になります。

migration:generatemigration:createの違いは何ですか?

migration:generateは、現在のEntity定義とデータベーススキーマを比較し、差分を検出して必要なSQL文を自動生成します。一方、migration:createは空のマイグレーションファイルを作成し、SQL文を手動で記述します。後者は、データ移行やシードデータの投入など、スキーマ変更以外の操作に使用されます。

OneToMany/ManyToOneリレーションで外部キーはどちら側に配置されますか?

外部キーは常に@ManyToOne側に配置されます。@OneToMany側はデータベースにカラムを生成せず、TypeScript上で逆方向の参照を表現するためだけに使用されます。

Repository APIとQueryBuilderはどのように使い分けますか?

Repository API(findfindOnesave)は、単純なCRUD操作や直接的なフィルタ条件によるクエリに適しています。QueryBuilderは、複雑なJOIN、サブクエリ、集約関数、動的に構築されるクエリが必要な場合に使用されます。

モジュール間の循環依存はどのように解決しますか?

NestJSはforwardRef()を提供して循環依存を解決できます。ただし、この関数の使用はアーキテクチャ上の問題を示唆していることが多いです。共通のロジックを独立したモジュールに分離する方が望ましいアプローチです。

トランザクション内で通常のRepositoryを使用した場合、何が起きますか?

通常のRepositoryを通じた操作はトランザクションの外で実行されます。つまり、それらの変更は即座にコミットされ、エラー発生時にもロールバックされません。これにより、データの不整合が発生する原因となります。

Node.js / NestJSの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

まとめ

NestJSとTypeORMの組み合わせは、型安全なNode.jsバックエンド開発のための成熟したエコシステムを提供します。本記事の要点を整理します。

  • マイグレーションは、ローカル開発を除くすべての環境でsynchronizeを置き換えるべきものです
  • リレーションの設計では、どちら側が外部キーを保持するかを正確に理解することが不可欠です
  • トランザクションでは、通常のRepositoryではなく、必ずManagerを通じて操作を実行する必要があります
  • QueryBuilderは、Repository APIでは対応できない複雑なクエリに対する補完的なツールです
  • 技術面接では、構文の知識だけでなく、アーキテクチャ上の判断の根拠を説明できることが求められます

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

共有

関連記事