LoginSignup
98

More than 5 years have passed since last update.

JPA と DDD の関係で僕が思っていること

Last updated at Posted at 2016-12-03
1 / 64

このスライドについて

このスライドは、 JJUG CCC 2016 Fall でお話ししたときに使用したスライドです。


自己紹介

  • opengl-8080
  • 主に Qiita で技術メモを書いたり
  • 関西の SIer 勤務

今日話すこと

  • JPA と DDD の関係について思っていること
  • JPA で DDD のパターンを実装するとどうなるか

JPAとDDDの関係で思っていること

  • 最初は、 JPA に対してあまり良いイメージはなかった
  • DDD を学ぶにつれて、徐々にイメージが変わっていった
  • なぜ変わっていったのか、どう変わっていったのか

JPAでDDDのパターンを実装

  • エンティティ・値オブジェクトなどを JPA で実装する
  • 仕様上の限界、実装ごとの現実

JPAとDDDの関係で思っていること


JPA へのイメージの変化

  • DB アクセスライブラリ1との出会い
    • DB アクセスってこうやるのかぁ
  • JPA との出会い
    • JPA 使いづらいなぁ
  • DDD との出会い
    • あれ、 JPA って。。。

DB アクセスライブラリとの出会い

  • 新人の頃に関わった案件で、初めて DB アクセスライブラリと出会う
  • JDBC を軽くラップした感じのシンプルなライブラリ
    • SQL は直接記述
    • 1レコードを1オブジェクトにマッピング
    • カラム名とフィールド名を一致させて自動マッピング
      • 一致させられない場合はアノテーションで明示
    • 基本型(プリミティブ型, String, Date など)はそのまま使用
イメージ
// テーブルとマッピングするためのクラス
public class Foo {
  private Long id;
  private String value;
  @ColumnMapping("user_name")
  private String userName;

  // Getter, Setter
}

// 検索
String sql = "select * from foo_table where id=?";
Foo foo = dbAccessLibrary.find(Foo.class, sql, 1L);

DB アクセスライブラリの使われ方

  • テーブルに対応するオブジェクトはデータの入れ物
    • フィールドと Getter, Setter のみ
    • Manager, Service と呼ばれるクラス達が値を Get してビジネスロジックを実現
  • テーブル1つに対して、次の2つのクラスを作る
    • 入れ物クラス
    • DB アクセス用クラス(Dao)
  • クラス間に関連はない
    • DB 上外部参照しているテーブルの ID を持ち、その値を取得して明示的に検索する
  • 変更を DB に反映する場合は、テーブルごとの Dao を使って明示的に update(), delete() を呼ぶ

最初のDBアクセスライブラリ.jpg

外部参照を辿って明示的に検索
Long barId = foo.getBarId();
Bar bar = barDao.findById(barId);
テーブルごとに変更を明示的に反映する
foo.setValue("xxxx");
fooDao.update(foo);

Bar bar = barDao.findById(foo.getBarId());
bar.setName("yyyy");
barDao.update(bar);

当時の私「DB アクセスってこうするのかぁ」


最初に覚えた DB アクセスのイメージ

  • 1テーブル、1クラス
  • 1レコード、1オブジェクト
  • フィールドの型は基本型(プリミティブ型、 String, Date など)
  • テーブルごとに Dao を作る
  • DB に変更を反映する場合は、プログラマが明示的に記述する
  • SQL は直接書く

JPA との出会い

  • Java EE の勉強をしていて JPA に出会う
  • DB アクセスの標準仕様ということで勉強を開始

JPA の第一印象

  • 仕様が大きくて、覚えることが多い
  • 個々の仕様も、難解だったり複雑な印象

大量のアノテーション

  • @Entity
  • @Table
  • @Column
  • @Id
  • @GeneratedValue
  • @JoinColumn
  • @JoinTable
  • @ElementCollection
  • @CollectionTable
  • @OneToOne
  • @OneToMany
  • @ManyToMany
  • @ManyToOne
  • @Embeddable
  • @Embedded
  • @EmbeddableId
  • etc...

(;´д`)ぇぇ・・・
名前のマッピングくらいでいいんじゃないの...?


複雑な Entity のライフサイクル

                    ○      ◎
                new |      ^ GC
                    v      |
                   +---------+
                   |   NEW   | <--------------+
                   +---------+                |
   find/JPQL             |                    |
○----------------------+ |                    |
                       | | persist            |
                       v v                    |
+---------+  merge   +---------+  remove   +---------+
|         | -------> |         | --------> |         |
|DETACHED | <------- | MANAGED |           | REMOVED |
|         |  clear   |         | <-+       |         |
+---------+  detach  +---------+   |       +---------+
                       ^    |      |
                       |    +------+
                       |      refresh
                       |      setter
                       v
                    +-------------+
                    +-------------+
                    |   Database  |
                    +-------------+

JPAエンティティのライフサイクルをテキストで書いてみた by making@github - Qiita

※表記を英語にしています


(;´д`) 複雑...
今まではそんなのなかったのに...


JPQL

    SELECT bar.id
      FROM Foo foo
