DEV Community

shreyas shinde
shreyas shinde

Posted on • Originally published at kanaeru.ai on

[🇯🇵] データベースアーキテクチャパターン:ドメインモデルからプロダクション対応リポジトリまで

堅牢でスケーラブルなデータベースアーキテクチャを構築するための体系的ガイド

はじめに

本番システムをレビューする際、私はデータ永続化層がアプリケーションアーキテクチャの基盤であり、同時に潜在的なボトルネックでもあることを一貫して観察しています。適切に設計されたデータ層と急ごしらえで構築されたデータ層の違いは、負荷がかかったとき、スキーマが進化するとき、または午前2時にトランザクションの異常をデバッグするときに明らかになります。

このガイドでは、ドメインモデルを本番環境対応のリポジトリ実装に変換するための実証済みのパターンをドキュメント化します。Repository パターン、データベースアーキテクチャへの CQRS の適用、ORM マッピング戦略、マイグレーションワークフロー、トランザクション処理、およびコネクションプール設定について検証します。すべて公式ドキュメントと実戦でテストされた実践に基づいています。

Diagram 1

Repository パターン: ドメインとデータの間を仲介する

パターンの定義と目的

Martin Fowler の Patterns of Enterprise Application Architecture における正規の定義によれば、Repository は「ドメインオブジェクトにアクセスするためのコレクションのようなインターフェースを使用して、ドメインとデータマッピング層の間を仲介する」ものです。 この抽象化は3つの重要な目的を果たします:

  1. 分離 : ドメインロジックは永続化メカニズムを認識しません
  2. テスタビリティ : Repository インターフェースは簡単にモック化できます
  3. 柔軟性 : 実装の詳細は消費者に影響を与えることなく進化できます

Repository パターンは ORM の直接使用とは根本的に異なります。ORM がエンティティレベルの CRUD 操作を提供するのに対し、Repository はビジネス意図を表現するドメイン中心のクエリメソッドを提供します。

TypeORM Repository の実装

TypeORM は Active Record と Data Mapper の両方のパターンをサポートしており、リポジトリは自然に Data Mapper アプローチに整合します。 各エンティティは独自のリポジトリを受け取り、そのエンティティタイプに固有の操作を処理します。

基本的な Repository 構造

// src/domain/entities/User.ts
import { Entity, PrimaryGeneratedColumn, Column, Index } from 'typeorm';

@Entity('users')
@Index(['email'], { unique: true })
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'varchar', length: 255 })
  email: string;

  @Column({ type: 'varchar', length: 255 })
  name: string;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;

  @Column({ type: 'timestamp', nullable: true })
  lastLoginAt: Date | null;

  @Column({ type: 'boolean', default: true })
  isActive: boolean;
}


