データアクセス基盤の概要
業務アプリケーションを構築する際、データベースとの連携を効率的かつ安全に行うための基盤整備は不可欠です。データの一貫性を保つためのトランザクション管理や、永続化ロジックの抽象化が求められます。ABP フレームワークは、特定のデータベースに依存せず、EF Core や MongoDB といった主要なプロバイダーとの統合を標準でサポートしています。
本稿では、ABP におけるデータアクセスの核心となるエンティティの定義方法、リポジトリパターンの活用、および単位作業(Unit of Work)の概念について解説します。具体的には、EF Core を利用した実装方針を中心に、ドメイン駆動設計(DDD)の観点から агрегат根や値オブジェクトの扱い方を学びます。
1. エンティティの定義戦略
ABP では、ドメインモデルを表現するクラスに対して、機能に応じた基底クラスが用意されています。これにより、共通のプロパティや振る舞いを標準化できます。
1.1 アグリゲートルートの設定
アグリゲートルートは、一貫性の境界となる主要なエンティティです。ABP では AggregateRoot クラス、またはその簡易版である BasicAggregateRoot クラスから派生させて定義します。
以下の例では、アンケート調査を表す Survey クラスを定義しています。主キーには Guid 型を採用しています。
namespace SurveySystem.Domain
{
public class Survey : BasicAggregateRoot<Guid>
{
public string Title { get; set; }
public string Description { get; set; }
public bool IsActive { get; set; }
public ICollection<SurveyQuestion> Questions { get; set; }
}
}
BasicAggregateRoot は主キー(Id)のみを定義する最小限のクラスです。必要に応じて、以下のような監査機能を備えた基底クラスを選択することも可能です。
AggregateRoot:楽観的ロックや拡張プロパティをサポート。CreationAuditedAggregateRoot:作成日時(CreationTime)と作成者 ID(CreatorId)を自動管理。AuditedAggregateRoot:作成情報に加え、更新日時(LastModificationTime)と更新者 ID を管理。FullAuditedAggregateRoot:削除日時(DeletionTime)と削除者 ID を追加し、ソフトデリート(ISoftDelete)を実装。
1.2 関連エンティティの定義
アグリゲートルートに紐づく子エンティティには、Entity クラスを使用します。これは集約の外部から直接参照されることを想定していない場合に適しています。
public class SurveyQuestion : Entity<Guid>
{
public Guid SurveyId { get; set; }
public string QuestionText { get; set; }
public bool IsMultipleChoice { get; set; }
}
このクラスもまた、監査機能付きの基底クラス(CreationAuditedEntity など)から派生させることができます。
1.3 複合主鍵の扱い
多対多の関係などを表現する際、複数の列を主鍵とする複合主鍵(Composite Primary Key)が必要になる場合があります。ABP では、非汎用型の Entity クラスを継承し、GetKeys メソッドをオーバーライドすることで対応します。
例えば、アンケートの管理者を管理する SurveyAssignment エンティティは以下のようになります。
public class SurveyAssignment : Entity
{
public Guid SurveyId { get; set; }
public Guid UserId { get; set; }
public bool IsOwner { get; set; }
public override object[] GetKeys()
{
return new object[] { SurveyId, UserId };
}
}
これにより、SurveyId と UserId の組み合わせが一意の識別子として機能します。
1.4 主キーに GUID を使用するメリット
ABP ではデフォルトで GUID を主キーに使用することを推奨しています。自動増分整数(int, long)と比較して、GUID には以下のような利点があります。
- 分散システムにおいてグローバルに一意であることを保証できる。
- データベースへの挿入前にクライアント側で ID を生成できる。
- ID からレコード数を推測されにくく、セキュリティ面で有利。
一方で、GUID はサイズが大きく、インデックスの断片化を引き起こす可能性があるという缺點もあります。しかし、ABP が提供する IGuidGenerator サービスを使用することで、順序性のある GUID を生成し、パフォーマンス上の問題を緩和できます。手動で Guid.NewGuid() を呼び出すのではなく、フレームワークの仕組みに任せることが推奨されます。
2. リポジトリパターンの実装
データアクセスロジックをドメイン層から分離するため、ABP は汎用リポジトリパターンを採用しています。
2.1 汎用リポジトリの注入
エンティティを定義すれば、特別な設定なしでそのエンティティ向けの汎用リポジトリを利用できます。依存性注入を通じて IRepository<TEntity, TKey> を取得します。
using Volo.Abp.Domain.Repositories;
using Volo.Abp.DependencyInjection;
namespace SurveySystem.Application
{
public class SurveyAppService : ITransientDependency
{
private readonly IRepository<Survey, Guid> _surveyRepository;
public SurveyAppService(IRepository<Survey, Guid> surveyRepository)
{
_surveyRepository = surveyRepository;
}
public async Task<List<Survey>> GetActiveSurveysAsync()
{
return await _surveyRepository.GetListAsync(s => s.IsActive);
}
}
}
原則として、リポジトリはアグリゲートルートに対して使用すべきですが、設定により他のエンティティでも利用可能になります。
2.2 基本的な CRUD 操作
汎用リポジトリは、非同期メソッドとして CRUD 操作を提供しています。
InsertAsync/InsertManyAsync:新規データの追加。UpdateAsync/UpdateManyAsync:既存データの更新。DeleteAsync/DeleteManyAsync:データの削除。
EF Core をバックエンドに使用している場合、これらの呼び出しは即時にデータベースに反映されず、変更追跡システムを通じて管理されます。実際の保存は、リクエスト終了時の単位作業(UoW)完了時、または autoSave: true を指定した場合に実行されます。
// 即時保存を行う場合
await _surveyRepository.InsertAsync(new Survey(), autoSave: true);
また、すべてのメソッドは CancellationToken を受け取り、長時間実行されるクエリのキャンセルに対応しています。
2.3 単一データの取得
ID または条件式に基づいて単一のエンティティを取得するには、以下のメソッドを使用します。
GetAsync:該当データがない場合に例外をスローします。FindAsync:該当データがない場合に null を返します。
ビジネスロジック上で「存在しないこと」が許容される場合は FindAsync を、必須である場合は GetAsync を使い分けます。
2.4 リスト取得とページネーション
複数のエンティティを取得するには GetListAsync を使用します。大量データを扱う場合は、GetPagedListAsync を利用してページネーション処理を行うことが可能です。
public async Task<List<Survey>> SearchSurveysAsync(string keyword)
{
return await _surveyRepository.GetListAsync(s => s.Title.Contains(keyword));
}
2.5 LINQ クエリの抽象化
複雑な查询を行う場合、IQueryable を直接操作することもできますが、ORM に依存しないようにするため、ABP は IAsyncQueryableExecuter を提供しています。
public class SurveyQueryService : ITransientDependency
{
private readonly IRepository<Survey, Guid> _surveyRepository;
private readonly IAsyncQueryableExecuter _queryExecuter;
public SurveyQueryService(
IRepository<Survey, Guid> surveyRepository,
IAsyncQueryableExecuter queryExecuter)
{
_surveyRepository = surveyRepository;
_queryExecuter = queryExecuter;
}
public async Task<List<Survey>> GetSortedSurveysAsync(string keyword)
{
var queryable = await _surveyRepository.GetQueryableAsync();
var query = from s in queryable
where s.Title.Contains(keyword)
orderby s.CreationTime descending
select s;
return await _queryExecuter.ToListAsync(query);
}
}
これにより、アプリケーション層が特定の ORM プロバイダー(例:EF Core の拡張メソッド)に結合されることを防ぎます。
2.6 複合主鍵を持つエンティティへのアクセス
複合主鍵を持つエンティティを扱う場合、IRepository<TEntity, TKey> の代わりに、キー型を指定しない IRepository<TEntity> を使用します。
public class AssignmentService : ITransientDependency
{
private readonly IRepository<SurveyAssignment> _assignmentRepository;
public AssignmentService(IRepository<SurveyAssignment> repository)
{
_assignmentRepository = repository;
}
public async Task<List<SurveyAssignment>> GetBySurveyAsync(Guid surveyId)
{
return await _assignmentRepository.GetListAsync(a => a.SurveyId == surveyId);
}
}
2.7 カスタムリポジトリの作成
汎用リポジトリで賄えない複雑なデータアクセスが必要な場合、カスタムリポジトリインターフェースを定義できます。これはドメインプロジェクト内に配置し、汎用インターフェースを継承するのが一般的です。
public interface ISurveyRepository : IRepository<Survey, Guid>
{
Task<List<Survey>> GetListByCategoryAsync(string category, bool includeInactive = false);
}
このインターフェースを実装するクラスを作成し、依存性注入に登録することで、独自のデータアクセスロジックをカプセル化できます。