Repositoryは何を返す?
基本はAggregate Rootを返す
取得したデータを使って業務上の変更を行う場合、Repositoryは基本的にAggregate Rootを返します。Aggregate RootはEntityなので、結果としてEntityを返すことになります。
フットサルNOWの募集用Repositoryは、「募集」のAggregate RootであるRecruitmentEntityを返します。募集が存在しない場合はnullを返します。
参加申請を受け付けるには、「申込期限を過ぎていないか」「同じ人が申し込んでいないか」「定員に達していないか」を確認する必要があります。これらのルールと現在の状態を持つ「募集」をEntityとして復元することで、requestParticipation()を呼び、ルールを守った状態だけを保存できます。
export interface IRecruitmentRepository {
findById(
recruitmentId: RecruitmentIdValueObject,
): Promise<RecruitmentEntity | null>;
save(recruitment: RecruitmentEntity): Promise<void>;
}DBの行やAggregate内部の値だけを返すと、Application Serviceが業務ルールを確認して状態を変更することになります。ルールをAggregate Rootに集めるため、更新ではAggregate Rootを取得します。
Entity以外を返す場合
取得したデータを一覧表示、検索、集計に使うだけで、業務上の変更を行わない場合は、Entityでなくても構いません。業務ルールを実行する振る舞いや、Aggregate全体の状態が必要ないためです。
この場合は、利用側に必要な項目だけを持つRead Modelを返します。読み取り処理は、更新用のRepositoryとは分けてQueryとして定義します。
例えば、近くで参加できる募集の一覧には、募集のすべての状態や振る舞いは必要ありません。「募集ID」「タイトル」「場所」「開催日時」「残り枠」だけを返せます。
// 募集一覧へ表示する値
export type RecruitmentListItem = {
readonly recruitmentId: string;
readonly title: string;
readonly locationName: string;
readonly startsAt: Date;
readonly remainingCapacity: number;
};
// 募集一覧を取得するためのApplication層の窓口
export interface IRecruitmentQuery {
findNearby(input: {
latitude: number;
longitude: number;
}): Promise<readonly RecruitmentListItem[]>;
}RecruitmentListItemはEntityではありません。募集一覧へ返す値を表すRead Modelです。
読み取りにもEntityを使ってはいけないわけではありません。単純な取得でRepositoryから返したEntityをそのまま使えるなら、無理にQueryとRead Modelを増やす必要はありません。必要な項目や検索条件がEntityの形と合わなくなったときに分けます。
DAOと型をどこに置くか
Application Serviceが使うRead ModelとQuery interfaceは、Application層に置きます。利用側が必要とする返却値だからです。
DBから読み取った行やORMの型は、DAOやQueryの実装と同じInfrastructure層に置きます。DBの型をそのままApplication層へ返さず、Read Modelへ変換します。
// Infrastructure層だけで使うDBの行
type RecruitmentListRow = {
recruitment_id: string;
title: string;
location_name: string;
starts_at: Date;
remaining_capacity: number;
};
export class RecruitmentQuery implements IRecruitmentQuery {
async findNearby(input: {
latitude: number;
longitude: number;
}): Promise<readonly RecruitmentListItem[]> {
const rows: RecruitmentListRow[] = await this.dao.findNearby(input);
// DBの行を、Application層へ返すRead Modelに変換する
return rows.map((row) => ({
recruitmentId: row.recruitment_id,
title: row.title,
locationName: row.location_name,
startsAt: row.starts_at,
remainingCapacity: row.remaining_capacity,
}));
}
}DAOという名前を使う場合も考え方は同じです。DAOはDBへのアクセスを担当し、DBの行を表す型はDAOの近くに置きます。Application層へ公開する型はDAOではなく、Application層に定義します。
返却値の決め方
取得後に業務ルールを使って変更する
RepositoryからAggregate Rootを返します。
一覧・検索・集計の結果を表示する
QueryからRead Modelを返します。DBアクセスをDAOへ分ける場合は、Queryの実装からDAOを呼びます。
存在するか、何件あるかだけを確認する
用途が明確なら、Repositoryからbooleanやnumberを返すメソッドも定義できます。
ディレクトリで表す
src/features/futsal/
├── application/
│ └── query/
│ ├── IRecruitmentQuery.ts
│ └── RecruitmentListItem.ts
├── domain/
│ └── recruitment/
│ ├── RecruitmentEntity.ts
│ └── IRecruitmentRepository.ts
└── infrastructure/
├── query/
│ └── RecruitmentQuery.ts
└── repository/
└── RecruitmentRepository.ts更新ではRepositoryとAggregate Rootを使い、読み取りではQueryとRead Modelを使います。DAOを分ける場合は、Infrastructure層のQuery実装から呼び出します。