はじめに
この記事は、以下の記事の補足説明です。
識別子による外部の集約の参照
集約ルートである以下のような Order がある。
@Entity public class Order { @Id private Long id; @OneToMany private List<LineItem> lineItems; @ManyToOne private Customer customer; }
注文明細である lineItem は Order の集約に属するが、Customer は集約の外側にある。
同一のトランザクションで、参照する側の Order と、参照される側の Customer の両方を変更してはならない。
上記のように、他の集約ルートである Customer の参照を保持すると、モデル上に制約が表現できない。
そこで、Customer の参照を保持するのではなく、Customer の識別子をバリューオブジェクトとして保持する。
@Entity public class Order { @Id private Long id; @OneToMany private List<LineItem> lineItems; @Embedded private CustomerId customerId; }
CustomerId は以下のようなバリューオブジェクトとなる。
@Embeddable public class CustomerId { @Column(name = "customer_id") private Long customerId; }
Order から Customer を参照する場合は、order.getCustomerId() で取得した CustomerId を用い、リポジトリから Customer を得る必要があり、集約の境界を越えていることが明確となる。
Association
CustomerId の導入により、集約の外部への参照をコントロールすることとしたが、この ID はデータベース上のテクニカルな値である。
ドメインモデルの言葉にすれば、Order から Customer への Association(関連) であり、これをそのままの言葉で扱うのが Association となる。
ここでは簡単な例として、以下のような Association を考えた場合、
public interface Association<T, ID> { ID getId(); }
CustomerAssociation は以下のように実装できる。
@Embeddable public class CustomerAssociation implements Association<Customer, Long> { @Column(name = "customer_id") private Long customerId; // ... @Override public Long getId() { return customerId; } }
Order には Customer への関連であることが明示的にモデル化される。
@Entity public class Order { @Id private Long id; @OneToMany private List<LineItem> lineItems; @Embedded private CustomerAssociation customerAssociation; }
リポジトリでは以下のように Association から参照先のオブジェクトを取得する。
public T findByAssociation(Association<T, ID> assoc) { return findOne(assoc.getId()); }
jmolecules の Association
jmolecules では、他のモデル要素との制約が型引数として付与された形となっており、Association は以下のインターフェースとして定義されている。
public interface Association<T extends AggregateRoot<T, ID>, ID extends Identifier> extends Identifiable<ID> { static <T extends AggregateRoot<T, ID>, ID extends Identifier> Association<T, ID> forAggregate(T aggregate) { return new SimpleAssociation<>(() -> aggregate.getId()); } static <T extends AggregateRoot<T, ID>, ID extends Identifier> Association<T, ID> forId(ID identifier) { return new SimpleAssociation<>(() -> identifier); } default boolean pointsToSameAggregateAs(Association<?, ID> other) { return getId().equals(other.getId()); } default boolean pointsTo(ID identifier) { return getId().equals(identifier); } default boolean pointsTo(T aggregate) { return pointsTo(aggregate.getId()); } }
Association は集約ルートのエンティティ(T)に限定され、ID は集約ルートのエンティティの ID 値となる。
実装である SimpleAssociation は以下のようになる。
class SimpleAssociation<T extends AggregateRoot<T, ID>, ID extends Identifier> implements Association<T, ID> { private final Supplier<ID> identifier; SimpleAssociation(Supplier<ID> identifier) { this.identifier = identifier; } @Override public ID getId() { return identifier.get(); } // ... }
これだけだと分かりにくいので、jmolecules における、AggregateRoot や Entity の定義は、別記事で説明を加えます。