INNER JOIN foo.barList bar
     WHERE foo.id = :id

SQL とは似て非なる、独自のクエリ言語


(;´д`) え、新しい言語覚えるの?!
SQL 書かせてよ...


DB に更新結果を反映する方法

  • 明示的な UPDATE は行わない
  • インスタンスのフィールドを書き換えると、自動的に変更内容が DB に反映される
  • CASCADE を指定すれば、関連する Entity の登録・削除が連動する

(;´д`) なにそれこわい
なんでそんなことを...


最初に覚えた DB アクセスのイメージ

  • 1テーブル、1クラス
  • 1レコード、1オブジェクト
  • フィールドの型は基本型(プリミティブ型、 String, Date など)
  • テーブルごとに Dao を作る
  • DB に変更を反映する場合は、プログラマが明示的に記述する
  • SQL は直接書く

※再掲


これまでと全然違う!


JPA に対する初期のイメージ

  • 当然のように「使いづらい」と感じる
  • それでも「標準仕様だからと覚えておこう」という後ろ向きな姿勢
  • もっとシンプルな仕様を追加して、選択できるようにして欲しい

DDD との出会い

  • ドメインモデル貧血症を知る
    • ビジネスロジックを持たないクラス
    • Getter, Setter だけのデータクラス
    • あれ...見覚えがあるぞ...?
  • 現状に疑問を持ち始める
  • オブジェクト指向についての情報を集めていたら DDD と出会う
  • Domain Driven Design (ドメイン駆動設計)

DDD で出会ったパターン

  • エンティティ
  • 値オブジェクト
  • 集約
  • リポジトリ

エンティティ

  • ドメインをモデリングしていると、「属性」ではなく「同一性」で識別するオブジェクトがあることに気付く
  • エンティティは属性が変化しても同一性は変わらない
    • 人は身長や名前などの属性が変化しても、「その人」であるという同一性は変わらない
  • DDD では、そのようなオブジェクトをエンティティと呼び、他のオブジェクトと区別する
  • 同一性を識別するための仕組みが必要になる
    • よくあるのは一意な連番を割り当てる

値オブジェクト

  • エンティティとは逆に、同一性を持たないオブジェクト
  • 属性が一致していれば同じ物として扱える
  • 例:金額、数量、名前、etc...
  • システムが対象とする領域(ドメイン)を記述する基本的な語彙をオブジェクトで表現する
  • インスタンスを共有できるようにするため、不変に設計することが望ましい

集約

集約.jpg

  • 1つのエンティティをルートとする、オブジェクト(エンティティと値オブジェクト)の論理的なまとまり
  • 集約内部へのアクセスはルートエンティティを介してのみ行えるようにする
    • 集約内の不変条件2を守る
    • 誤ったデータアクセス・変更を防ぐ
    • 集約内の変更を1回の操作(ルートエンティティのメソッド呼び出し)で完結させる
  • 一種のカプセル化
  • 集約内のオブジェクトは、一括で登録・削除されなければならない

リポジトリ

  • 永続化層とのやり取りを抽象化するクラス
  • ルートエンティティを取得するための手段を提供する
    • ルートエンティティ以外のオブジェクトは、ルートエンティティを介して取得する(集約)
  • ドメインから永続化層の関心事を切り離す

JPA は DDD を意識している...?

  • エンティティと @Entity
  • 値オブジェクトと @Embeddable
  • ルートエンティティから関連を辿る
  • 集約とリポジトリ、 JPA のユニットオブワーク

というよりも、 JPA と DDD の両方がオブジェクト指向的なプログラミングスタイルを意識しているから一致しているように見える?


エンティティと @Entity

DDD のエンティティ

  • 同一性を実現するため、一意に識別するための方法を定義する

JPA の Entity

  • @Id などのアノテーションで一意となる項目を定義する

Every entity must have a primary key.3

(訳)
全ての Entity は、一意なキーを持たなければならない。


値オブジェクトと @Embeddable

DDD の値オブジェクト

  • 同一性を持たない
  • 基本型をラップしてドメインの基本的な語彙を表現する
  • 他の値オブジェクトやエンティティと関連を持てる

JPA の @Embeddable (埋め込み可能クラス)

  • 一意なキーは定義しない

Instances of these classes, unlike entity instances, do not have persistent identity of their own.4

(訳)
これら(埋め込み可能クラス)のインスタンスは、 Entity のインスタンスとは異なり、永続化のための一意なキーを持ちません。

  • 基本型をラップしたオブジェクトを定義できる
  • 他の埋め込み可能クラスや Entity と関連を持てる

An embeddable class may be used to represent the state of another embeddable class.
An embeddable class may contain a relationship to an entity or collection of entities.4

(訳)
埋め込み可能クラスは、他の埋め込み可能クラスの状態を表現するために使用できる。
埋め込み可能クラスには、 Entity や Entity のコレクションの関連が含まれることがある。


ルートエンティティから関連を辿る

DDD の関連

  • 集約のルートエンティティから、オブジェクトの関連を辿って必要なオブジェクトにアクセスする

