MVC и NOSQL: сохранение моделей представления непосредственно в MongoDB?

Я понимаю, что «правильная» структура для разделения интересов в MVC состоит в том, чтобы иметь модели представления для структурирования ваших представлений и отдельные модели данных для сохранения в выбранном вами репозитории. Я начал экспериментировать с MongoDB и начинаю думать, что это может не применяться при использовании базы данных без схемы в стиле NO-SQL. Я хотел представить этот сценарий сообществу stackoverflow и узнать, что все думают. Я новичок в MVC, поэтому для меня это имело смысл, но, возможно, я что-то упускаю из виду...

Вот мой пример для этого обсуждения: когда пользователь хочет отредактировать свой профиль, он переходит к представлению UserEdit, в котором используется модель UserEdit, представленная ниже.

public class UserEditModel
{
    public string Username
    {
        get { return Info.Username; }
        set { Info.Username = value; }
    }

    [Required]
    [MembershipPassword]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [DisplayName("Confirm Password")]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }

    [Required]
    [Email]
    public string Email { get; set; }

    public UserInfo Info { get; set; }
    public Dictionary<string, bool> Roles { get; set; }
}

public class UserInfo : IRepoData
{
    [ScaffoldColumn(false)]
    public Guid _id { get; set; }

    [ScaffoldColumn(false)]
    public DateTime Timestamp { get; set; }

    [Required]
    [DisplayName("Username")]
    [ScaffoldColumn(false)]
    public string Username { get; set; }

    [Required]
    [DisplayName("First Name")]
    public string FirstName { get; set; }

    [Required]
    [DisplayName("Last Name")]
    public string LastName { get; set; }

    [ScaffoldColumn(false)]
    public string Theme { get; set; }

    [ScaffoldColumn(false)]
    public bool IsADUser { get; set; }
}

Заметили, что класс UserEditModel содержит экземпляр UserInfo, который наследуется от IRepoData? UserInfo — это то, что сохраняется в базе данных. У меня есть общий класс репозитория, который принимает любой объект, наследующий форму IRepoData, и сохраняет его; поэтому я просто звоню Repository.Save(myUserInfo) и все готово. IRepoData определяет _id (соглашение об именовании MongoDB) и временную метку, поэтому репозиторий может обновляться на основе _id и проверять наличие конфликтов на основе временной метки и любых других свойств, которые объект только что сохранил в MongoDB. Представление, по большей части, просто нужно использовать @Html.EditorFor, и мы готовы к работе! По сути, все, что нужно только представлению, входит в базовую модель, все, что нужно только репозиторию, просто получает аннотацию [ScaffoldColumn(false)], а все остальное является общим между ними. (Кстати, имя пользователя, пароль, роли и адрес электронной почты сохраняются в поставщиках .NET, поэтому их нет в объекте UserInfo.)

У этого сценария есть два больших преимущества...

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

  2. Я могу рефакторить за секунды... Если мне нужно добавить второй адрес электронной почты, я просто добавляю его в объект UserInfo — он добавляется в представление и сохраняется в репозитории, просто добавляя одно свойство объекта. Поскольку я использую MongoDB, мне не нужно изменять схему моей базы данных или возиться с какими-либо существующими данными.

При такой настройке есть ли необходимость делать отдельные модели для хранения данных? Каковы, по вашему мнению, недостатки этого подхода? Я понимаю, что очевидными ответами являются стандарты и разделение интересов, но можете ли вы придумать какие-либо примеры из реального мира, которые продемонстрируют некоторые из головных болей, которые это вызовет?

Также стоит отметить, что я работаю в команде всего из двух разработчиков, поэтому легко увидеть преимущества и не заметить отклонение от некоторых стандартов. Как вы думаете, работа в небольшой команде имеет значение в этом отношении?


person jrizzo    schedule 27.06.2011    source источник


Ответы (2)


Преимущества моделей представлений в MVC существуют независимо от используемой системы баз данных (черт возьми, даже если вы ее не используете). В простых ситуациях CRUD объекты вашей бизнес-модели будут очень близко имитировать то, что вы показываете в представлениях, но в чем-то большем, чем базовый CRUD, это не так.

