Подтвердить что ты не робот

С# MongoDB: Как правильно отобразить объект домена?

Недавно я начал читать книгу дизайна Evans под управлением домена и начал небольшой пробный проект, чтобы получить некоторый опыт работы с DDD. В то же время я хотел узнать больше о MongoDB и начал заменять свои репозитории SQL EF4 на MongoDB и последним официальным драйвером С#. Теперь этот вопрос касается отображения 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 с общедоступными getter/seters для MongoDB и сопоставлять их с моими объектами домена? Может быть, кто-то может дать мне несколько советов по этому поводу.

4b9b3361

Ответ 1

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

Ответ 2

Я бы пошел с разбором документов BSON и переместил логику синтаксического анализа на factory.

Сначала определите базовый класс factory, который содержит класс строителя. Класс строителя будет действовать как 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 fields before constructing.

            return new Transport(this.transportId, this.personCapacity);
        }
    }
}

Теперь создайте подкласс factory в вашем репозитории. Этот factory будет создавать объекты домена из документов 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();
        }
    }
}

Преимущества такого подхода:

  • Строитель отвечает за создание объекта домена. Это позволяет вам перенести некоторую тривиальную проверку вне объекта домена, особенно если объект домена не предоставляет публичных конструкторов.
  • factory отвечает за разбор исходных данных.
  • Объект домена может сосредоточиться на бизнес-правилах. Это не мешало разбору или тривиальной проверке.
  • Абстрактный factory класс определяет общий контракт, который может быть реализован для каждого типа исходных данных, которые вам нужны. Например, если вам нужно взаимодействовать с веб-службой, которая возвращает XML, вы просто создаете новый подкласс factory:

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

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

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

Ответ 3

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

например. У меня есть класс с именем Time с тремя свойствами readonly: 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);
}

Ответ 5

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

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

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

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