JPA の関連

  • アノテーションで Entity 間の関連を定義
    • @OneToOne, @OneToMany, @ManyToOne, @ManyToOne, @ManyToMany
  • DB とのマッピングもアノテーションで宣言
    • @JoinTable, @JoinColumn
  • 各種定義情報に従って JPA が自動で関連する Entity を取得していく
    • クライアントは、普通にオブジェクトのフィールドにアクセスするだけ
    • DB アクセスは JPA が隠蔽する

集約とユニットオブワーク

DDD の集約

  • エンティティをルートとしたオブジェクトのまとまり
  • リポジトリからルートエンティティを取得し、ビジネスロジックを実行して状態を変更させる
  • ビジネスルールなどは集約内部のエンティティや値オブジェクトがチェックする

これを冒頭で紹介したようなシンプルな DB アクセスライブラリで実装しようとしたときに、手が止まった―――


集約とリポジトリをシンプルな DB アクセスライブラリで実現する

シンプルなDBアクセスライブラリでリポジトリを実装する.jpg

例えばこんな感じになる?
// エンティティ
public class Foo {
    private Long id;
    private String value;
    private Bar bar;
    // ビジネスロジック...
}

public class Bar {
    private Long id;
    private String value;
    // ビジネスロジック...
}

// テーブルとマッピングするためのクラス
class FooTable {
    Long id;
    String value;
    Long barId;
}

class BarTable {
    Long id;
    String value;
}

public class FooRepository {
    // JDBC をラップした架空の DB アクセスライブラリ
    private MyJdbcWrapper jdbc;

    // 検索
    public Foo find(Long id) {
        // Foo と
        FooTable fooTable = this.jdbc.select(
            FooTable.class,
            "SELECT * FROM FOO_TABLE WHERE ID=?",
            id
        );

        // Bar のデータを検索して
        BarTable barTable = this.jdbc.select(
            BarTable.class,
            "SELECT * FROM BAR_TABLE WHERE ID=?",
            fooTable.barId
        );

        // Entity に詰めて返す
        Bar bar = new Bar(barTable.id, barTable.value);
        return new Foo(fooTable.id, fooTable.value, bar);
    }

    // 登録
    public void persist(Foo foo) {
        // 先に Bar を登録して ID を採番し
        BarTable barTable = new BarTable();
        barTable.value = foo.getBar().getValue();

        this.jdbc.insert(barTable);
        foo.getBar().setId(barTable.id);

        // 採番された Bar の ID を使って Foo を登録
        FooTable fooTable = new FooTable();
        fooTable.value = foo.getValue();
        fooTable.barId = barTable.id;

        this.jdbc.insert(fooTable);
        foo.setId(fooTable.id);
    }

    // 更新
    public void update(Foo foo) {
        // Foo と
        FooTable fooTable = new FooTable();
        fooTable.id = foo.getId();
        fooTable.value = foo.getValue();
        fooTable.barId = foo.getBar().getId();

        this.jdbc.update(fooTable);

        // Bar の情報を Entity の値で更新
        BarTable barTable = new BarTable();
        barTable.id = foo.getBar().getId();
        barTable.value = foo.getBar().getValue();

        this.jdbc.update(barTable);
    }

    // 削除
    public void remove(Foo foo) {
        // 外部参照の都合上、先に Bar を消して、
        BarTable barTable = new BarTable();
        barTable.id = foo.getBar().getId();

        this.jdbc.delete(barTable);

        // 次に Foo を消す
        FooTable fooTable = new FooTable();
        fooTable.id = foo.getId();

        this.jdbc.delete(fooTable);
    }
}

集約の一部を更新することの難しさ

前述の実装には問題がある。

再掲
public void update(Foo foo) {
    // Foo と
    FooTable fooTable = new FooTable();
    fooTable.id = foo.getId();
    fooTable.value = foo.getValue();
    fooTable.barId = foo.getBar().getId();

    this.jdbc.update(fooTable);

    // Bar の情報を Entity の値で更新
    BarTable barTable = new BarTable();
    barTable.id = foo.getBar().getId();
    barTable.value = foo.getBar().getValue();

    this.jdbc.update(barTable);
}

問題点

bar が別のインスタンスに差し変わる可能性がある場合、このままだとダメ

集約の一部を変更することの難しさ.jpg

  • 集約のルート(Foo)は更新でも、集約の内部(Bar)は単純な更新ではない可能性がある
  • 関連が bar のコレクションになると、さらに複雑
    • 一部の要素が削除、一部の要素は更新、一部の要素は登録
  • bar がさらに別のオブジェクトと関連を持っていたら...
  • いずれにしても、更新前の状態がどうたったかという情報が必要になる
    • 「更新前の状態」と「更新後の状態」を比較し、必要な処理を決定しなければならない

解決方法を色々考えた...(1)

変更前の foo と変更情報を持ったオブジェクトをリポジトリに渡す?

public void update(Foo foo, UpdateData data) {
  Foo before = this.deepCopy(foo);
  foo.callBusinessLogic(/* data から必要な情報を渡す */);
  // before と foo を比較して、必要な更新処理を実行
}

