ArchUnit 実践:集約操作専用のリポジトリ(やDAO)によってのみ、集約が永続化されることを強制する①<個別 ver.>


// 実行環境
* AdoptOpenJDK 11.0.9.1+1
* JUnit 5.7.0
* ArchUnit 0.14.1

アーキテクチャテストのモチベーション

集約を構成するオブジェクトは、データベース等の永続化層から、個別に参照や更新するのではなく、集約ルートを起点として集約(オブジェクトのまとまり)としての整合性を保ちながら、参照や更新したい。

アーキテクチャテストの実装

テスト対象の集約とクラスのサンプルは後述。

package com.example;
 
import com.example.domain.order.DenpyoAggregateDao;
import com.example.domain.order.DenpyoDao;
import com.example.domain.order.MeisaiDao;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.theClass;

class ArchitectureTest {

    // 検査対象のクラス
    private static final JavaClasses CLASSES =
            new ClassFileImporter()
                    .withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
                    .importPackages("com.example");

    @ParameterizedTest
    @ValueSource(classes = {
        // 集約を構成する各エンティティに対応するDAO
        DenpyoDao.class,
        MeisaiDao.class
    })
    void 集約を構成する各エンティティに対応するDAOは集約操作専用のDAOによってのみ操作される(
        final Class<?> daoClass
    ) {
        // 集約操作専用のDAO
        Class<DenpyoAggregateDao> aggregateDaoClass = DenpyoAggregateDao.class;

        theClass(daoClass)
            .should()
            .onlyBeAccessed()
                .byClassesThat()
                .haveFullyQualifiedName(aggregateDaoClass.getName())
            .check(CLASSES);
    }
}

(参考)テスト対象の集約のサンプル

以下のような集約があるとする。

エンティティの識別子を表す値オブジェクト

public final class Identity<ENTITY> {
    //...
}

伝票エンティティと Dao

@Entity
public class Denpyo {
    @Id
    Identity<Denpyo> id;

    @Transient
    List<Meisai> meisaiList;
}

public interface DenpyoDao {
    Optional<Denpyo> findById(Identity<Denpyo> id);
    int insert(Denpyo denpyo);
    int update(Denpyo denpyo);
}

明細エンティティと Dao

@Entity
public class Meisai {
    @Id
    Identity<Meisai> id;

    Identity<Denpyo> denpyoId;
}

public interface MeisaiDao {
    List<Meisai> findByDenpyoId(Identity<Denpyo> denpyoId);
    int[] insert(List<Meisai> meisaiList);
    int[] update(List<Meisai> meisaiList);
}

伝票・明細を集約として操作する Aggregate Dao

public class DenpyoAggregateDao {

    private final DenpyoDao denpyoDao;
    private final MeisaiDao meisaiDao;

    public DenpyoAggregateDao(final DenpyoDao denpyoDao, final MeisaiDao meisaiDao) {
        this.denpyoDao = denpyoDao;
        this.meisaiDao = meisaiDao;
    }

    // 集約の取得
    Optional<Denpyo> findById(final Identity<Denpyo> id) {
        return denpyoDao.findById(id).map(denpyo -> {
            denpyo.meisaiList = meisaiDao.findByDenpyoId(denpyo.id);
            return denpyo;
        });
    }

    // 集約の登録
    @Transactional
    void insert(final Denpyo denpyo) {
        assert denpyo.meisaiList != null;

        denpyoDao.insert(denpyo);
        // `伝票ID`がデータベース等により自動採番される場合はその値を`明細.伝票ID`に反映する
        // denpyo.meisaiList.forEach(meisai -> meisai.denpyoId = denpyo.id);
        meisaiDao.insert(denpyo.meisaiList);
    }

    // 集約の更新
    @Transactional
    void update(final Denpyo denpyo) {
        assert denpyo.meisaiList != null;

        denpyoDao.update(denpyo);
        meisaiDao.update(denpyo.meisaiList);
    }
}