C# MongoDB:如何正确映射领域对象?

32

最近我开始阅读Evans的领域驱动设计书籍,并开始了一个小样本项目,以获取一些DDD的经验。同时,我想学习更多关于MongoDB的知识,并开始用MongoDB和最新的官方C#驱动程序替换我的SQL EF4存储库。 现在,这个问题是关于MongoDB映射的。我发现很容易映射具有公共getter和setter的简单对象——没有难度。但是,如果一个领域实体没有公共setter,则我很难进行映射。据我所学,构造有效实体的唯一真正干净的方法是将所需参数传递给构造函数。请考虑以下示例:

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

很明显这里不能使用自动映射。我可以通过 BsonClassMaps 手动映射这三个类,它们将被正确存储。问题是,当我想从数据库中加载它们时,我必须将它们作为 BsonDocuments 加载,并将它们解析为我的领域对象。我尝试了很多方法,但最终未能找到一个简洁的解决方案。我真的需要为 MongoDB 生产具有公共 getter/setter 的 DTO 并将其映射到我的领域对象吗?也许有人可以给我一些建议。

5个回答

19

可以序列化/反序列化属性为只读的类。如果你试图保持你的领域对象与持久性无关,你不会想使用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序列化也是这样工作的)。

这是一个完整的样例程序来测试这个问题:

http://www.pastie.org/1822994


太好了!我简直不敢相信我错过了这个。我想对于规模不是很大的应用程序来说,从私有字段中删除“readonly”是一个可以接受的权衡,因为不需要创建额外的DTO层。正如你所指出的,人们必须小心,不要通过绕过构造函数来构造无效的对象。尽管如此,我还是要感谢Bryan和Niels的回答。你们都让我变得更聪明了,谢谢。 - hoetz
3
小提示:移除 readonly 关键字后,该字段等效于具有自动实现属性(auto-property)的 public ObjectId Id { get; private set; },此时可以将该字段完全移除。 - Zaid Masud
2
虽然这是一个可能的解决方案,但它仍会影响您的模型,在您给出的具体示例中,要求字段不是“只读”的。每个团队必须自行决定是否接受这种解决方案适用于他们的项目。 - theDmi
@theDmi 我花了好几个小时来弄清楚为什么我的反序列化不起作用...原来是readonly关键字...一旦我把它移除,它就可以工作了。这很遗憾...我不想创建DTO类来对Mongo进行序列化/反序列化...尽管我不喜欢移除readonly,但我认为这比创建多个DTO类更值得。 - JobaDiniz

4

我建议对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();
        }
    }
}

这种方法的优点:
  • The builder is responsible for building the domain object. This allows you to move some trivial validation out of the domain object, especially if the domain object doesn't expose any public constructors.
  • The factory is responsible for parsing the source data.
  • The domain object can focus on business rules. It's not bothered with parsing or trivial validation.
  • The abstract factory class defines a generic contract, which can be implemented for each type of source data you need. For example, if you need to interface with a web service that returns XML, you just create a new factory subclass:

    public class TransportWebServiceWrapper
    {
        private class TransportFactory : TransportFactory<XDocument>
        {
            protected override Transport Create(XDocument source, TransportBuilder builder)
            {
                // Construct domain object from XML.
            }
        }
    }
    
  • The parsing logic of the source data is close to where the data originates, i.e. the parsing of BSON documents is in the repository, the parsing of XML is in the web service wrapper. This keeps related logic grouped together.

一些缺点:

  • 我还没有在大型复杂项目中尝试过这种方法,只在小型项目中尝试过。在我还没有遇到的某些场景中可能会有一些困难。
  • 对于一个看似简单的东西来说,需要写相当多的代码。特别是建造者部分可能会变得十分庞大。您可以通过将所有的 WithXxx() 方法转换为简单的属性来减少建造者中的代码量。

非常有趣的概念,我想没有其他方法,只能添加一层。在Evans的书中的c# DDD演示应用程序中,他们使用了nHibernate,我非常喜欢不必这样做的概念。您只需投入未经修改的领域对象,就可以将它们返回而无需任何其他类(当然除了xml映射)。 - hoetz

3

现在更好的处理方法是使用MapCreator(可能是在大多数这些答案写出之后添加的)。

例如,我有一个名为Time的类,具有三个只读属性:HourMinuteSecond。下面是我如何将它们存储到数据库中,并在反序列化期间构造新的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);
}

4
如果所有属性都已明确映射,为什么要使用 AutoMap() - Eugene

0

Niels提出了一个有趣的解决方案,但我提出了一个截然不同的方法:简化您的数据模型。

我之所以这样说,是因为您正在尝试将RDBMS风格的实体转换为MongoDB,但它们并不很好地映射过来,正如您所发现的那样。

在使用任何NoSQL解决方案时,最重要的事情之一就是考虑您的数据模型。您需要摆脱对SQL和关系的许多了解,更多地考虑嵌入式文档。

请记住,MongoDB并不是每个问题的正确答案,因此请不要强迫它成为。您正在遵循的示例可能在标准SQL服务器上运行得很好,但不要费力地尝试弄清楚如何使它们与MongoDB配合工作-它们可能不会。相反,我认为一个好的练习是尝试找出使用MongoDB建模示例数据的正确方法。


5
但是在领域驱动设计中,我不需要担心持久化问题。我设计领域实体时不会考虑特定的存储技术。 - hoetz
@Malkier:没错,你不应该让持久化影响你的领域设计。这就是为什么我认为MongoDB实际上是一个非常好的选择,因为它的存储模型更加自然。使用关系型数据库,你必须拆分实体以便将它们存储在规范化的数据模型中。而使用MongoDB,你可以将整个聚合根存储在单个文档中,也许还带有一些对其他子实体的引用。 - Niels van der Rest
完全同意@Niels的观点。但我认为值得一提的是,虽然理论上你不必担心持久层,但在现实世界中,这绝对是需要考虑的事情 - 特别是在考虑NoSQL解决方案时。 - Bryan Migliorisi


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接