ビジネスロジックの実行がリポジトリの中になって不自然...


解決方法を色々考えた...(2)

変更前の情報は DB から再検索する?
※排他ロックされている前提

public void update(Foo after) {
    Foo before = this.find(after.getId());
    // before と after を比較して、必要な更新処理を実行
}

二度手間感...
でもやむを得ない...?


解決方法を色々考えた...(3)

検索した時点の変更前のインスタンスのコピーをキャッシュしておいて、変更後のインスタンスと差分を取る?

public Foo find(Long id) {
    Foo foo = ...;
    FooRepository.putCache(id, this.deepCopy(foo));
    return foo;
}

public void update(Foo after) {
    Foo before = FooRepository.getCache(after.getId());
    // before と after を比較して、必要な更新処理を実行
}

え...それって JPA がやってることを自力で実装してるやん...


JPA のユニットオブワーク

  • Entity のインスタンスは EntityManager によってライフサイクルが管理される
  • Managed 状態になったインスタンスは、トランザクションがコミットされるときにインスタンスに加えた変更が自動的に DB に反映される
JPAでリポジトリを実装した場合
public class FooRepository {
    private EntityManager em; // DI などで注入

    public Foo find(Long id) {
        return this.em.find(Foo.class, id);
    }

    public void persist(Foo foo) {
        this.em.persist(foo);
    }

    public void remove(Foo foo) {
        this.em.remove(foo);
    }
}
  • find(Long) で検索した Foo のインスタンスは EntityManager によって管理されている
  • Foo のインスタンスへの変更は JPA が自動的に検出して DB に反映するので、 update() はそもそも不要になる
  • あたかもエンティティのインスタンスをメモリ上に保持したコレクションであるかのように、リポジトリが振る舞う
    • リポジトリの実装をただの Map の実装に差し替えられる
コレクションでCRUD
// 登録
Map<Long, Foo> map = new HashMap<>();
map.put(1L, foo);

// 検索
Foo foo = map.get(1L);

// 更新
foo.setValue("xxx");
foo.getBar().setValue("yyy");

// 削除
map.remove(foo);
リポジトリでCRUD
// 登録
repository.persist(foo);

// 検索
Foo foo = repository.find(1L);

// 更新
foo.setValue("xxx");
foo.getBar().setValue("yyy");

// 削除
repository.remove(foo);

グローバルアクセスを必要とするオブジェクトの各型に対して、あるオブジェクトを生成し、その型のすべてのオブジェクトで構成されるコレクションが、メモリ上にあると錯覚させることができるようにすること5


JPA に対するイメージの変化

  • 最初のイメージ
    • 重厚で複雑で使いにくい
  • 最近のイメージ
    • オブジェクト指向に徹底的に振り切っている
    • データベースに関する関心事がクライアントの実装に漏れないように徹底している
    • 大量で複雑なアノテーション
      • DB とオブジェクトとの変換ルールをすべてアノテーションで表現できるようにした結果...?
      • 変換ルールさえ定義しておけば、あとは JPA が裏で勝手にやる
    • JPQL
      • オブジェクトモデルに対してクエリを実行できるようにしている
    • ユニットオブワーク
      • DB を隠蔽し、メモリ上のオブジェクトを操作しているかのような感覚

前半まとめ

JPA に対するイメージの変化

  • DB アクセスライブラリとの出会い
    • データ中心で非オブジェクト指向
    • DB アクセスライブラリは、 JDBC のシンプルなラッパー程度の認識
  • JPA との出会い
    • それまでの DB アクセスライブラリのイメージを覆す、複雑で重厚な仕様
    • 使いづらいイメージ
  • DDD との出会い
    • 非オブジェクト指向スタイルなイメージで JPA を見ていたため、使いづらいと誤解していた(と思ってる)
    • オブジェクト指向を徹底しようとすると、 JPA の機能が力を発揮する(と期待している)

JPA で DDD のパターンを実装するとどうなるか(後半)

  • DDD で紹介されているいくつかのパターンを JPA で実装する
    • エンティティ
    • 値オブジェクト
    • 集約
  • JPA の仕様上、どうしても理想通りできない部分がある
    • JPA の標準仕様に従った場合の限界を紹介
    • しかし、 JPA の実装によっては可能なものもある
      • ※実際に動かした結果であり、各ライブラリが明確にサポートしているかどうかまでは調べられていません、すみません m(__)m

前提

以下の環境で動作確認をしました

  • JPA の実装(ver 2.1 相当)
    • EclipseLink 2.6.4
    • Hibernate 5.2.2
    • Open JPA 2.4.1 (一部だけ)
      • JPA 2.1 未サポート
  • データベース
    • MySQL (mysql Ver 14.14 Distrib 5.5.28, for Win64 (x86))

エンティティを実装する

package sample.jpa;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name="table_alpha")
public class EntityAlpha implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Column(name="alpha_name")
    private String name;
}
  • Getter, Setter は不要
    • フィールドを直接参照するか、アクセサ経由で参照するかを選択できる
  • フィールドは private 可。

