問題の声明
この記事では、当社のプロジェクトの1つで実装されているDTO作成メカニズムについて説明します。 このプロジェクトは、サーバー部分と複数のクライアント(Silverlight、Outlook、iPad)で構成されています。 サーバーは、WCFに実装された一連のサービスです。 サービスがあるため、サービスとデータを交換する必要があります。 クライアントがドメインドメインエンティティを認識してサーバーから受信する場合、このオプションはいくつかの理由ですぐに消えました。
- すべてのクライアントが.NETに実装されているわけではありません。
- 複雑なオブジェクトグラフのシリアル化の問題の可能性
- データの冗長性
原則として、これらの欠点はすべて長い間知られており、賢い人たちはそれらを修正するためにデータ転送オブジェクト (DTO)パターンを思いつきました。 つまり、ドメインドメインエンティティクラスはサーバーのみが認識し、クライアントはDTOクラスを操作し、同じクラスのインスタンスをサービスと交換します。 理論的にはすべてが問題ありませんが、実際には、DTOを作成し、その中のエンティティからデータを記録するという問題があります。 小さなプロジェクトでは、=演算子はこのジョブでうまく機能します。 しかし、プロジェクトの規模が拡大し始め、コードのパフォーマンスと保守性の要件が高まると、より柔軟なソリューションの必要性が生じます。 以下では、DTOの作成と設定に使用するメカニズムの進化について説明します。
ドメインモデルの例
より具体的な説明のために、テストドメインモデルを定義します。 アプリケーションでmet石を追跡するとします。 次の主なタイプがあります。
/// <summary> /// . /// </summary> public class Entity { /// <summary> /// . /// </summary> public Guid Id { get; set; } } /// <summary> /// . /// </summary> public abstract class Meteor: Entity { /// <summary> /// . /// </summary> public string Name { get; set; } /// <summary> /// . /// </summary> public double Weight { get; set; } /// <summary> /// . /// </summary> public Material Material { get; set; } /// <summary> /// . /// </summary> public double DistanceToEarth { get; set; } /// <summary> /// . /// </summary> public RiskLevel RiskLevel { get; set; } } /// <summary> /// . /// </summary> public class SpaceMeteor: Meteor { /// <summary> /// / . /// </summary> public DateTime DetectedAt { get; set; } /// <summary> /// . /// </summary> public Person DetectingPerson { get; set; } /// <summary> /// , . /// </summary> public Galaxy PlaceOfOrigin { get; set; } } /// <summary> /// . /// </summary> public class ArtificialMeteor: Meteor { /// <summary> /// -. /// </summary> public Country Country { get; set; } /// <summary> /// -. /// </summary> public SecretFactory Maker { get; set; } /// <summary> /// . /// </summary> public string SerialNumber { get; set; } /// <summary> /// . /// </summary> public Person QualityEngineer { get; set; } }
対応するドメインクラスに対して、DTOクラスを作成します。 このプロジェクトでは、完全にフラットなDTOを使用していることに注意してください。
/// <summary> /// DTO. /// </summary> public class BaseDto { public Guid Id { get; set; } } /// <summary> /// DTO . /// </summary> public abstract class MeteorDto: BaseDto { public string Name { get; set; } public double Weight { get; set; } public string MaterialName { get; set; } public Guid? MaterialId { get; set; } public double DistanceToEarth { get; set; } public string RiskLevelName { get; set; } public Guid RiskLevelId { get; set; } } /// <summary> /// DTO . /// </summary> public class SpaceMeteorDto: MeteorDto { public DateTime DetectedAt { get; set; } public string DetectingPersonName { get; set; } public Guid DetectingPersonId { get; set; } public string PlaceOfOriginName { get; set; } public Guid? PlaceOfOriginId { get; set; } } /// <summary> /// DTO . /// </summary> public class ArtificialMeteorDto: MeteorDto { public string CountryName { get; set; } public Guid CountryId { get; set; } public string MakerName { get; set; } public string MakerAddress { get; set; } public string MakerDirectorName { get; set; } public Guid MakerId { get; set; } public string SerialNumber { get; set; } public string QualityEngineerName { get; set; } public Guid QualityEngineerId { get; set; } }
メカニズムNo. 1-データベースのエンティティ、次にエンティティのDTO
問題への最初のアプローチは、インターフェースの作成をもたらしました
interface IDtoMapper<TEntity, TDto> { IEnumerable<TDto> Map(IEnumerable<TEntity> entities); }
エンティティとDTOのペアごとに、対応する型によって閉じられるインターフェイスを実装するクラスが作成されました。 マッピングはAutomapperを介して実装されました 。これにより、プロパティのルーチン割り当てを取り除き、プロパティの命名がAutomapperが使用する合意に該当しない場合のみを明示的に記述することができました。 仕組み:
- クライアントがWCF操作を呼び出してデータセットを取得します
- 特定の基準に従ったエンティティがデータベースからダウンロードされ、IDtoMapperに転送されます
- IDtoMapperはDTOインスタンスを作成し、エンティティからDTOにデータをコピーします。
- DTOコレクションが顧客に返されます。
このメカニズムは機能し、負荷が増加してパフォーマンスの問題が発生するまで非常に満足していました。 測定の結果、犯人の1人は古いIDtoMapperでした。 (私たちは彼をtraditionりません。アジャイルの最高の伝統では、彼は製品を迅速にリリースし、作業中にそれをよりよく理解し、得られた経験に基づいて個々の部分を書き換えるのを助けました。)問題はデータベースからより多くのデータが抽出されたことでしたDTOに記入する必要がありました。 オブジェクト間の関連付けと集約により、多数の結合が発生し、システムの速度に悪影響を及ぼしました。
この問題を解決するための2つのオプションを検討しました。データ抽出戦略(遅延ロードまたは高速ロードなど)を思いつくか、データベースから直接DTOを抽出します。 2番目のパスは、最もシンプルで生産的で柔軟なものとして選択されました。 ここでは、ORMとしてNHibernateを使用していることに注意してください。データベースクエリはLINQを介して行われます。 以下はすべて、Entity Frameworkでも機能します。
メカニズムNo. 2-データベースからのDTO
次のインターフェースが作成されました。
interface IDtoFetcher<TEntity, TDto> { IEnumerable<TDto> Fetch(IQueryable<TEntity> query, Paging paging, FetchAim fetchAim); }
このメソッドは、1つではなく3つのパラメーターを受け入れます。
- クエリ-エンティティタイプごとのLINQクエリ
- ページング-抽出されたページに関する情報
- fetchAim-抽出ターゲット
DTOマッピングメカニズムは完全に書き直されたため、一度に複数の方向で最適化することが決定されました。 それらの1つは、抽出目標の概念でした。 クライアント上の異なるフォームの場合、DTOには異なるプロパティが設定されている必要があります。 たとえば、ドロップダウンリストから選択するには、名前と識別子のみが必要です。 レジストリに表示するには、テキストプロパティを設定する必要があります。 カードには別のデータセットが必要です。 したがって、Fetchメソッドは、DTO抽出の目的を決定するパラメーターを受け取り、それに応じて、必要なフィールドのみがデータベースからロードされます。
/// <summary> /// DTO. /// </summary> public enum FetchAim { /// <summary> /// /// </summary> None, /// <summary> /// /// </summary> Card, /// <summary> /// /// </summary> List, /// <summary> /// /// </summary> Index }
抽象化が特定され、それは実装の順番でした。 SQLでフィールドを選択的に選択するには、たとえば次のような投影法が使用されます。
SELECT (id, name) FROM meteors
LINQでは、Select()メソッドを使用して投影が実装されます。 次のLINQクエリを実行すると、上記のSQLクエリが生成されます。
IQueryable<Meteor> meteorQuery = _meteorRepository.Query(); IEnumerable<MeteorDto> meteors = meteorQuery .Select(m => new MeteorDto { Id = m.Id, Name = m.Name }) .ToList();
このLINQ機能について学習した後、IDtoFetcherの特定の実装を作成するために一生懸命働き始めました。
class SpaceMeteorDtoFetcher: IDtoFetcher<SpaceMeteor, SpaceMeteorDto> { public IEnumerable<SpaceMeteorDto> Fetch(IQueryable<SpaceMeteor> query, Page page, FetchAim fetchAim) { if (fetchAim == FetchAim.Index) { return query .Select(m => new SpaceMeteorDto { Id = m.Id, Name = m.Name }) .Page(page) .ToList(); } else if (fetchAim == FetchAim.List) { // ... } // ... } }
しかし、2番目のクラスの後、怠の突然の攻撃がありました(そして、このアプローチは、コードの大規模な複製と、システムのさらなるメンテナンスと新しいエンティティの追加に大きな困難をもたらすという認識)。 たとえば、基本クラスの相続人をマッピングする場合、すべての相続人は共通プロパティの初期化で行を繰り返す必要があります。 また、1つのエンティティをさまざまな抽出目的にマッピングするときに複製が発生します。 そして、単純なロシア語の単語が思わず私の頭に浮かびました: 式の木 ...
LINQクエリは式ツリーであり、これが解析されてSQLクエリが生成されるため、エンティティプロパティのDTOプロパティへのマッピングを記述し、この情報から必要なLINQクエリを構築できる宣言メカニズムを作成することにしました。
実装
実装(.NET 4.0、NHibernate 3.3.2、Visual Studio 2012)を含むプロジェクトのソースコードはこちらです。
式ツリーとの不平等な闘争に参加しなければならなかった理由を理解するために、特定のクラスのフェッチャーの構成がどのように実行されるかの例を示します。
/// <summary> /// DTO . /// </summary> public class SpaceMeteorDtoFetcher: BaseMeteorDtoFetcher<SpaceMeteor, SpaceMeteorDto> { static SpaceMeteorDtoFetcher() { CreateMapForIndex(); CreateMapForList(); CreateMapForCard(); } private static void CreateMapForIndex() { var map = CreateFetchMap(FetchAim.Index); // MapBaseForIndex(map); } private static void CreateMapForList() { var map = CreateFetchMap(FetchAim.List); // MapBaseForList(map); MapSpecificForList(map); } /// <summary> /// . /// </summary> /// <param name="map"> </param> private static void MapSpecificForList(IFetchMap<SpaceMeteor, SpaceMeteorDto> map) { map.Map(d => d.DetectedAt, e => e.DetectedAt) .Map(d => d.DetectingPersonName, e => e.DetectingPerson.FullName) .Map(d => d.PlaceOfOriginName, e => e.PlaceOfOrigin.Name); } private static void CreateMapForCard() { var map = CreateFetchMap(FetchAim.Card); MapBaseForCard(map); MapSpecificForCard(map); } /// <summary> /// . /// </summary> /// <param name="map"> </param> private static void MapSpecificForCard(IFetchMap<SpaceMeteor, SpaceMeteorDto> map) { map.Map(d => d.DetectedAt, e => e.DetectedAt) .Map(d => d.DetectingPersonId, e => e.DetectingPerson.Id) .Map(d => d.PlaceOfOriginId, e => e.PlaceOfOrigin.Id); } public SpaceMeteorDtoFetcher(IRepository repository) : base(repository) { } }
マッピング抽象化は、フェッチャーを構成するために使用されます。
public interface IFetchMap<TSource, TTarget> where TSource : Entity where TTarget : BaseDto { /// <summary> /// DTO. /// </summary> IFetchMap<TSource, TTarget> Map<TProperty>( Expression<Func<TTarget, TProperty>> targetProperty, Expression<Func<TSource, TProperty>> sourceProperty); /// <summary> /// DTO, . /// </summary> IFetchMap<TSource, TTarget> CustomMap(Action<IQueryable<TSource>, IEnumerable<TTarget>> fetchOperation); }
構成の方法:特定の抽出目的のマッピングオブジェクトを作成し、Mapメソッドを呼び出して、データベースからロードするプロパティを示します。 CustomMapメソッドは拡張ポイントとして使用されます-渡されたデリゲートで、データベースからデータを手動でロードし、抽出されたDTOに書き込むためのロジックを指定できます。
DTO eo石Fetcherの基本クラスBaseMeteorDtoFetcherは、基本クラスのプロパティをマッピングするためのメソッドを提供します。この方法により、重複を避け、新しいタイプのmet石のフェッチャーの作成を高速化します。 BaseMeteorDtoFetcher自体は、BaseDtoFetcherから継承します。BaseDtoFetcherは、タイプIFetchMapの作成済みオブジェクトのコレクションを格納し、それらを使用してDTOを取得します。
新しい抽象化が追加され、確立された伝統によると、その実装が必要です。 (実際、人生では逆でした。最初に特定のクラスが出現し、次にそこからインターフェースが抽出されました。)実装はFetchMapクラスによって表されます。 その中に、式ツリーを操作するロジック全体が配置されています。 この記事は非常に大きいため、ここではFetchMapの実装を段階的に分析しません。 添付のプロジェクトで確認できます。 理解するには、式ツリーをある程度理解する必要があります。
おわりに
したがって、現時点では、データベースからDTOを最適に取得できるメカニズムがあり、マッピング設定の宣言構文があり、作成を簡素化できます。 新しいエンティティとDTOの拡張の速度と容易さの点で私たちに適しています。
上記の経験により、一部の読者がプロジェクトの実施中に安全に収集したレーキを回避できることを願っています。 そして、これらすべてがより簡単かつ柔軟に実現できる方法を誰かが知っていれば、それについて聞いてうれしいです。 ご清聴ありがとうございました!