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

Posted

技术标签:

【中文标题】C# MongoDB:如何正确映射域对象?【英文标题】:C# MongoDB: How to correctly map a domain object? 【发布时间】:2011-08-10 07:23:21 【问题描述】:

我最近开始阅读 Evans 的领域驱动设计书,并开始了一个小型示例项目,以获取一些 DDD 经验。同时,我想了解更多关于 MongoDB 的信息,并开始用 MongoDB 和最新的官方 C# 驱动程序替换我的 SQL EF4 存储库。 现在这个问题是关于 MongoDB 映射的。我看到使用公共 getter 和 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,并将它们映射到我的域对象吗?也许有人可以给我一些建议。

【问题讨论】:

【参考方案1】:

我会解析 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文档的解析在repository中,XML的解析在web service wrapper中。这将相关逻辑组合在一起。

一些缺点:

我还没有在大型复杂项目中尝试过这种方法,只在小型项目中尝试过。在一些我还没有遇到过的场景中可能会遇到一些困难。 这是一些看似简单的代码。尤其是建造者可以长得很大。您可以通过将所有 WithXxx() 方法转换为简单属性来减少构建器中的代码量。

【讨论】:

非常有趣的概念,我想除了添加一层之外别无他法。在 Evans 的书中的 c# DDD 演示应用程序中,他们使用了 nHibernate,我只是喜欢不必这样做的概念。您将未更改的域对象放入其中,然后在没有任何其他类的情况下将它们取回(当然,xml 映射除外)【参考方案2】:

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

我这样说是因为您正在尝试将 RDBMS 样式实体转换为 MongoDB,但它并没有像您发现的那样很好地映射。

使用任何 NoSQL 解决方案时要考虑的最重要的事情之一就是您的数据模型。您需要将大部分关于 SQL 和关系的知识解放出来,并更多地考虑嵌入式文档。

请记住,MongoDB 并不是解决所有问题的正确答案,因此请尽量不要强迫它这样做。您所遵循的示例可能适用于标准 SQL 服务器,但不要试图弄清楚如何使它们与 MongoDB 一起工作——他们可能不会。相反,我认为一个很好的练习是尝试找出使用 MongoDB 对示例数据进行建模的正确方法。

【讨论】:

但是有了 DDD,我完全不用担心持久性。我在设计我的域实体时没有考虑到某种存储技术。 @Malkier:正确,您不应该让持久性影响您的域设计。这就是为什么我认为MongoDB实际上是一个非常好的选择,因为它的存储模型要自然得多。使用 RDBMS,您必须将实体拆分,以便将它们存储在规范化的数据模型中。使用 MongoDB,您可以将整个聚合根存储在单个文档中,可能还包含对其他子实体的一些引用。 完全同意@Niels。但我认为值得一提的是,虽然理论上您不必担心持久层,但在现实世界中,这绝对是值得考虑的事情 - 很多,尤其是在考虑 NoSQL 解决方案时。【参考方案3】:

可以序列化/反序列化属性为只读的类。如果您试图让您的域对象持久性无知,您将不想使用 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

【讨论】:

行得通!我不敢相信我错过了这个。我猜对于不是那么大规模的应用程序,从私有字段中删除“只读”是一个可以接受的权衡,因为不必创建额外的 DTO 层。正如您所指出的,必须小心不要通过绕过 ctor 来构造无效对象。尽管如此,我还要感谢 Bryan 和 Niels 的回答。你们都让我变得更聪明了,谢谢。 小点:去掉readonly字段就相当于public ObjectId Id get; private set; auto-property,该字段可以全部去掉。 虽然这是一种可能的解决方案,但它仍然会影响您的模型,在您给出的具体示例中,您要求字段不是readonly。每个团队都必须自己决定这是否适合他们的项目。 @theDmi 我花了好几个小时试图弄清楚为什么我的反序列化不起作用......这是readonly 关键字......一旦我删除它,它就起作用了。太可悲了...我不想创建 DTO 类来序列化/反序列化为 Mongo ...尽管不喜欢删除 readonly,但我认为与其创建多个 DTO 类相比,这是值得的【参考方案4】:

考虑一下 NoRM,它是 C# 中用于 MongoDB 的开源 ORM。

这里有一些链接:

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(下载)

【讨论】:

【参考方案5】:

现在处理这个问题的更好方法是使用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);

【讨论】:

如果所有属性都显式映射,为什么还要使用AutoMap()

以上是关于C# MongoDB:如何正确映射域对象?的主要内容,如果未能解决你的问题,请参考以下文章

如何正确互连 Apollo Server/mongodb/express?

如何正确地将 XML 内容映射到 C# 中的 DTO 属性

MongoDB c#:关于分页的问题

Nhibernate - 如何使用 CompositeId 设计域对象和映射

MongoDB用户权限管理

MongoDB $lookup 仅替换对象数组中的 ID