DDDにおける領域モデルの永続化とORM対応戦略

領域モデルの永続化と技術的隔離

ドメイン駆動設計(DDD)では、業務の複雑さを表現するために領域モデルの構築が最優先されます。この設計思想では、ビジネスロジックと技術的な複雑さを層で分離し、ドメイン層ではデータベーススキーマや永続化機構を意識しすぎないようにすることが求められます。リポジトリパターンの導入は、まさにこの課題を解決するための抽象化レイヤーです。DDDの推進力となるのはデータベースの構造ではなくドメインロジックそのものであるため、まずは純粋な領域モデルを定義し、その後に永続化用データモデルを派生させるアプローチが採用されます。これがデータモデル先行型の設計とDDDを区別する根本的な違いです。

オブジェクトとリレーショナルDBのインピーダンスミスマッチ

領域モデルはオブジェクト指向言語で構築される一方、データストアは多くの場合リレーショナルデータベース(RDB)が採用されます。この2つの世界観を結ぶ際に生じる根本的な課題が、オブジェクト・リレーショナルマッピング(ORM)におけるインピーダンスミスマッチです。主な差異は以下の3つの側面で顕著に現れます。

  • データ型の変換: RDBにおける数値精度、文字列長の制限、あるいはJavaにおけるEnum型や特殊な日時型など、言語の型システムとDBの型定義が必ずしも一致するわけではありません。
  • データ構造の形状: ドメインモデルは階層的で相互参照を含むオブジェクトグラフですが、RDBはフラットなテーブル構造です。このグラフ構造を関係代数の表形式に変換する必要があります。
  • オブジェクト指向パラダイム: カプセル化、継承、多態性といったOOPの核心要素を、結合や正規化が基本であるRDBで直接的に表現するのは困難です。特にLiskovの置換原理やポリモーフィズムをテーブル設計に反映させるには工夫が求められます。

これらのギャップを埋めるためにORMフレームワークが存在します。Javaの生態系では、JPA(Java Persistence API)がこれらのマッピングを標準化する役割を果たしており、ORM実装の指針となっています。

JPAによるミスマッチ克服手法

ORMはオブジェクトグラフとテーブル構造の橋渡しを目的としており、通常はアノテーションやXMLといったメタデータでマッピングルールを宣言します。JPAは多様なシナリオを想定し、標準的なアノテーションを提供しています。

型変換の処理戦略

列挙型(Enum)の表現
RDBにはネイティブなEnum型が存在しないため、通常は整数や文字列で代替します。

public enum WorkerCategory {
    HOURLY, SALARIED, COMMISSION_BASED
}

public class Worker {
    @Enumerated(EnumType.STRING)
    @Column(name = "category", length = 30)
    private WorkerCategory category;
}

数値で保存すると順序は保持されますが、DB直接操作時に意味が不明確になります。そのため、実際のシステムでは文字列変換(@Enumerated(EnumType.STRING))を採用することが推奨されます。

日時型のマッピング
Javaの日時APIは歴史的に複雑ですが、JPA 2.2以降はjava.timeパッケージの大半をネイティブにサポートします。旧バージョンや特殊なケースでは@TemporalAttributeConverterが使用されます。

@Converter(autoApply = true)
public class LocalDateConverter implements AttributeConverter<LocalDate, java.sql.Date> {
    @Override
    public java.sql.Date convertToDatabaseColumn(LocalDate locDate) {
        return locDate == null ? null : java.sql.Date.valueOf(locDate);
    }

    @Override
    public LocalDate convertToEntityAttribute(java.sql.Date sqlDate) {
        return sqlDate == null ? null : sqlDate.toLocalDate();
    }
}

主キーとアイデンティティの設計
RDBの主キーは行の物理的識別子ですが、DDDにおける実体(Entity)のIDはビジネス上の同一性を保証する概念です。この違いを吸収するため、値オブジェクト(Value Object)としてアイデンティティをカプセル化します。

@Embeddable
public class WorkerId implements Serializable {
    @Column(name = "id")
    private String rawValue;

    public WorkerId() {}

    private WorkerId(String rawValue) {
        this.rawValue = rawValue;
    }

    public static WorkerId generate() {
        return new WorkerId(UUID.randomUUID().toString());
    }

    public String raw() { return rawValue; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof WorkerId)) return false;
        return rawValue.equals(((WorkerId) o).rawValue);
    }
    @Override
    public int hashCode() { return Objects.hash(rawValue); }
}

エンティティ側では@EmbeddedIdを用いて紐付けます。これにより、DBスキーマ変更時の影響を局所化し、ドメインの意図を明確に保てます。

構造・関連性の表現

エンティティ同士の関連は@OneToMany@ManyToOneで定義されます。一方、エンティティと値オブジェクトの集合関係は@ElementCollectionを使用します。値オブジェクトは独立したIDを持たないため、親エンティティのキーを外部キーとして持ち、归属関係を表現します。

@Entity
@Table(name = "salaried_workers")
public class SalariedWorker {
    @EmbeddedId
    private WorkerId id;

    @ElementCollection
    @CollectionTable(name = "leave_records", joinColumns = @JoinColumn(name = "worker_id"))
    private List<LeaveRecord> leaves = new ArrayList<>();
}

継承とポリモーフィズムの永続化

OOPの継承をRDBにマッピングするには3つの標準戦略が用意されています。

  • Single Table: 全サブクラスのフィールドを1つのテーブルに統合し、判別列(Discriminator Column)で型を区別します。クエリは高速ですが、カラムのスパース化(無駄なNULL)が起こりやすいです。
  • Joined: 各クラスに1つのテーブルを作成し、親テーブルとの結合で継承構造を再現します。正規化は徹底されますが、頻繁なJOIN処理により性能が低下するリスクがあります。
  • Table Per Class: サブクラスごとに親のフィールドも含めた独立したテーブルを作成します。JOIN不要でクエリが簡潔ですが、スキーマ定義に重複が発生します。フィールドの差異が大きい場合は最も実用的な選択です。
@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@Table(name = "workers")
public abstract class Worker { /* ... */ }

継承の本質はコード再利用ではなく、ポリモーフィズムによる拡張性にあります。DDDでは「継承より合成」の原則を重視し、データモデルとの整合性を取るため、適切な継承戦略の選定とインターフェース抽象の活用が不可欠です。

永続化対象外のドメイン概念

すべてのドメインクラスをDBにマッピングする必要はありません。ビジネスルールをカプセル化し、状態や振る舞いを安全に定義するための一時的なクラス(トランジエントクラス)や、マッピングから除外するフィールド(@Transient)が頻繁に使用されます。

public class PaymentPeriod {
    private final LocalDate start;
    private final LocalDate end;

    public PaymentPeriod(YearMonth month) {
        this.start = month.atDay(1);
        this.end = month.atEndOfMonth();
    }

    public boolean covers(LocalDate date) {
        return !date.isBefore(start) && !date.isAfter(end);
    }

    public Salary calculateBasePay(Salary rate, int workedHours) {
        return rate.multiply(workedHours / 8.0);
    }
}

このように、永続化機構に依存しない純粋なPOJOとして設計することで、クリーンアーキテクチャが求める層間依存の制御とテスト容易性を両立できます。JPAアノテーションはメタデータとして解釈されるため、ドメイン層の純粋性を損なうことなく、インフラ層との接続を安全に維持できます。

タグ: DomainDrivenDesign jpa ORM インピーダンスミスマッチ Java

5月30日 21:24 投稿