LoginSignup
0

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-21
// 実行環境
* 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);
    }
}

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

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

image.png

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

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);
    }
}

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
0