The persistent state of an entity is accessed by the persistence provider runtime either via JavaBeans style property accessors (“property access”) or via instance variables (“field access”).6

(訳)
エンティティの永続化された状態は、 JavaBeans スタイルのプロパティアクセサ経由(プロパティアクセス)か、インスタンス変数経由(フィールドアクセス)のいずれかで、永続化プロバイダのランタイムによってアクセスされる。

The instance variable of a class must be private, protected, or package visibility independent of whether field access or property access is used.6

(訳)
クラスのインスタンス変数は、フィールドアクセス・プロパティアクセスのどちらを使うかに関係なく、 private, protected またはパッケージプライベートでなければならない。


制約1:コンストラクタ

  • 次の条件をすべて満たすコンストラクタが必要
    • 引数なし
    • 可視性が protectedpublic

The entity class must have a no-arg constructor.
The no-arg constructor must be public or protected.7

(訳)
エンティティクラスは、引数なしのコンストラクタを持っていなければならない。
引数なしのコンストラクタは、 publicprotected でなければならない。

JPAの仕様に沿ったエンティティの実装
...

@Entity
@Table(name="table_alpha")
public class EntityAlpha implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Column(name="alpha_name")
    private String name;

    // 引数ありのコンストラクタを定義した場合は...
    public EntityAlpha(String name) {
        this.name = name;
    }

    // 引数なしのコンストラクタを定義しなければならない
    protected EntityAlpha() {}
}
  • 外部から間違って使われると嫌な場合は @Deprecated でアノテートしておくとか...

実際のところ

引数なしのコンストラクタ 可視性 private
EclipseLink 必須
Hibernate 必須
Open JPA 必須 不可
EclipseLinkなどならこう書ける
...

@Entity
@Table(name="table_alpha")
public class EntityAlpha implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Column(name="alpha_name")
    private String name;

    public EntityAlpha(String name) {
        this.name = name;
    }

    // private にできる
    private EntityAlpha() {}
}

制約2:IDを埋め込み可能クラスにする

  • ID を埋め込み可能クラスにすることは可能
    • @Id の代わりに @EmbeddedId でアノテートする
  • ただし、関連がサポートされなくなる

Relationship mappings defined within an embedded id class are not supported.8

(訳)
埋め込みID クラスで定義された関連のマッピングは、サポートされない。

実際のところ

ID を埋め込み可能クラスにしたときの挙動

検索 CASCADE
EclipseLink
Hibernate 不可

EclipseLink で実装するとこんな感じ

IDを埋め込み可能クラスにした場合.jpg

EntityAlpha.java
package sample.jpa;

import sample.Id;

import javax.persistence.CascadeType;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name="table_alpha")
public class EntityAlpha implements Serializable {
    @EmbeddedId
    private Id<EntityAlpha> id;

    private String name;

    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(
        name="table_beta_id",
        referencedColumnName="id"
    )
    private EntityBeta beta;

    @PrePersist
    private void setupId() {
        this.id = new Id<>();
    }
}
EntityBeta.java
package sample.jpa;

import sample.Id;

import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.PrePersist;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name="table_beta")
public class EntityBeta implements Serializable {
    @EmbeddedId
    private Id<EntityBeta> id;

    private String name;

    @PrePersist
    private void setupId() {
        this.id = new Id<>();
    }
}
Id.java
package sample;

import javax.persistence.Embeddable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import java.io.Serializable;

@Embeddable
public class Id<T> implements Serializable {
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
}

ポイントは次の2つ

  1. @JoinColumnreferencedColumnName を指定
  2. @PrePersist で ID クラスのインスタンスを new する

Hibernate は、 INSERT の CASCADE がうまく動いてくれなかった(外部参照キーがセットされない)


そもそも ID を埋め込み可能クラスにしたときに CASCADE は必要か?

集約.jpg

  • 埋め込み可能クラス(値オブジェクト)にしたい

    • 型として意識したい
    • その値が単独でやり取りされることがある
  • ID が単独でやり取りされるのは、ルートエンティティの ID くらい(のはず)

    • 集約内のエンティティは関連を辿って取得するので、 ID を明示して使うことは基本的にない(はず)
    • 他のルートエンティティを参照することがあるが、集約が別なので CASCADE は不要(のはず)

値オブジェクトを実装する

package sample.jpa;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;

@Embeddable
public class EmbeddableAlpha implements Serializable {
    @Column(name="embeddable_value")
    private int value;

    public EmbeddableAlpha(int value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object obj) {
        return (obj instanceof EmbeddableAlpha)
               && this.value == ((EmbeddableAlpha)obj).value;
    }

    @Override
    public int hashCode() {
        return this.value;
    }

    protected EmbeddableAlpha() {}
}
  • 値が等しければ同じであると判断できるように、 equals()hashCode() を実装
    • Lombok の @EqualsAndHashCode を使うと楽
    • @Value はフィールドを final にしてしまうので NG(詳細次ページ)
    • @Data は Getter, Setter を生成してしまうので良くない
  • コンストラクタやフィールドアクセスに関する制約は Entity と同じ

