C # MongoDB: Как правильно сопоставить объект домена? - PullRequest
26 голосов
/ 21 апреля 2011

Я недавно начал читать книгу Эванса о доменном дизайне и начал небольшой пример проекта, чтобы получить некоторый опыт работы с 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; }
    }
}

Очевидно, что автоматическое отображение здесь не работает.Теперь я могу сопоставить эти три класса вручную через BsonClassMaps, и они будут храниться очень хорошо.Проблема в том, что когда я хочу загрузить их из БД, я должен загрузить их как BsonDocuments и проанализировать их в моем доменном объекте.Я много чего перепробовал, но в итоге не смог найти чистого решения.Действительно ли мне нужно создавать DTO с общедоступными средствами получения / установки для MongoDB и отображать их на мои доменные объекты?Может быть, кто-нибудь может дать мне совет по этому поводу.

Ответы [ 5 ]

15 голосов
/ 22 апреля 2011

Возможно сериализовать / десериализовать классы, где свойства доступны только для чтения. Если вы пытаетесь сохранить постоянство невосприимчивости объектов вашего домена, вы не захотите использовать 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

3 голосов
/ 21 апреля 2011

Я бы пошел с разбором документов 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, вы просто создаете новый фабричный подкласс:

    public class TransportWebServiceWrapper
    {
        private class TransportFactory : TransportFactory<XDocument>
        {
            protected override Transport Create(XDocument source, TransportBuilder builder)
            {
                // Construct domain object from XML.
            }
        }
    }
    
  • Логика синтаксического анализа исходных данных близка к тому, гдеданные происходят, то есть синтаксический анализ документов BSON находится в хранилище, синтаксический анализ XML - в оболочке веб-службы.Это позволяет сгруппировать связанную логику.

Некоторые недостатки:

  • Я еще не пробовал этот подход в больших и сложных проектах, только в небольшихпроекты.В некоторых сценариях могут возникнуть некоторые трудности, с которыми я еще не сталкивался.
  • Это довольно некий код для чего-то, казалось бы, простого.Особенно строители могут вырасти довольно большими.Вы можете уменьшить количество кода в сборщиках, преобразовав все методы WithXxx() в простые свойства.
2 голосов
/ 26 октября 2015

Лучшим подходом к решению этой проблемы сейчас является использование MapCreator (которое, возможно, было добавлено после того, как было написано большинство этих ответов).

например. У меня есть класс с именем Time с тремя свойствами только для чтения: Hour, Minute и Second. Вот как я могу сохранить эти три значения в базе данных и создать новые 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);
}
0 голосов
/ 23 апреля 2011

Рассмотрим NoRM, ORM с открытым исходным кодом для MongoDB в C #.

Вот несколько ссылок:

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 (скачать)

0 голосов
/ 22 апреля 2011

У Niels есть интересное решение, но я предлагаю совершенно другой подход: Упростите вашу модель данных.

Я говорю это потому, что вы пытаетесь преобразовать сущности в стиле RDBMS в MongoDB, а это не так.очень хорошо сопоставьте, как вы уже нашли.

Одна из самых важных вещей, о которой следует помнить при использовании любого решения NoSQL, - это ваша модель данных.Вам нужно освободить свой разум от большей части того, что вы знаете о SQL и отношениях, и больше думать о встраиваемых документах.

И помните, MongoDB - это не правильный ответ для каждой проблемы, поэтому постарайтесь не заставлять его.Примеры, которые вы используете, могут отлично работать со стандартными серверами SQL, но не убивайте себя, пытаясь понять, как заставить их работать с MongoDB - они, вероятно, этого не делают.Вместо этого я думаю, что хорошим упражнением будет попытка найти правильный способ моделирования данных примера с MongoDB.

...