外部集約ルート参照の関連を表現する Association オブジェクト


はじめに

この記事は、以下の記事の補足説明です。

blog1.mammb.com


識別子による外部の集約の参照

集約ルートである以下のような Order がある。

@Entity
public class Order {
    @Id
    private Long id;
    @OneToMany
    private List<LineItem> lineItems;
    @ManyToOne
    private Customer customer;
}

注文明細である lineItemOrder の集約に属するが、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 における、AggregateRootEntity の定義は、別記事で説明を加えます。