MVC 和 NOSQL:将视图模型直接保存到 MongoDB?

发布于 2024-11-17 10:23:31 字数 2507 浏览 6 评论 0原文

我知道 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 类包含一个继承自 IRepoData 的 UserInfo 实例? UserInfo 是保存到数据库的内容。我有一个通用存储库类,它接受继承 IRepoData 的任何对象并保存它;所以我只需调用 Repository.Save(myUserInfo) 即可完成。 IRepoData 定义了 _id(MongoDB 命名约定)和时间戳,因此存储库可以根据 _id 进行更新插入,并根据时间戳检查冲突,以及对象刚刚保存到 MongoDB 的任何其他属性。在大多数情况下,视图只需要使用 @Html.EditorFor ,我们就可以开始了!基本上,只有视图需要的任何内容都会进入基本模型,只有存储库需要的任何内容都只需获取 [ScaffoldColumn(false)] 注释,而其他所有内容在两者之间都是通用的。 (顺便说一句 - 用户名、密码、角色和电子邮件被保存到 .NET 提供程序中,因此这就是它们不在 UserInfo 对象中的原因。)

巨大优势这种情况有两个方面...

  1. 我可以使用更少的代码,因此更容易理解,开发速度更快,并且更易于维护(在我看来)。

  2. 我可以在几秒钟内重构...如果我需要添加第二个电子邮件地址,我只需将其添加到 UserInfo 对象中 - 它会添加到视图中并保存到存储库中只需向对象添加一个属性即可。因为我使用的是 MongoDB,所以我不需要更改我的数据库架构或弄乱任何现有数据。

鉴于这种设置,是否需要制作单独的模型来存储数据?大家认为这种方法的缺点是什么?我意识到显而易见的答案是标准和关注点分离,但是您能想到一些现实世界的例子来证明这会引起一些麻烦吗?

还值得注意的是,我正在一个总共由两名开发人员组成的团队中工作,因此很容易看到好处而忽略了一些标准的改变。您认为在较小的团队中工作在这方面会有所不同吗?

I understand that the "proper" structure for separation-of-concerns in MVC is to have view-models for your structuring your views and separate data-models for persisting in your chosen repository. I started experimenting with MongoDB and I'm starting to think that this may not apply when using a schema-less, NO-SQL style database. I wanted to present this scenario to the stackoverflow community and see what everyone's thoughts are. I'm new to MVC, so this made sense to me, but maybe I am overlooking something...

Here is my example for this discussion: When a user wants to edit their profile, they would go to the UserEdit view, which uses the UserEdit model below.

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

Notice that the UserEditModel class contains an instance of UserInfo that inherits from IRepoData? UserInfo is what gets saved to the database. I have a generic repository class that accepts any object that inherits form IRepoData and saves it; so I just call Repository.Save(myUserInfo) and its's done. IRepoData defines the _id (MongoDB naming convention) and a Timestamp, so the repository can upsert based on _id and check for conflicts based on the Timestamp, and whatever other properties the object has just get saved to MongoDB. The view, for the most part, just needs to use @Html.EditorFor and we are good to go! Basically, anything that just the view needs goes into the base-model, anything that only the repository needs just gets the [ScaffoldColumn(false)] annotation, and everything else is common between the two. (BTW - the username, password, roles, and email get saved to .NET providers, so that is why they are not in the UserInfo object.)

The big advantages of this scenario are two-fold...

  1. I can use less code, which is therefore more easily understood, faster to develop, and more maintainable (in my opinion).

  2. I can re-factor in seconds... If I need to add a second email address, I just add it to the UserInfo object - it gets added to the view and saved to the repository just by adding one property to the object. Because I am using MongoDB, I don't need to alter my db schema or mess with any existing data.

Given this setup, is there a need to make separate models for storing data? What do you all think the disadvantages of this approach are? I realize that the obvious answers are standards and separation-of-concerns, but are there any real world examples can you think of that would demonstrate some of the headaches this would cause?

Its also worth noting that I'm working on a team of two developers total, so it's easy to look at the benefits and overlook bending some standards. Do you think working on a smaller team makes a difference in that regard?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

ㄟ。诗瑗 2024-11-24 10:23:31