制約3:フィールドを final にできない

  • 値オブジェクトは不変に設計すべきなので、できれば final にできたほうが良い
  • しかし、仕様上 Entity と 埋め込み可能クラスのフィールドは final にできない
  • Setter や副作用のあるメソッドを定義しないようにすれば、不変にすること自体は可能

No methods or persistent instance variables of the entity class may be final.9

(意訳)
エンティティクラスのメソッドまたはインスタンス変数を final にすることはできません。

実際のところ

フィールドの final
EclipseLink
Hibernate
Open JPA 不可
EclipseLinkなら値オブジェクトをこう実装できる
...

@Embeddable
public class EmbeddableAlpha implements Serializable {
    @Column(name="embeddable_value")
    private final int value; // final にできる

    public EmbeddableAlpha(int value) {
        this.value = value;
    }

    @Override
    public boolean equals(Object obj) {
        return (obj instanceof EmbeddableAlpha)
               && this.value == ((EmbeddableAlpha)obj).value;
    }

    @Override
    public int hashCode() {
        return this.value;
    }

    private EmbeddableAlpha() { // private にできる
        this.value = -1;
    }
}

final なのに値が JPA によって書き換えられるのがとてもキモイけど、一応動く


制約4:埋め込み可能クラスをそのままの形で JPQL で比較することはできない

埋め込み可能クラスを、中の値を取り出さずにそのまま比較できると嬉しい

理想
String jpql =
    "SELECT alpha"
  + "  FROM EntityAlpha alpha"
  + " WHERE alpha.embeddableValue = :embeddableValue";

TypedQuery<EntityAlpha> query
    = entityManager.createQuery(jpql, EntityAlpha.java);

// これは仕様上不可
query.setParameter("embeddableValue", new EmbeddableValue("FOO"));

でも仕様上は、、、

Only equality/inequality comparisons over enums are required to be supported.
Comparisons over instances of embeddable class or map entry types are not supported. 10

(訳)
enum の等価/不等価の比較だけはサポートされなければなりません。
埋め込み可能クラス、または Map エントリ型のインスタンスの比較はサポートされません。

なので、現実はこう↓

現実
String jpql =
    "SELECT alpha"
  + "  FROM EntityAlpha alpha"
  + " WHERE alpha.embeddableValue.value = :value";

TypedQuery<EntityAlpha> query
    = entityManager.createQuery(jpql, EntityAlpha.java);

query.setParameter("value", "FOO");

実際のところ

等価比較のサポート
EclipseLink
Hibernate
  • ただし、大小比較などは、 EclipseLink はサポートしていない(詳細次ページ)
  • 埋め込み可能クラスの比較は、対象の値が直接比較される模様
    • equals() をオーバーライドしても、 JPQL 実行時に呼ばれない

埋め込み可能クラスを JPQL でどこまで使用できるか

JPQLと埋め込み可能クラス.jpg

EclipseLink で IN で使用した場合、エラーにはならない。しかし比較が正しく行われないので注意。


集約と CASCADE

  • 集約内部のエンティティはアトミックに更新されなければならない
    • 登録のときは一緒に登録される
    • 削除のときは一緒に削除される
  • JPA だと、 CASCADE で指定できる

集約とCASCADE.png

EntityAlpha.java
package sample.jpa;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name="table_alpha")
public class EntityAlpha implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    // CASCADE を指定する
    @OneToOne(cascade=CascadeType.ALL)
    @JoinColumn(name="table_beta_id")
    private EntityBeta beta;

    ...
}
EntityBeta.java
package sample.jpa;

...

@Entity
@Table(name="table_beta")
public class EntityBeta implements Serializable {
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    // CASCADE を指定しない
    @OneToOne
    @JoinColumn(name="table_gamma_id")
    private EntityGamma gamma;

    ...
}
EntityGamma.java
package sample.jpa;

...

@Entity
@Table(name="table_gamma")
public class EntityGamma implements Serializable {
    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    ...
}

実装例(モデル)

ここまでの話を全てまとめつつ、もうちょっと具体的な実装例を紹介

JPA実装例_Java.png

JPA実装例_DB.jpg

実際はもっと複雑になるだろうけど、ここは感触を伝えるため簡略なモデルで


実装例(コード)

注文
package order;

import javax.persistence.CascadeType;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;
import java.util.List;

@Entity
@Table(name="order_requests")
public class OrderRequest implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @Embedded
    private DeliveryDate deliveryDate;

    // 同じ集約内なので CASCADE を指定
    @OneToMany(
        cascade=CascadeType.ALL,
        fetch=FetchType.EAGER
    )
    @JoinTable(
        name="order_request_relations",
        joinColumns=@JoinColumn(
            name="order_requests_id"
        ),
        inverseJoinColumns=@JoinColumn(
            name="order_request_details_id"
        )
    )
    private List<OrderRequestDetail> orderRequestDetails;

    public OrderRequest(
        DeliveryDate deliveryDate,
        List<OrderRequestDetail> orderRequestDetails
    ) {
        this.deliveryDate = deliveryDate;
        this.orderRequestDetails = orderRequestDetails;
    }

    // 引数なしで protected 以上のコンストラクタが必要
    protected OrderRequest() {}
}
納期
package order;