Одной из важных вещей является проблема бизнес-логики/целостности данных с использованием того же класса для моделирования/сохранения данных, который вы используете в представлениях. Возьмем ситуацию, когда у вас есть свойство DateTime DateAdded в вашем пользовательском классе, чтобы указать, когда пользователь был добавлен. Если вы предоставите форму, которая подключается прямо к вашему классу UserInfo, вы получите обработчик действий, который выглядит так:

[HttpPost]
public ActionResult Edit(UserInfo model) { }

Скорее всего, вы не хотите, чтобы пользователь мог измениться при добавлении в систему, поэтому ваша первая мысль — не предоставлять поле в форме.

Однако полагаться на это нельзя по двум причинам. Во-первых, значение для DateAdded будет таким же, как если бы вы сделали new DateTime(), или оно будет null (в любом случае это будет неверным для этого пользователя).

Вторая проблема заключается в том, что пользователи могут подделать это в запросе формы и добавить &DateAdded=<whatever date> к данным POST, и теперь ваше приложение изменит поле DateAdded в БД на то, что ввел пользователь.

Это предусмотрено дизайном, поскольку механизм привязки модели MVC просматривает данные, отправленные через POST, и пытается автоматически связать их с любыми доступными свойствами в модели. У него нет способа узнать, что переданное свойство не было в исходной форме, и, таким образом, оно все равно будет связывать его с этим свойством.

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

В вашем точном сценарии я могу легко воспроизвести это с помощью манипуляции со строкой POST, поскольку ваша модель представления имеет прямой доступ к вашему объекту данных.

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

public DateTime? BannedDate { get; set; }
public DateTime? ActivationDate { get; set; } // Date the account was activated via email link

Теперь предположим, что вас как администратора интересует статус всех пользователей, и вы хотите отображать сообщение о статусе рядом с каждым пользователем, а также предоставлять различные действия, которые администратор может выполнять в зависимости от статуса этого пользователя. Если вы используете свою модель данных, код вашего представления будет выглядеть так:

// In status column of the web page's data grid

@if (user.BannedDate != null)
{
    <span class="banned">Banned</span>
}
else if (user.ActivationDate != null)
{
    <span class="Activated">Activated</span>
}

//.... Do some html to finish other columns in the table
// In the Actions column of the web page's data grid
@if (user.BannedDate != null)
{
    // .. Add buttons for banned users
}
else if (user.ActivationDate != null)
{
    // .. Add buttons for activated  users
}

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

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

Тогда ваш код выше упрощается как:

// In status column of the web page's data grid

@if (user.Status == UserStatuses.Banned)
{
    <span class="banned">Banned</span>
}
else if (user.Status == UserStatuses.Activated)
{
    <span class="Activated">Activated</span>
}

//.... Do some html to finish other columns in the table
// In the Actions column of the web page's data grid
@if (user.Status == UserStatuses.Banned)
{
    // .. Add buttons for banned users
}
else if (user.Status == UserStatuses.Activated)
{
    // .. Add buttons for activated  users
}

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

person KallDrexx    schedule 27.06.2011
comment
Я в основном согласен. В пятом абзаце, возможно, вам следует указать, что привязка модели является причиной проблемы (т. е. каждое поле с правильным именем будет привязано), и привязка модели также может быть настроена на игнорирование определенных полей, хотя я считаю, что опасно. Поскольку код сопоставления скучен и подвержен ошибкам, AutoMapper может быть хорошей идеей для сопоставления с ViewModels и из них. - person mnemosyn; 28.06.2011
comment
Ах, хороший момент, я добавлю это. И я согласен с тем, что automapper, вероятно, является лучшим решением для преобразования в/из модели представления, чем делать это вручную. - person KallDrexx; 28.06.2011
comment
Вау, спасибо, что нашли время, чтобы сделать этот хорошо написанный ответ для меня! - person jrizzo; 28.06.2011

tl;dr

В приложении как минимум 3 слоя моделей, иногда их можно безопасно комбинировать, иногда нет. В контексте вопроса можно комбинировать модели сохранения и предметной области, но не модель представления.

полный пост

Сценарий, который вы описываете, одинаково хорошо подходит для использования любой модели сущностей напрямую. Это может быть использование модели Linq2Sql в качестве ViewModel, модели фреймворка сущностей, модели гибернации и т. д. Суть в том, что вы хотите использовать сохраненную модель непосредственно в качестве модели представления. Разделение ответственности, как вы упомянули, явно не заставляет вас избегать этого. На самом деле разделение задач даже не является самым важным фактором при построении слоев вашей модели.

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

