NestJSとTypeORM 2026年版:マイグレーション、リレーション、面接質問

NestJS TypeORM統合をマスターする。TypeORM 1.1マイグレーション、エンティティリレーション、リポジトリパターン、バックエンド開発者向けの面接質問を解説します。

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

NestJS TypeORMは、エンタープライズNode.jsバックエンドにおいて最も実績のあるORM統合の一つです。TypeORMが2026年5月にバージョン1.0に到達し、続く1.1.xパッチシリーズでコネクション処理とリレーション永続化が改善され、NestJS 12がESM移行を完了したことで、このスタックは本番アプリケーションの安定した基盤として成熟しました。本ガイドでは、バックエンド開発者が習得すべき重要なパターンを解説します。マイグレーション、エンティティリレーション、リポジトリパターン、そして面接で問われる質問です。

NestJS 12とTypeORM 1.1

NestJS 12はコアパッケージをESMで提供し、Node.js 20.19以上または22.12以上が必要です。@nestjs/typeorm v12パッケージも同様です。TypeORM 1.1.2ではDataSource.initialize()のコネクションリークが修正され、リレーションがロードされていない場合のManyToMany中間テーブル行の永続化が改善されています。

NestJS 12でのTypeORMマイグレーション設定

本番データベースの第一原則は、synchronize: trueを使用しないことです。自動同期はエンティティのメタデータをライブスキーマと比較して直接変更を適用するため、カラム削除、型変更、フィールド名変更時にデータ損失のリスクがあります。マイグレーションは、バージョン管理され、レビュー可能で、元に戻すことができる代替手段を提供します。

TypeORM 1.xでは、CLIのためにNestJSモジュール設定とは別の専用DataSource設定ファイルが必要です。CLIはNestJSの依存性注入を解決できないため、スタンドアロンのエントリポイントが必要です。

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,
});

NestJSモジュール設定はこれらの設定を反映しますが、依存性注入を利用したTypeOrmModule.forRootAsyncパターンを使用します。

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 {}

autoLoadEntities: trueは、手動のentities配列を置き換え、フィーチャーモジュールでTypeOrmModule.forFeature()を通じてインポートされたすべてのエンティティを自動的に登録します。

マイグレーションワークフロー:生成、レビュー、実行

TypeORM 1.xは推奨CLIランナーとしてtypeorm-ts-node-commonjsを提供し、従来のts-node ./node_modules/typeorm/cliアプローチを置き換えます。以下のスクリプトをpackage.jsonに追加します。

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"
}

generateコマンドはエンティティのメタデータをライブデータベースと比較し、SQL文を生成します。コミット前に必ず出力を確認してください。TypeORMはカラムの追加、削除、型変更、インデックス変更、外部キー変更を検出しますが、データのバックフィル、テーブル名変更(削除後に作成として表示される)、データベース拡張やトリガーに関連する操作は検出しません。

生成されたマイグレーションを必ず確認

カラム名の変更は、ALTER RENAMEではなくDROP + ADDとして生成されます。これは既存データを破壊します。up()メソッドのSQLを必ず確認し、必要に応じて手動で調整してください。

エンティティリレーション:OneToMany、ManyToOne、ManyToMany

TypeORMのエンティティリレーションは、デコレータを使用してSQLの外部キーに直接マッピングされます。3つのコアリレーションタイプとそのオプションを理解することは、NestJSデータベースモジュールの基本です。

複数のOrderレコードを持つUserは、最も一般的なパターンを示します。

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;
}

@ManyToOne側が外部キーを所有します。@JoinColumnは@ManyToOneではオプション(TypeORMが推測する)ですが、明示的な命名により、生SQLクエリやマイグレーション差分でカラム名が重要な場合の予期しない動作を回避できます。

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はリレーションの片側にのみ配置する必要があります。これはどのエンティティが中間テーブルを所有するかを定義し、テーブル名を制御します。

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

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

リポジトリパターンとカスタムリポジトリ

NestJSモジュールとDIシステムは、TypeOrmModule.forFeature()を通じてTypeORMリポジトリを統合します。デフォルトのRepository<T>を注入すると、標準的なCRUD操作をカバーできます。クエリが複雑になった場合、カスタムリポジトリがそのロジックをカプセル化します。

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 {}
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();
  }
}

find() APIは単純なクエリを処理します。QueryBuilderは結合、サブクエリ、集約、およびきめ細かなSQL制御が必要な場合を処理します。同じサービス内で両方を混在させることは問題ありません。単純な検索にはfind()を使用し、オプションAPIで表現できないクエリにはQueryBuilderを使用します。