import lombok.EqualsAndHashCode;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;
import java.util.Date;

@Embeddable
@EqualsAndHashCode
public class DeliveryDate implements Serializable {
    @Temporal(TemporalType.DATE)
    @Column(name="delivery_date")
    private Date date; // final にできない

    public DeliveryDate(Date date) {
        this.date = date;
    }

    // 引数なしで protected 以上のコンストラクタが必要
    protected DeliveryDate() {}
}
注文明細
package order;

import item.Item;
import item.ItemName;
import item.ItemUnitPrice;

import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name="order_request_details")
public class OrderRequestDetail implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;
    // 別の集約なので、 CASCADE は指定しない
    @OneToOne
    @JoinColumn(name="items_id")
    private Item item;
    @Embedded
    private ItemName itemName;
    @Embedded
    private ItemUnitPrice itemUnitPrice;
    @Embedded
    private Quantity quantity;

    public OrderRequestDetail(Item item, Quantity quantity) {
        this.item = item;
        this.itemName = item.getName();
        this.itemUnitPrice = item.getUnitPrice();
        this.quantity = quantity;
    }

    // 引数なしで protected 以上のコンストラクタが必要
    protected OrderRequestDetail() {}
}
商品名称
package item;

import lombok.EqualsAndHashCode;

import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.io.Serializable;

@Embeddable
@EqualsAndHashCode
public class ItemName implements Serializable {
    @Column(name="item_name")
    private String value; // final にできない

    public ItemName(String value) {
        this.value = value;
    }

    // 引数なしで protected 以上のコンストラクタが必要
    protected ItemName() {}
}
商品
package item;

import javax.persistence.AttributeOverride;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;

@Entity
@Table(name="items")
public class Item implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    // カラム名のマッピングを上書き
    @Embedded
    @AttributeOverride(
        name="value",
        column=@Column(name="name")
    )
    private ItemName name;

    @Embedded
    @AttributeOverride(
        name="value",
        column=@Column(name="unit_price")
    )
    private ItemUnitPrice unitPrice;

    public Item(ItemName name, ItemUnitPrice unitPrice) {
        this.name = name;
        this.unitPrice = unitPrice;
    }

    // 引数なしで protected 以上のコンストラクタが必要
    protected Item() {}

    ...
}