ORM бывают разных форм и размеров, с разными концептуальными целями, и MongoDB как вы ее описываете — просто одна из них. Иллюзия, которую обещает большинство из них, заключается в том, что ваша модель постоянства должна быть такой же, как ваша модель домена, а ORM — это просто инструмент сопоставления вашего хранилища данных с вашим объектом домена. Это, безусловно, верно для простых сценариев, когда все ваши данные поступают из одного места, но в конечном итоге имеют свои ограничения, и ваше хранилище превращается в нечто более практичное для вашей ситуации. Когда это происходит, модели, как правило, становятся различимыми.

Единственное практическое правило, которому следует следовать при принятии решения о том, можете ли вы отделить свою модель предметной области от своей модели постоянства, заключается в том, можете ли вы легко заменить свое хранилище данных, не меняя свою модель предметной области. Если ответ положительный, их можно комбинировать, в противном случае это должны быть отдельные модели. Интерфейс репозитория естественным образом подходит для доставки моделей предметной области из любого доступного хранилища данных. Некоторые из новых облегченных ORM, такие как dapper и massive, упростите использование модели предметной области в качестве модели постоянства, поскольку им не требуется конкретная модель данных для выполнять постоянство, вы просто пишете запросы напрямую и позволяете ORM просто обрабатывать сопоставление.

Что касается чтения, то модели представлений снова представляют собой отдельный уровень модели, поскольку они представляют собой подмножество вашей модели предметной области, объединенное так, как вам нужно для отображения информации на странице. Если вы хотите отобразить информацию о пользователе со ссылками на всех его друзей, и когда вы наводите курсор на их имя, вы получаете некоторую информацию об этом пользователе, ваша модель постоянства для обработки этого напрямую, даже с MongoDB, вероятно, будет довольно безумной. Конечно, не каждое приложение отображает такой набор взаимосвязанных данных в каждом представлении, и иногда модель предметной области — это именно то, что вы хотите отобразить. В этом случае нет причин придавать дополнительный вес отображению объекта, который имеет именно то, что вы хотите отобразить, на конкретную модель представления с теми же свойствами. В простых приложениях, если все, что я хочу сделать, это расширить модель предметной области, моя модель представления будет напрямую наследоваться от модели предметной области и добавлять дополнительные свойства, которые я хочу отобразить. При этом, прежде чем ваше приложение MVC станет большим, я настоятельно рекомендую использовать модель представления для ваших макетов и наследовать все модели представлений на основе страниц от этой модели макета.

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

Наконец о ваших пунктах:

  1. Меньший объем кода является преимуществом только тогда, когда он на самом деле более понятен. Его читабельность и понятность являются результатом навыков человека, пишущего его. Есть известные примеры короткого кода, на анализ и понимание которого даже солидным разработчикам потребовалось много времени. Большинство этих примеров исходят из искусно написанного кода, который не более понятен. Более важно, чтобы ваш код на 100% соответствовал вашей спецификации. Если ваш код короток, легко понятен и читабелен, но не соответствует спецификации, он бесполезен. Если все это соответствует спецификации, но ее легко использовать, то спецификация и код бесполезны.

  2. Безопасный рефакторинг за считанные секунды является результатом хорошо написанного кода, а не его лаконичности. Следование принципу DRY сделает ваш код легко рефакторинговым, если ваша спецификация правильно соответствует вашим целям. В случае со слоями модели ваша модель предметной области является ключом к написанию хорошего, удобного в сопровождении и простого для рефакторинга кода. Ваша доменная модель будет меняться в соответствии с теми темпами, с которыми меняются ваши бизнес-требования. Изменения в ваших бизнес-требованиях — это большие изменения, и необходимо позаботиться о том, чтобы новая спецификация была полностью продумана, разработана, реализована, протестирована и т. д. Например, сегодня вы говорите, что хотите добавить второй адрес электронной почты. Вам все равно придется изменить вид (если вы не используете какие-то строительные леса). Кроме того, что, если завтра вы получите изменение требований, чтобы добавить поддержку до 100 адресов электронной почты? Изменение, которое вы первоначально предложили, было довольно простым для любой системы, большие изменения требуют больше работы.

person Nick Larsen    schedule 27.06.2011