データ整合性のためのトランザクション管理

複数のテーブルを変更する操作にはトランザクションが必要です。TypeORMは2つのアプローチを提供します。DataSource.transaction()メソッドとQueryRunner APIです。

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);
  });
}

コールバックベースのtransaction()メソッドは、成功時に自動的にコミットし、エラーがスローされるとロールバックします。コールバック内のすべてのデータベース操作は、注入されたリポジトリではなく、提供されたmanagerを使用する必要があります。そうでなければ、それらの操作はトランザクション外で実行されます。

QueryRunnerとtransaction()の使い分け

単純な複数ステップの操作にはDataSource.transaction()を使用します。コミット/ロールバックポイントやセーブポイントの手動制御が必要な場合(バッチ処理での部分的なロールバックなど)はQueryRunnerに切り替えます。

NestJS TypeORMの一般的な面接質問

これらの質問はバックエンド開発者の面接で頻繁に出題されます。各回答はシニアレベルの技術的議論で期待される深さを目指しています。

本番環境でsynchronize: trueを使用してはいけない理由は?

自動同期は確認なしに破壊的なスキーマ変更を適用します。カラムの削除、型の変更、インデックスの削除がライブデータに対して即座に実行されます。マイグレーションはバージョン管理、コードレビュー、ロールバック機能を提供します。synchronizeの唯一の安全な使用は、破棄可能なデータを使用するローカル開発です。

eagerリレーションとlazyリレーションの違いは?

Eagerリレーションは親エンティティへのすべてのクエリで自動的にロードされます。LazyリレーションはPromiseを返し、アクセス時にのみ別のクエリを実行します。関連データが不要な場合、Eagerロードは N+1 スタイルのオーバーヘッドを引き起こします。Lazyロードはコストを遅延させますが、コールスタックの深い場所で予期しないクエリをトリガーする可能性があります。find()の明示的なrelationsオプションまたはQueryBuilderのleftJoinAndSelectにより、クエリごとに何をロードするかを完全に制御できます。

TypeORMリレーションのcascadeはどのように機能するか?

リレーションにcascade: trueを設定すると、親エンティティを保存する際に未保存の子エンティティも永続化されます。cascade: ['insert', 'update']は動作を特定の操作に限定します。カスケードはORMレベルで動作し、データベースレベルではありません。データベースレベルのカスケード削除には、@ManyToOneデコレータでonDelete: 'CASCADE'を使用し、マイグレーションでON DELETE CASCADE制約を生成します。

デフォルトのRepository<T>ではなくカスタムリポジトリを使用すべき場合は?

カスタムリポジトリは、サービスに属さない複雑なクエリロジックをカプセル化します。カスタムリポジトリを抽出すべきサイン:同じQueryBuilderチェーンが複数のサービスに出現する、クエリが複数の結合やサブクエリを含む、クエリが生SQLを必要とする。単純なfind()呼び出しはカスタムリポジトリを正当化しません。

NestJSアーキテクチャに関するその他の質問については、NestJSガードとインターセプターの完全ガイドをご覧ください。

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

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

ソース

NestJS TypeORM本番アプリケーションの重要ポイント

  • TypeORM CLIには、NestJSモジュール設定とは独立した別のDataSource設定ファイルを使用する
  • 本番環境ではsynchronizeを有効にしない。すべてのスキーマ変更には生成およびレビュー済みのマイグレーションを使用する
  • 外部キーを所有するエンティティに@ManyToOneを配置し、生SQLクエリとマイグレーション出力の明確性のために明示的な@JoinColumn命名を使用する
  • 単純なクエリにはfind()オプションAPI、複雑な結合、集約、条件付きロジックにはQueryBuilderを選択する
  • 複数テーブルの変更はDataSource.transaction()を使用してトランザクションでラップし、コールバック内のすべての操作がトランザクショナルmanagerを使用することを確認する
  • TypeORM 1.1.xがAPIサーフェスを安定させ、NestJS 12がESM移行を完了したことで、このスタックは構造化されたタイプセーフなデータベースアクセスを必要とするエンタープライズバックエンドに強力な選択肢として残っている

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

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

今日のチャレンジ

Node.js / NestJS のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年10月21日 更新

タグ

#nestjs
#typeorm
#migrations
#database
#typescript
#backend

共有

関連記事