// src/infrastructure/repositories/UserRepository.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from '../../domain/entities/User';

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

  async findByEmail(email: string): Promise<User | null> {
    return this.repository.findOne({
      where: { email }
    });
  }

  async findActiveUsers(): Promise<User[]> {
    return this.repository.find({
      where: { isActive: true },
      order: { createdAt: 'DESC' },
    });
  }

  async updateLastLogin(userId: string): Promise<void> {
    await this.repository.update(
      { id: userId },
      { lastLoginAt: new Date() }
    );
  }

  async save(user: User): Promise<User> {
    return this.repository.save(user);
  }

  async countActiveUsers(): Promise<number> {
    return this.repository.count({
      where: { isActive: true },
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

この実装はいくつかの重要な原則を示しています:

  • ドメイン固有のメソッド : findActiveUsers()updateLastLogin() はビジネス操作を表現します
  • 型安全性 : TypeScript はエンティティプロパティのコンパイル時検証を保証します
  • 関心の分離 : リポジトリはクエリロジックをドメインエンティティから分離してカプセル化します

TypeORM のリポジトリは基礎的なメソッド(find、save、update、delete)を提供し、カスタムリポジトリクラスはドメイン固有のクエリメソッドを追加します。 この二層アプローチは柔軟性と利便性のバランスを取ります。

CQRS: 読み取りと書き込みの責任を分離する

パターンの概要と適用性

Command Query Responsibility Segregation (CQRS) は、異なるモデルを使用して読み取り操作と書き込み操作を分離します。 この分離により、各ワークロードの独立した最適化が可能になります。これは、非対称な読み取り/書き込みパターンを持つシステムにおいて特に価値のある特性です。

Martin Fowler からの重要なガイダンス : 「CQRS はシステム全体ではなく、システムの特定の部分(DDD 用語では BoundedContext)にのみ使用すべきです。特に、CQRS がソフトウェアシステムを深刻な困難に陥れたケースに遭遇したことがあります。」

Diagram 2

データベースレベルの CQRS 実装

Microsoft Azure のアーキテクチャドキュメントは、CQRS データベース分離のいくつかのアプローチを概説しています:

  1. 読み取りレプリカを持つ単一データベース : PostgreSQL 読み取りレプリカがクエリを処理し、プライマリがコマンドを処理します
  2. 個別の論理データベース : 読み取りワークロードと書き込みワークロードに対する異なるスキーマ最適化
  3. 異種ストア : 書き込み用のリレーショナルデータベース、読み取り用のドキュメントストア

読み取りパターンが書き込みパターンと大きく異なる場合、3番目のアプローチは特に効果的であることが証明されています。e コマースシステムを考えてみましょう:

  • 書き込みモデル : 参照整合性を保証する正規化された PostgreSQL スキーマ
  • 読み取りモデル : 製品カタログクエリ用に最適化された非正規化 MongoDB ドキュメント

同期戦略

AWS Prescriptive Guidance は2つの主要な同期アプローチを特定しています:

同期(強い整合性) :

  • データベースレベルのレプリケーション(PostgreSQL ストリーミングレプリケーション)
  • 分散トランザクション内の二重書き込み
  • トレードオフ: 可用性の低下、書き込みレイテンシの増加

非同期(結果整合性) :

  • メッセージキュー経由のイベント駆動同期
  • Debezium などのツールを使用した Change Data Capture (CDC)
  • トレードオフ: 一時的な不整合ウィンドウ、複雑性の増加

ほとんどのアプリケーションでは、非同期同期による結果整合性が最適なバランスを提供します。主要な実装要件は、書き込みモデルからの堅牢なイベント発行です。

// src/application/commands/CreateOrderCommand.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EventBus } from '../events/EventBus';
import { Order } from '../../domain/entities/Order';
import { OrderCreatedEvent } from '../events/OrderCreatedEvent';

@Injectable()
export class CreateOrderCommandHandler {
  constructor(
    @InjectRepository(Order)
    private readonly orderRepository: Repository<Order>,
    private readonly eventBus: EventBus,
  ) {}

  async execute(command: CreateOrderCommand): Promise<void> {
    // 正規化されたコマンドデータベースに書き込む
    const order = this.orderRepository.create({
      userId: command.userId,
      items: command.items,
      totalAmount: command.totalAmount,
      status: 'pending',
    });

    await this.orderRepository.save(order);

    // 読み取りモデル同期のためにイベントを発行
    await this.eventBus.publish(
      new OrderCreatedEvent(order.id, order.userId, order.totalAmount)
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

EventBus は読み取りモデル更新ハンドラーへの非同期配信を処理し、クエリデータベースが注文データの非正規化ビューを維持できるようにします。

ORM マッピング戦略: 継承をテーブルに変換する

3つの主要な戦略

ドメインモデルが継承を利用する場合、ORM はクラス階層をリレーショナルスキーマにマッピングする必要があります。Hibernate、Doctrine、および SQLAlchemy の公式ドキュメントはすべて、3つの基本的な戦略を説明しています:

Diagram 3

1. Single Table Inheritance (STI)

階層内のすべてのクラスが、具体的な型を示す識別子列を持つ1つのテーブルにマッピングされます。

利点 :

  • 優れたクエリパフォーマンスを持つシンプルなスキーマ
  • ポリモーフィッククエリに結合が不要
  • 実装と理解が簡単

欠点 :

  • サブクラス固有のプロパティのためのスパース列(NULL 値)
  • テーブルの幅は階層の複雑さとともに増加
  • データ整合性の問題の可能性

2. Joined Table Inheritance (JTI)

基底クラスと各サブクラスが個別のテーブルを受け取ります。サブクラステーブルは基底テーブルへの外部キー参照を持ちます。

利点 :

  • 正規化されたスキーマで冗長性を最小化
  • 基底プロパティとサブクラスプロパティの明確な分離
  • 型安全なスキーマ強制

欠点 :

  • サブクラスクエリに結合が必要(パフォーマンスへの影響)
  • 保守がより複雑なスキーマ
  • 挿入操作が複数のテーブルにまたがる

3. Table-Per-Concrete-Class (TPC)

各具象クラスが、継承されたものを含むすべてのプロパティを含む独自のテーブルを受け取ります。

利点 :

  • 具象型クエリに結合が不要
  • 各テーブルがエンティティを完全に記述
  • 単一型クエリの良好なパフォーマンス

欠点 :

  • 非正規化スキーマが継承された列を複製
  • ポリモーフィッククエリに UNION 操作が必要
  • 基底クラスへのスキーマ変更がすべてのテーブルに波及

TypeORM 実装例

TypeORM は Single Table と Joined Table 戦略をサポートしています。以下は Joined Table の実装です:

// src/domain/entities/Content.ts
import { Entity, PrimaryGeneratedColumn, Column, TableInheritance } from 'typeorm';

@Entity()
@TableInheritance({ column: { type: 'varchar', name: 'type' } })
export abstract class Content {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'varchar', length: 500 })
  title: string;

  @Column({ type: 'text' })
  description: string;

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  createdAt: Date;
}

@Entity()
export class Article extends Content {
  @Column({ type: 'text' })
  body: string;

  @Column({ type: 'varchar', length: 255 })
  author: string;

  @Column({ type: 'int', default: 0 })
  readCount: number;
}

@Entity()
export class Video extends Content {
  @Column({ type: 'varchar', length: 500 })
  videoUrl: string;

  @Column({ type: 'int' })
  durationSeconds: number;

  @Column({ type: 'varchar', length: 100, nullable: true })
  resolution: string | null;
}

Enter fullscreen mode Exit fullscreen mode

この Joined Table アプローチは3つのテーブルを作成します:

  • content: 基底プロパティ(id、title、description、createdAt、type)
  • article: サブクラスプロパティ(body、author、readCount)と content への FK
  • video: サブクラスプロパティ(videoUrl、durationSeconds、resolution)と content への FK

識別子列 'type' は、正規化されたスキーマを維持しながらポリモーフィッククエリを可能にします。

マイグレーションのベストプラクティス: バージョン管理下でのスキーマの進化

なぜ同期よりもマイグレーションなのか

TypeORM の synchronize: true オプションは、エンティティ定義とデータベーススキーマを自動的に整合させます。これは開発に便利な機能です。しかし、公式 TypeORM ドキュメントが述べているように:「データベースにデータが入った後、本番環境でスキーマ同期に synchronize: true を使用することは安全ではありません。」

マイグレーションは、ロールバック機能を備えた、バージョン管理された監査可能なスキーマ変更を提供します。これは本番システムにとって不可欠な特性です。

マイグレーションワークフロー

2025年の NestJS と TypeORM マイグレーションガイドは、この体系的なワークフローをドキュメント化しています:

  1. エンティティ定義 : TypeORM エンティティを定義または変更
  2. マイグレーション生成 : npm run migration:generate -- src/migrations/AddUserLastLoginAt を実行
  3. 生成された SQL のレビュー : UP と DOWN マイグレーションメソッドを検証
  4. バージョン管理 : エンティティ変更と一緒にマイグレーションファイルをコミット
  5. デプロイメント : 新しいコードをデプロイする前にマイグレーションを実行

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

// src/migrations/1696875432123-AddUserLastLoginAt.ts
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddUserLastLoginAt1696875432123 implements MigrationInterface {
  name = 'AddUserLastLoginAt1696875432123';

  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      ALTER TABLE "users"
      ADD "last_login_at" TIMESTAMP
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      ALTER TABLE "users"
      DROP COLUMN "last_login_at"
    `);
  }
}

Enter fullscreen mode Exit fullscreen mode

マイグレーションでのトランザクション制御

TypeORM はマイグレーション用に3つのトランザクションモードを提供します:

  • デフォルト : すべてのマイグレーションが単一のトランザクションで実行されます(全か無かのデプロイメント)
  • --transaction each: 各マイグレーションが独自のトランザクションで実行されます(部分的なロールバックが可能)
  • --transaction none: トランザクションラッピングなし(CREATE INDEX CONCURRENTLY などの操作用)

PostgreSQL の CREATE INDEX CONCURRENTLY 操作はトランザクションブロック内では実行できないため、そのようなマイグレーションには --transaction none フラグが必要です。

マイグレーションの追跡と状態管理

TypeORM は、実行されたマイグレーションを記録する migrations テーブルをデータベースに維持します。 このテーブルは以下を保証します:

  • 冪等性 : マイグレーションは正確に1回実行されます
  • 順序 : マイグレーションは時系列順に実行されます
  • 整合性 : すべての環境が同一のスキーマに収束します

マイグレーションテーブルアプローチは、Flyway、Liquibase、およびほとんどのマイグレーションフレームワークで使用されており、環境全体で信頼性の高い状態追跡を提供します。

Diagram 4

トランザクション分離と ACID 保証

PostgreSQL の ACID 実装

PostgreSQL は ACID 準拠であり、すべてのトランザクションに対して Atomicity(原子性)、Consistency(一貫性)、Isolation(分離性)、および Durability(永続性)の保証を提供します。 これらのプロパティを理解することで、正しいトランザクションの使用が導かれます:

  • Atomicity(原子性) : トランザクションは全か無かの作業単位です
  • Consistency(一貫性) : データベース制約はトランザクション境界を超えて強制されます
  • Isolation(分離性) : 並行トランザクションは干渉しません(設定可能なレベル)
  • Durability(永続性) : コミットされたデータはシステム障害を通じて永続します(WAL 経由)

PostgreSQL は Write-Ahead Logging (WAL) を通じて永続性を実装しており、コミット確認が返る前にトランザクション記録がディスクに到達します。

分離レベルとそのトレードオフ

PostgreSQL 公式ドキュメントは4つの分離レベルを定義していますが、PostgreSQL は3つを実装しています:

Read Committed(デフォルト)

クエリはクエリが開始される前にコミットされたデータのみを参照します。このレベルはダーティリードを防ぎますが、反復不可能な読み取りとファントムリードを許可します。

ユースケース : ほとんどのアプリケーショントランザクションの汎用分離

Repeatable Read

クエリはトランザクション開始時からの一貫したスナップショットを参照します。このレベルはダーティリードと反復不可能な読み取りを防ぎますが、理論的にはファントムリードを許可します(ただし、PostgreSQL の実装はファントムも防ぎます)。

ユースケース : 複数のクエリにわたって一貫したデータを必要とするレポート

Serializable

最も厳密な分離で、トランザクションの連続実行をエミュレートします。すべての異常を防ぎますが、再試行ロジックを必要とする直列化失敗を引き起こす可能性があります。

ユースケース : 絶対的な整合性を必要とする金融トランザクション

TypeORM での実用的なトランザクション処理

// src/infrastructure/services/AccountService.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DataSource, Repository } from 'typeorm';
import { Account } from '../../domain/entities/Account';

@Injectable()
export class AccountService {
  constructor(
    @InjectRepository(Account)
    private readonly accountRepository: Repository<Account>,
    private readonly dataSource: DataSource,
  ) {}

  async transferFunds(
    fromAccountId: string,
    toAccountId: string,
    amount: number
  ): Promise<void> {
    await this.dataSource.transaction(
      'SERIALIZABLE', // 金融トランザクション用の分離レベル
      async (transactionalEntityManager) => {
        // SELECT FOR UPDATE で読み取ってロックを取得
        const fromAccount = await transactionalEntityManager.findOne(Account, {
          where: { id: fromAccountId },
          lock: { mode: 'pessimistic_write' },
        });

        const toAccount = await transactionalEntityManager.findOne(Account, {
          where: { id: toAccountId },
          lock: { mode: 'pessimistic_write' },
        });

        if (!fromAccount || !toAccount) {
          throw new Error('Account not found');
        }

        if (fromAccount.balance < amount) {
          throw new Error('Insufficient funds');
        }

        // 残高更新を実行
        fromAccount.balance -= amount;
        toAccount.balance += amount;

        await transactionalEntityManager.save(fromAccount);
        await transactionalEntityManager.save(toAccount);
      }
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

この実装は重要なトランザクションパターンを示しています:

  • 明示的な分離レベル : SERIALIZABLE は並行転送の異常を防ぎます
  • 悲観的ロック : SELECT FOR UPDATE は更新喪失を防ぎます
  • アトミック操作 : すべての変更が一緒にコミットまたはロールバックされます
  • ビジネス検証 : 残高不足チェックがトランザクション内で発生します

PostgreSQL の MVCC(Multi-Version Concurrency Control)システムにより、ほとんどの場合、読み取り側と書き込み側のブロッキングなしでこれらの分離レベルが可能になります。

コネクションプーリング: データベースアクセスのスケーリング

なぜコネクションプーリングが重要なのか

PostgreSQL のアーキテクチャは、各接続に対して新しいプロセスをフォークします。これは短いトランザクションにとって高コストな操作です。コネクションプーリングは、確立された接続を再利用することでこのコストを償却します。

Stack Overflow のエンジニアリングブログは次のように述べています:「コネクションプーリングは、すべてのクエリに対して新しい接続を確立するオーバーヘッドを削減し、データベース接続を再利用するために使用される技術です。」

プールサイジング: 数学的アプローチ

PostgreSQL コネクションプールのサイジングに関する権威ある公式は、PostgreSQL コミュニティから来ています:

connections = ((core_count × 2) + effective_spindle_count)

1つの SSD を持つ4コアのデータベースサーバーの場合:

  • (4 × 2) + 1 = 9 connections

この公式は、CPU 使用率とディスク I/O 容量のバランスを取ります。プールを大きく設定しすぎるとコンテキストスイッチングのオーバーヘッドが発生し、小さすぎるとキューイング遅延が発生します。

PgBouncer: 本番グレードのコネクションプーリング

PgBouncer は PostgreSQL の業界標準コネクションプーラーとして機能し、3つのプーリングモードを提供します:

Transaction Mode(推奨) :

  • トランザクション期間中に接続を割り当て
  • COMMIT/ROLLBACK 後にプールに接続を返す
  • 短いトランザクションの高い接続再利用を可能にします

Session Mode :

  • クライアントセッション期間中に接続を割り当て
  • アドバイザリロックとプリペアドステートメントに必要
  • 接続再利用が低く、データベース負荷が高い

Statement Mode :

  • ステートメントごとに接続を割り当て
  • 複数ステートメントトランザクションと互換性がない
  • 最高の再利用、最も多くの制限

PgBouncer 設定例

# /etc/pgbouncer/pgbouncer.ini

[databases]
production_db = host=localhost port=5432 dbname=production_db

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt

# 4コアデータベースサーバーに基づくプールサイジング
default_pool_size = 9
max_client_conn = 100
reserve_pool_size = 3
reserve_pool_timeout = 5

# 最適な再利用のためのトランザクションレベルプーリング
pool_mode = transaction

# 接続タイムアウト
server_idle_timeout = 600
server_lifetime = 3600
server_connect_timeout = 15

# ロギング
log_connections = 1
log_disconnections = 1
log_pooler_errors = 1

Enter fullscreen mode Exit fullscreen mode

主要なパラメータの説明 :

  • default_pool_size = 9: ユーザー/データベースペアごとの最大サーバー接続(公式に基づく)
  • max_client_conn = 100: 最大クライアント接続(キューイングを有効化)
  • reserve_pool_size = 3: リザーブプール用の追加接続
  • pool_mode = transaction: トランザクション完了後に接続を解放

単一 PgBouncer を超えたスケーリング

PgBouncer はシングルスレッドプロセスとして実行され、1つの CPU コアのみを使用します。高スループットシステムの場合、Crunchy Data は複数の PgBouncer インスタンスの実行をドキュメント化しています:

  • ロードバランサーの背後にある複数の PgBouncer プロセス
  • 各 PgBouncer インスタンスが独自のプールを持つ
  • 集合的なプールサイズは依然としてコアカウント公式に従う

複数の PgBouncer インスタンスが必要な兆候:

  • PostgreSQL が十分に利用されていない間に PgBouncer の CPU が 100% になる
  • データベースに余裕があるにもかかわらずアプリケーションクエリレイテンシが増加する

Diagram 5

統合: 本番対応データ層の構築

階層化アーキテクチャパターン

これらのパターンを組み合わせると、階層化されたアーキテクチャが生まれます:

  1. ドメイン層 : 純粋なビジネスエンティティとインターフェース
  2. リポジトリ層 : ドメイン中心のデータアクセス抽象化
  3. ORM 層 : TypeORM エンティティとマイグレーション
  4. 接続層 : PgBouncer プールとデータベースクラスター

各層は下の層にのみ依存し、独立したテストと進化を可能にします。

設定管理

本番システムには環境固有の設定が必要です:

// src/config/database.config.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import { DataSourceOptions } from 'typeorm';

export const getDatabaseConfig = (): TypeOrmModuleOptions => {
  const isProduction = process.env.NODE_ENV === 'production';

  return {
    type: 'postgres',
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT || '5432', 10),
    username: process.env.DB_USERNAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,

    // エンティティとマイグレーションのパス
    entities: ['dist/**/*.entity.js'],
    migrations: ['dist/migrations/*.js'],

    // 本番環境固有の設定
    synchronize: false, // 本番環境では絶対に使用しない
    migrationsRun: false, // CLI 経由でマイグレーションを明示的に実行
    logging: isProduction ? ['error', 'warn'] : true,

    // コネクションプール設定(アプリケーションレベル)
    extra: {
      max: 20, // アプリケーションプールサイズ
      idleTimeoutMillis: 30000,
      connectionTimeoutMillis: 10000,
    },

    // 本番環境用 SSL
    ssl: isProduction ? { rejectUnauthorized: false } : false,
  };
};

Enter fullscreen mode Exit fullscreen mode

この設定は多層防御を示しています:

  • 明示的なマイグレーション制御 : 自動スキーマ同期なし
  • コネクションプーリング : PgBouncer の前のアプリケーションレベルプール
  • 環境固有のロギング : 開発では詳細、本番ではエラー
  • SSL 強制 : 本番環境での暗号化接続

モニタリングと可観測性

本番データ層には複数のレベルでのモニタリングが必要です:

データベースレベル :

  • クエリパフォーマンス: pg_stat_statements 拡張
  • 接続数: pg_stat_activity ビュー
  • レプリケーションラグ: pg_stat_replication ビュー

コネクションプールレベル :

  • プール使用率: PgBouncer の SHOW POOLS コマンド
  • キュー深度: SHOW CLIENTS 出力
  • 接続待機時間: アプリケーションレベルのメトリクス

アプリケーションレベル :

  • Repository メソッドのレイテンシ
  • トランザクション期間のヒストグラム
  • 直列化失敗数(SERIALIZABLE 分離の場合)

PostgreSQL と PgBouncer 用の Prometheus エクスポーターが存在し、Grafana での包括的なダッシュボードを可能にします。

結論: 体系的なデータアーキテクチャ

本番対応のデータベースアーキテクチャを構築するには、ドキュメント化されたパターンの体系的な適用が必要です。Repository パターンはドメインロジックを永続化の懸念から分離します。CQRS はワークロード特性が異なる場合に独立した読み取り/書き込みの最適化を可能にします。ORM マッピング戦略は、理解されたトレードオフを持つオブジェクト階層をリレーショナルスキーマに変換します。マイグレーションはバージョン管理されたスキーマ進化を提供します。トランザクション分離レベルは整合性保証と並行性のバランスを取ります。コネクションプーリングはリソース枯渇なしでデータベースアクセスをスケールします。

各パターンは特定のアーキテクチャ上の懸念に対処します。公式ドキュメントと業界のベストプラクティスに導かれた組み合わせは、負荷下で整合性を維持し、要件とともにクリーンに進化し、実用的な運用メトリクスを表面化する堅牢なデータ層をもたらします。

私は、TypeORM に支えられたシンプルな Repository 実装から始め、読み取り/書き込みパターンが大幅に異なる場合にのみ CQRS を追加し、クエリパターンに基づいてマッピング戦略を選択し、プロジェクト開始からマイグレーション駆動のスキーマ変更を強制し、整合性要件に一致する分離レベルを選択し、データベースサーバーリソースに従ってコネクションプールをサイジングすることを推奨します。

これらのパターンはドキュメント化され、テストされ、実証されています。体系的に実装してください。

アーキテクチャの視覚化

これらの概念を強化するために、本番システムでこれらのパターンがどのように接続されるかを示します:

階層化アーキテクチャ: ドメインからデータベースまで

層間のクリーンな分離は保守性を保証します:

┌─────────────────────────────────┐
│ Domain Layer (Business Logic) │
│ - Entities with behavior │
│ - Value Objects │
│ - Domain Services │
└────────────┬────────────────────┘
             │ Repository Interface
┌────────────▼────────────────────┐
│ Data Adapter Layer │
│ - TypeORM Repositories │
│ - ORM Models (.model.ts) │
│ - Mapping Logic │
└────────────┬────────────────────┘
             │ TypeORM Connection
┌────────────▼────────────────────┐
│ PostgreSQL Database │
│ - Tables & Indexes │
│ - Constraints │
│ - Connection Pool │
└─────────────────────────────────┘

Enter fullscreen mode Exit fullscreen mode

各層には明確な責任があり、依存関係は一方向に流れます。

CQRS データフロー

CQRS を実装する場合、コマンドとクエリは個別のパスをたどります:

コマンドパス (書き込み): ユーザーリクエスト → コマンドハンドラー → 書き込みリポジトリ → マスター DB → イベント発行

クエリパス (読み取り): ユーザーリクエスト → クエリハンドラー → 読み取りリポジトリ → 読み取りレプリカ → レスポンス

この分離により、読み取りと書き込み操作の独立した最適化が可能になり、書き込みの整合性を維持しながら読み取りレプリカを水平にスケールできます。


参考文献

: [1] Fowler, M. (2002). "Repository." Patterns of Enterprise Application Architecture. Retrieved from https://martinfowler.com/eaaCatalog/repository.html

: [2] TypeORM. (2024). "Working with Repository." TypeORM Documentation. Retrieved from https://typeorm.io/docs/working-with-entity-manager/working-with-repository/

: [3] TypeORM. (2024). "Repository APIs." TypeORM Documentation. Retrieved from https://typeorm.io/docs/working-with-entity-manager/repository-api/

: [4] Microsoft. (2024). "CQRS Pattern." Azure Architecture Center. Retrieved from https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs

: [5] Fowler, M. (2011). "CQRS." Martin Fowler's Blog. Retrieved from https://martinfowler.com/bliki/CQRS.html

: [6] AWS. (2024). "CQRS Pattern." AWS Prescriptive Guidance. Retrieved from https://docs.aws.amazon.com/prescriptive-guidance/latest/modernization-data-persistence/cqrs-pattern.html

: [7] Doctrine Project. (2024). "Inheritance Mapping." Doctrine ORM Documentation. Retrieved from https://www.doctrine-project.org/projects/doctrine-orm/en/3.5/reference/inheritance-mapping.html

: [8] SQLAlchemy. (2024). "Mapping Class Inheritance Hierarchies." SQLAlchemy 2.0 Documentation. Retrieved from https://docs.sqlalchemy.org/en/20/orm/inheritance.html

: [9] TypeORM. (2024). "Migrations." TypeORM Documentation. Retrieved from https://typeorm.io/docs/advanced-topics/migrations/

: [10] Gunawardena, B. (2025). "NestJS & TypeORM Migrations in 2025." JavaScript in Plain English. Retrieved from https://javascript.plainenglish.io/nestjs-typeorm-migrations-in-2025-50214275ec8d

: [11] Aviator. (2024). "ACID Transactions and Implementation in a PostgreSQL Database." Retrieved from https://www.aviator.co/blog/acid-transactions-postgresql-database/

: [12] PostgreSQL Global Development Group. (2024). "Transaction Isolation." PostgreSQL 18 Documentation. Retrieved from https://www.postgresql.org/docs/current/transaction-iso.html

: [13] ScaleGrid. (2024). "PostgreSQL Connection Pooling: Part 1 - Pros & Cons." Retrieved from https://scalegrid.io/blog/postgresql-connection-pooling-part-1-pros-and-cons/

: [14] Stack Overflow. (2020). "Improve Database Performance with Connection Pooling." Stack Overflow Blog. Retrieved from https://stackoverflow.blog/2020/10/14/improve-database-performance-with-connection-pooling/

: [15] ScaleGrid. (2024). "PostgreSQL Connection Pooling: Part 2 - PgBouncer." Retrieved from https://scalegrid.io/blog/postgresql-connection-pooling-part-2-pgbouncer/

: [16] Crunchy Data. (2024). "Postgres at Scale: Running Multiple PgBouncers." Crunchy Data Blog. Retrieved from https://www.crunchydata.com/blog/postgres-at-scale-running-multiple-pgbouncers


Originally published at kanaeru.ai

Top comments (0)