はじめに
この記事は、以下の記事の補足説明です。
識別子による外部の集約の参照
集約ルートである以下のような 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
の定義は、別記事で説明を加えます。