无论使用哪种数据库系统,MVC 中视图模型的优点都存在(即使您不使用数据库系统)。在简单的 CRUD 情况下,您的业务模型实体将非常接近地模仿您在视图中显示的内容,但在除基本 CRUD 以外的任何情况下,情况都不会如此。

其中一件大事是业务逻辑/数据完整性问题,即使用与视图中使用的相同的类进行数据建模/持久性。假设您的用户类中有一个 DateTime DateAdded 属性,用于表示添加用户的时间。如果您提供一个直接挂钩到您的 UserInfo 类的表单,您最终会得到一个如下所示的操作处理程序:

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

很可能您不希望用户在添加到系统,因此您的第一个想法是不要在表单中提供字段。

但是,您不能依赖它,原因有两个。首先,DateAdded 的值将与执行 new DateTime() 时获得的值相同,否则将为 null (任何一种方式对于该用户来说都是不正确的)。

第二个问题是,用户可以在表单请求中欺骗这一点,并将 &DateAdded= 添加到 POST 数据,现在您的应用程序将更改数据库中的 DateAdded 字段用户输入的任何内容。

这是设计使然,因为 MVC 的模型绑定机制会查看通过 POST 发送的数据,并尝试自动将它们与模型中的任何可用属性连接起来。它无法知道发送过来的属性不是原始形式,因此它仍然会将其绑定到该属性。

ViewModel 不存在此问题,因为您的视图模型应该知道如何将自身转换为数据实体或从数据实体转换自身,并且它没有可以欺骗的 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
}

在这个简单的场景中,这可能看起来并没有更少的代码,但当确定用户状态的逻辑变得更加复杂时,它会使事情变得更易于维护。现在,您可以更改确定用户状态的逻辑,而无需更改数据模型(您不必因为查看数据的方式而更改数据模型),并且它将状态确定保留在一处。

