私は最近、Evansのドメイン駆動設計の本を読み始め、DDDの経験を積むために小さなサンプルプロジェクトを開始しました。同時に、MongoDBについてもっと知りたいと思い、SQL EF4リポジトリをMongoDBと最新の公式C#ドライバーに置き換え始めました。さて、この質問はMongoDBマッピングについてです。単純なオブジェクトをパブリックゲッターとセッターでマッピングするのは非常に簡単であることがわかります-そこに苦痛はありません。しかし、パブリックセッターなしでドメインエンティティをマッピングするのは困難です。私が学んだように、有効なエンティティを構築するための唯一の本当にクリーンなアプローチは、必要なパラメーターをコンストラクターに渡すことです。次の例を考えてみましょう。
public class Transport : IEntity<Transport>
{
private readonly TransportID transportID;
private readonly PersonCapacity personCapacity;
public Transport(TransportID transportID,PersonCapacity personCapacity)
{
Validate.NotNull(personCapacity, "personCapacity is required");
Validate.NotNull(transportID, "transportID is required");
this.transportID = transportID;
this.personCapacity = personCapacity;
}
public virtual PersonCapacity PersonCapacity
{
get { return personCapacity; }
}
public virtual TransportID TransportID
{
get { return transportID; }
}
}
public class TransportID:IValueObject<TransportID>
{
private readonly string number;
#region Constr
public TransportID(string number)
{
Validate.NotNull(number);
this.number = number;
}
#endregion
public string IdString
{
get { return number; }
}
}
public class PersonCapacity:IValueObject<PersonCapacity>
{
private readonly int numberOfSeats;
#region Constr
public PersonCapacity(int numberOfSeats)
{
Validate.NotNull(numberOfSeats);
this.numberOfSeats = numberOfSeats;
}
#endregion
public int NumberOfSeats
{
get { return numberOfSeats; }
}
}
明らかに、自動マッピングはここでは機能しません。これで、これら3つのクラスをBsonClassMaps
を介して手動でマップでき、問題なく保存されます。問題は、それらをDBからロードする場合、それらをBsonDocuments
としてロードし、ドメインオブジェクトに解析する必要があることです。私はたくさんのことを試みましたが、最終的にはきれいな解決策を得ることができませんでした。 MongoDBのパブリックゲッター/セッターを使用してDTOを作成し、それらをドメインオブジェクトにマップする必要がありますか?多分誰かが私にこれについていくつかのアドバイスを与えることができます。
プロパティが読み取り専用であるクラスをシリアル化/逆シリアル化することが可能です。ドメインオブジェクトの永続性を無視しないようにしようとしている場合は、BsonAttributesを使用してシリアル化をガイドする必要はありません。また、AutoMappingには読み取り/書き込みプロパティが必要であるため、クラスマップを自分で登録する必要があります。たとえば、クラス:
public class C {
private ObjectId id;
private int x;
public C(ObjectId id, int x) {
this.id = id;
this.x = x;
}
public ObjectId Id { get { return id; } }
public int X { get { return x; } }
}
次の初期化コードを使用してマッピングできます。
BsonClassMap.RegisterClassMap<C>(cm => {
cm.MapIdField("id");
cm.MapField("x");
});
プライベートフィールドは読み取り専用ではないことに注意してください。デシリアライズはコンストラクターをバイパスし、プライベートフィールドを直接初期化することにも注意してください(.NETシリアル化もこのように機能します)。
これをテストする完全なサンプルプログラムは次のとおりです。
BSONドキュメントの解析を行い、解析ロジックをファクトリに移動します。
まず、ビルダークラスを含むファクトリ基本クラスを定義します。ビルダークラスはDTOとして機能しますが、ドメインオブジェクトを構築する前に値の検証を追加します。
_public class TransportFactory<TSource>
{
public Transport Create(TSource source)
{
return Create(source, new TransportBuilder());
}
protected abstract Transport Create(TSource source, TransportBuilder builder);
protected class TransportBuilder
{
private TransportId transportId;
private PersonCapacity personCapacity;
internal TransportBuilder()
{
}
public TransportBuilder WithTransportId(TransportId value)
{
this.transportId = value;
return this;
}
public TransportBuilder WithPersonCapacity(PersonCapacity value)
{
this.personCapacity = value;
return this;
}
public Transport Build()
{
// TODO: Validate the builder's fields before constructing.
return new Transport(this.transportId, this.personCapacity);
}
}
}
_
次に、リポジトリにファクトリサブクラスを作成します。このファクトリは、BSONドキュメントからドメインオブジェクトを構築します。
_public class TransportRepository
{
public Transport GetMostPopularTransport()
{
// Query MongoDB for the BSON document.
BsonDocument transportDocument = mongo.Query(...);
return TransportFactory.Instance.Create(transportDocument);
}
private class TransportFactory : TransportFactory<BsonDocument>
{
public static readonly TransportFactory Instance = new TransportFactory();
protected override Transport Create(BsonDocument source, TransportBuilder builder)
{
return builder
.WithTransportId(new TransportId(source.GetString("transportId")))
.WithPersonCapacity(new PersonCapacity(source.GetInt("personCapacity")))
.Build();
}
}
}
_
このアプローチの利点:
抽象ファクトリクラスは、必要なソースデータのタイプごとに実装できる汎用コントラクトを定義します。たとえば、XMLを返すWebサービスとインターフェイスする必要がある場合は、新しいファクトリサブクラスを作成するだけです。
_public class TransportWebServiceWrapper
{
private class TransportFactory : TransportFactory<XDocument>
{
protected override Transport Create(XDocument source, TransportBuilder builder)
{
// Construct domain object from XML.
}
}
}
_
ソースデータの解析ロジックは、データの発信元に近いです。つまり、BSONドキュメントの解析はリポジトリにあり、XMLの解析はWebサービスラッパーにあります。これにより、関連するロジックがグループ化されたままになります。
いくつかの欠点:
WithXxx()
メソッドを単純なプロパティに変換することで、ビルダー内のコードの量を減らすことができます。これを処理するためのより良いアプローチは、MapCreator
を使用することです(これらの回答のほとんどが書かれた後に追加された可能性があります)。
例えばTime
、Hour
、Minute
の3つの読み取り専用プロパティを持つSecond
というクラスがあります。これらの3つの値をデータベースに格納し、逆シリアル化中に新しいTime
オブジェクトを構築する方法を次に示します。
BsonClassMap.RegisterClassMap<Time>(cm =>
{
cm.AutoMap();
cm.MapCreator(p => new Time(p.Hour, p.Minute, p.Second));
cm.MapProperty(p => p.Hour);
cm.MapProperty(p => p.Minute);
cm.MapProperty(p => p.Second);
}
C#のMongoDB用のオープンソースORMであるNoRMについて考えてみましょう。
ここにいくつかのリンクがあります:
http://www.codevoyeur.com/Articles/20/A-NoRM-MongoDB-Repository-Base-Class.aspx
http://lukencode.com/2010/07/09/getting-started-with-mongodb-and-norm/
https://github.com/atheken/NoRM (ダウンロード)