(;´д`)アノテーション邪魔...


XML で定義する(XML 定義ファイル)

  • アノテーションが邪魔すぎてドメインを汚染すると思う場合は、 XML でマッピングを定義することもできる
persistence.xml
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1"
       xmlns="http://xmlns.jcp.org/xml/ns/persistence"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
                 http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">

  <persistence-unit name="SampleUnit" transaction-type="RESOURCE_LOCAL">
    <!-- クラスパス上の場所を指定 -->
    <mapping-file>mapping.xml</mapping-file>

    ...
  </persistence-unit>
</persistence>
mapping.xml
<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings
     xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
               http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"
     version="2.1">
  <description>JPA Sample</description>

  <persistence-unit-metadata>
    <!-- アノテーションの定義を無視する -->
    <xml-mapping-metadata-complete />

    <persistence-unit-defaults>
      <!-- デフォルトのアクセス方法をフィールドにする -->
      <access>FIELD</access>
    </persistence-unit-defaults>
  </persistence-unit-metadata>

  <entity class="order.OrderRequest">
    <table name="order_requests" />

    <attributes>
      <id name="id">
        <generated-value strategy="IDENTITY" />
      </id>

      <embedded name="deliveryDate" />

      <one-to-many name="orderRequestDetails" fetch="EAGER">
        <cascade>
          <cascade-all/>
        </cascade>
        <join-table name="order_request_relations">
          <join-column name="order_requests_id" />
          <inverse-join-column name="order_request_details_id" />
        </join-table>
      </one-to-many>
    </attributes>
  </entity>

  <embeddable class="order.DeliveryDate">
    <attributes>
      <basic name="date">
        <column name="delivery_date" />
        <temporal>DATE</temporal>
      </basic>
    </attributes>
  </embeddable>

  <entity class="order.OrderRequestDetail">
    <table name="order_request_details" />

    <attributes>
      <id name="id">
        <generated-value strategy="IDENTITY" />
      </id>

      <one-to-one name="item">
        <join-column name="items_id" />
      </one-to-one>

      <embedded name="itemName" />

      <embedded name="itemUnitPrice" />

      <embedded name="quantity" />
    </attributes>
  </entity>

  <embeddable class="order.Quantity">
    <attributes>
      <basic name="value">
        <column name="quantity" />
      </basic>
    </attributes>
  </embeddable>

  <embeddable class="item.ItemName">
    <attributes>
      <basic name="value">
        <column name="item_name" />
      </basic>
    </attributes>
  </embeddable>

  <embeddable class="item.ItemUnitPrice">
    <attributes>
      <basic name="value">
        <column name="item_unit_price" />
      </basic>
    </attributes>
  </embeddable>

  <entity class="item.Item">
    <table name="items" />

    <attributes>
      <id name="id">
        <generated-value strategy="IDENTITY" />
      </id>

      <embedded name="name">
        <attribute-override name="value">
          <column name="name" />
        </attribute-override>
      </embedded>

      <embedded name="unitPrice">
        <attribute-override name="value">
          <column name="unit_price" />
        </attribute-override>
      </embedded>
    </attributes>
  </entity>
</entity-mappings>

XMLで定義する(Java コード)

JPA に関する依存関係は全てなくなる

注文
package order;

import java.io.Serializable;
import java.util.Date;
import java.util.List;

public class OrderRequest implements Serializable {
    private Long id;
    private DeliveryDate deliveryDate;
    private List<OrderRequestDetail> orderRequestDetails;

    public OrderRequest(DeliveryDate deliveryDate, List<OrderRequestDetail> orderRequestDetails) {
        this.deliveryDate = deliveryDate;
        this.orderRequestDetails = orderRequestDetails;
    }

    protected OrderRequest() {}
}
納期
package order;

import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.util.Date;

@EqualsAndHashCode
public class DeliveryDate implements Serializable {
    private Date date;

    public DeliveryDate(Date date) {
        this.date = date;
    }

    protected DeliveryDate() {}
}
注文明細
package order;

import item.Item;
import item.ItemName;
import item.ItemUnitPrice;

import java.io.Serializable;

public class OrderRequestDetail implements Serializable {
    private Long id;
    private Item item;
    private ItemName itemName;
    private ItemUnitPrice itemUnitPrice;
    private Quantity quantity;

    public OrderRequestDetail(Item item, Quantity quantity) {
        this.item = item;
        this.itemName = item.getName();
        this.itemUnitPrice = item.getUnitPrice();
        this.quantity = quantity;
    }

    protected OrderRequestDetail() {}
}

後半まとめ

仕様上できること

  • Getter, Setter は不要
  • フィールドは private にできる
  • CASCADE を利用することで、集約の登録・削除を一括で行える
  • アノテーションが邪魔な場合は XML で定義できる

仕様上できないこと

  • フィールドは final にできない
  • protected or public で引数なしのコンストラクタが必要
  • 一意キーを埋め込み可能クラスにした場合、関連は使用できない
  • 埋め込み可能クラスを、そのままの形で JPQL で比較することはできない

実装(EclipseLink, Hibernate)による実際の動き

  • フィールドは final にできる
  • 引数なしのコンストラクタを private にできる
  • 一意キーを埋め込み可能クラスにしても、検索はできる
    • 更新は、 EclipseLink なら動く
  • 埋め込み可能クラスの JPQL での比較は、等価・不等価くらいなら可能

注意:実際に動かして検証しただけで、各々のライブラリの仕様を確認したわけではないので、ご利用は計画的に

教訓

RI(リファレンス実装) である EclipseLink が既に仕様と異なる動きをするので、「RI でできたから仕様通り」と思っているとポータビリティのない実装になってしまう可能性がある!


全体まとめ

  • JPA はオブジェクト指向に振り切ったライブラリ
  • DB アクセスを全力で隠蔽し、オブジェクトの世界だけで完結させようとしている(ように思える)
  • DDD のパターンを試そうとすると、 JPA の仕様上の制限により理想通りにいかないことがある
  • けど、それっぽいところまではいける
  • JPA の実装によっては制限が緩くなっており、理想に近づける
  • ただし、標準仕様のメリットであるはずのポータビリティは失われるので、ご利用は計画的に

以上


おまけ

  • 個人的に、 JPA の鬼門の1つは「アノテーションによるマッピング」だと思っている
  • あの大量のアノテーションに圧倒されて、やりたいマッピングを、どう定義すればいいのかすぐには分からない
  • ということで、 JPA のアノテーションのマッピングのカタログっぽいものを書いてみました。

  1. 「O/Rマッパー」と書くと厳密な定義とかでいろいろ面倒なマサカリが来そうなので、DB アクセスライブラリと濁すなど 

  2. 常に満たされなければならない条件・ビジネスルール。例えば、「限度額を超えて買い物かごに商品を入れられない」、とか。 

  3. JPA 2.1 仕様書 P.29 「2.4 Primary Keys and Entity Identity」 

  4. JPA 2.1 仕様書 P.40「2.5 Embeddable Classes」 

  5. エリックエヴァンスのドメイン駆動設計 P.151 リポジトリ 

  6. JPA 2.1 仕様書 P.24「2.2 Persistent Fields and Properties」 

  7. JPA 2.1 仕様書 P.23「The Entity Class」 

  8. JPA 2.1 仕様書 P.444「11.1.17 EmbeddedId Anootation」 

  9. JPA 2.1 仕様書 P.23「2.1 The Entity Class」 

  10. JPA 2.1 仕様書 P207「4.12 Equality and Comparison Semantics」 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
98