The advantages of view models in MVC exist regardless of database system used (hell even if you don't use one). In simple CRUD situations, your business model entities will very closely mimick what you show in the views, but in anything more than basic CRUD this will not be the case.

One of the big things are business logic / data integrity concerns with using the same class for data modeling/persistence as what you use in views. Take the situation where you have a DateTime DateAdded property in your user class, to denote when a user was added. If you provide an form that hooks straight into your UserInfo class you end up with an action handler that looks like:

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

Most likely you don't want the user to be able to change when they were added to the system, so your first thought is to not provide a field in the form.

However, you can't rely on that for two reasons. First is that the value for DateAdded will be the same as what you would get if you did a new DateTime() or it will be null ( either way will be incorrect for this user).

The second issue with this is that users can spoof this in the form request and add &DateAdded=<whatever date> to the POST data, and now your application will change the DateAdded field in the DB to whatever the user entered.

This is by design, as MVC's model binding mechanism looks at the data sent via POST and tries to automatically connect them with any available properties in the model. It has no way to know that a property that was sent over wasn't in the originating form, and thus it will still bind it to that property.

ViewModels do not have this issue because your view model should know how to convert itself to/from a data entity, and it does not have a DateAdded field to spoof, it only has the bare minimum fields it needs to display (or receive) it's data.

In your exact scenario, I can reproduce this with ease with POST string manipulation, since your view model has access to your data entity directly.

Another issue with using data classes straight in the views is when you are trying to present your view in a way that doesn't really fit how your data is modeled. As an example, let's say you have the following fields for users:

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

Now let's say as an Admin you are interested on the status of all users, and you want to display a status message next to each user as well as give different actions the admin can do based on that user's status. If you use your data model, your view's code will look like:

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

This is bad because you have a lot of business logic in your views now (user status of banned always takes precedence over activated users, banned users are defined by users with a banned date, etc...). It is also much more complicated.

Instead, a better (imho at least) solution is to wrap your users in a ViewModel that has an enumeration for their status, and when you convert your model to your view model (the view model's constructor is a good place to do this) you can insert your business logic once to look at all the dates and figure out what status the user should be.

Then your code above is simplified as:

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

Which may not look like less code in this simple scenario, but it makes things a lot more maintainable when the logic for determining a status for a user becomes more complicated. You can now change the logic of how a user's status is determined without having to change your data model (you shouldn't have to change your data model because of how you are viewing data) and it keeps the status determination in one spot.

所有深爱都是秘密 2024-11-24 10:23:31

tl;dr

应用程序中至少有 3 层模型,有时它们可​​以安全地组合,有时则不能。在问题的上下文中,可以组合持久性模型和域模型,但不能组合视图模型。

全文

您描述的场景同样适合直接使用任何实体模型。它可以使用 Linq2Sql 模型作为 ViewModel、实体框架模型、hibernate 模型等。要点是您希望直接使用持久模型作为视图模型。正如您提到的,关注点分离并没有明确强迫您避免这样做。事实上,关注点分离甚至不是构建模型层时最重要的因素。

在典型的 Web 应用程序中,至少有 3 个不同的模型层,尽管将这些层组合成单个对象是可能的,有时也是正确的。模型层从最高级别到最低级别是视图模型、域模型和持久性模型。您的视图模型应该准确地描述您的视图中的内容,不多也不少。您的领域模型应该准确描述系统的完整模型。您的持久性模型应该准确描述域模型的存储方法。

ORM 有多种形状和大小,具有不同的概念目的,而 MongoDB 如您所描述的只是其中之一。他们中的大多数人承诺的幻想是,您的持久性模型应该与您的域模型相同,而 ORM 只是从数据存储到域对象的映射工具。对于简单的场景来说确实如此,其中所有数据都来自一个地方,但最终有其局限性,并且您的存储会降级为更适合您的情况的实用工具。当这种情况发生时,模型往往会变得截然不同。

在决定是否可以将域模型与持久性模型分离时要遵循的一条经验法则是是否可以在不更改域模型的情况下轻松交换数据存储。如果答案是肯定的,那么它们可以组合起来,否则它们应该是单独的模型。存储库接口自然适合这里,以从任何可用的数据存储中提供域模型。一些较新的轻量级 ORM,例如 dappermassive,使得使用域模型作为持久性模型变得非常容易,因为它们不需要特定的数据模型来坚持不懈,你就是只需直接编写查询,然后让 ORM 处理映射即可。

在读取方面,视图模型又是一个独特的模型层,因为它们代表了域模型的子集,但您需要将其组合在一起,以便向页面显示信息。如果你想显示一个用户的信息,以及他所有朋友的链接,当你将鼠标悬停在他们的名字上时,你会得到有关该用户的一些信息,你的持久性模型直接处理这些信息,即使使用 MongoDB,也可能会非常疯狂。当然,并不是每个应用程序都在每个视图上显示这样一组互连的数据,有时域模型正是您想要显示的内容。在这种情况下,没有理由从具有您想要显示的内容的对象到具有相同属性的特定视图模型的映射中加入额外的权重。在简单的应用程序中,如果我想做的只是增强域模型,我的视图模型将直接继承域模型并添加我想要显示的额外属性。话虽这么说,在您的 MVC 应用程序变大之前,我强烈建议您为布局使用视图模型,并让所有基于页面的视图模型继承该布局模型。

在写入方面,视图模型应该只允许您希望针对访问视图的用户类型进行编辑的属性。不要将管理视图模型发送到非管理员用户的视图。如果您自己为此模型编写映射层以考虑访问用户的权限,那么您可以摆脱这种情况,但这可能比仅创建继承自的第二个管理模型的开销更大常规视图模型并使用管理属性对其进行增强。

最后关于您的观点:

  1. 只有当代码实际上更容易理解时,更少的代码才是一个优势。它的可读性和可理解性是编写它的人的技能的结果。有一些著名的短代码示例,即使是优秀的开发人员也需要很长时间才能剖析和理解。这些示例大多数来自巧妙编写的代码,但这些代码并不更容易理解。更重要的是您的代码 100% 符合您的规范。如果你的代码很短,易于理解和可读,但不符合规范,那么它就毫无价值。如果它是所有这些东西并且确实符合规范,但很容易被利用,那么规范代码就毫无价值。

  2. 在几秒钟内安全地重构是代码编写良好的结果,而不是简洁的结果。只要您的规范正确满足您的目标,遵循 DRY 原则将使您的代码易于重构。就模型层而言,领域模型是编写良好、可维护且易于重构代码的关键。您的域模型将随着业务需求的变化而变化。您的业​​务需求的变化是巨大的变化,必须小心确保新规范经过充分考虑、设计、实施、测试等。例如,您今天说您想添加第二个电子邮件地址。您仍然需要更改视图(除非您使用某种脚手架)。另外,如果明天您收到要求更改以添加对最多 100 个电子邮件地址的支持怎么办?您最初提出的更改对于任何系统来说都相当简单,更大的更改需要更多工作。

tl;dr

There are at least 3 layers of models in an application, sometimes they can be combined safely, sometimes not. In the context of the question, it's ok to combine the persistence and domain models but not the view model.

full post

The scenario you describe fits equally well using any entity model directly. It could be using a Linq2Sql model as your ViewModel, an entity framework model, a hibernate model, etc. The main point is that you want to use the persisted model directly as your view model. Separation of concerns, as you mention, does not explicitly force you to avoid doing this. In fact separation of concerns is not even the most important factor in building your model layers.

In a typical web application there are at least 3 distinct layers of models, although it is possible and sometimes correct to combine these layers into a single object. The model layers are, from highest level to lowest, your view model, your domain model and your persistence model. Your view model should describe exactly what is in your view, no more and no less. Your domain model should describe your complete model of the system exactly. Your persistence model should describe your storage method for your domain models exactly.

ORMs come in many shapes and sizes, with different conceptual purposes, and MongoDB as you describe it is simply one of them. The illusion most of them promise is that your persistence model should be the same as your domain model and the ORM is just a mapping tool from your data store to your domain object. This is certainly true for simple scenarios, where all of your data comes from one place, but eventually has it's limitations, and your storage degrades into something more pragmatic for your situation. When that happens, the models tend to become distinct.

The one rule of thumb to follow when deciding whether or not you can separate your domain model from your persistence model is whether or not you could easily swap out your data store without changing your domain model. If the answer is yes, they can be combined, otherwise they should be separate models. A repository interface naturally fits here to deliver your domain models from whatever data store is available. Some of the newer light weight ORMs, such as dapper and massive, make it very easy to use your domain model as your persistence model because they do not require a particular data model in order to perform persistence, you are simply writing the queries directly, and letting the ORM just handle the mapping.

On the read side, view models are again a distinct model layer because they represent a subset of your domain model combined however you need in order to display information to the page. If you want to display a user's info, with links to all his friends and when you hover over their name you get some info about that user, your persistence model to handle that directly, even with MongoDB, would likely be pretty insane. Of course not every application is showing such a collection of interconnected data on every view, and sometimes the domain model is exactly what you want to display. In that case there is no reason to put in the extra weight of mapping from an object that has exactly what you want to display to a specific view model that has the same properties. In simple apps if all I want to do is augment a domain model, my view model will directly inherit from the domain model and add the extra properties I want to display. That being said, before your MVC app becomes large, I highly recommend using a view model for your layouts, and having all of page based view models inherit from that layout model.

On the write side, a view model should only allow the properties you wish to be editable for the type of user accessing the view. Do not send an admin view model to the view for a non admin user. You could get away with this if you write the mapping layer for this model yourself to take into account the privileges of the accessing user, but that is probably more overhead than just creating a second admin model that inherits from the regular view model and augments it with the admin properties.

Lastly about your points:

  1. Less code is only an advantage when it actually is more understandable. Readability and understand-ability of it are results of the skills of the person writing it. There are famous examples of short code that has taken even solid developers a long time to dissect and understand. Most of those examples come from cleverly written code which is not more understandable. More important is that your code meets your specification 100%. If your code is short, easily understood and readable but does not meet the specification, it is worthless. If it is all of those things and does meet the specification, but is easily exploitable, the specification and the code are worthless.

  2. Refactoring in seconds safely is the result of well written code, not it's terseness. Following the DRY principle will make your code easily refactorable as long as your specification correctly meets your goals. In the case of model layers, your domain model is the key to writing good, maintainable and easy to refactor code. Your domain model will change at the pace at which your business requirements change. Changes in your business requirements are big changes, and care has to be taken to make sure that a new spec is fully thought out, designed, implemented, tested, etc. For example you say today you want to add a second email address. You still will have to change the view (unless you're using some kind of scaffolding). Also, what if tomorrow you get a requirements change to add support for up to 100 email addresses? The change you originally proposed was rather simple for any system, bigger changes require more work.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文