【问题标题】:MVC and NOSQL: Saving View Models directly to MongoDB?MVC 和 NOSQL:将视图模型直接保存到 MongoDB?
【发布时间】:2011-06-27 19:37:29
【问题描述】:

我了解 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,所以我不需要更改我的数据库架构或弄乱任何现有数据。

鉴于此设置,是否需要制作单独的模型来存储数据?大家认为这种方法的缺点是什么?我意识到显而易见的答案是标准和关注点分离,但是您是否有任何现实世界的例子可以证明这会导致一些令人头疼的问题?

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

【问题讨论】:

    标签: asp.net-mvc mongodb separation-of-concerns


    【解决方案1】:

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

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

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

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

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

    第二个问题是用户可以在表单请求中欺骗它并将&amp;DateAdded=&lt;whatever date&gt; 添加到 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 中,该 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
    }
    

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

    【讨论】:

    • 我基本同意。在第五段中,也许你应该指出模型绑定是问题的原因(即每个具有正确名称的字段都将被绑定),模型绑定也可以配置为忽略某些字段——即使我认为那个危险。由于映射代码枯燥且容易出错,因此 AutoMapper 可能是与 ViewModel 进行映射的好主意。
    • 啊,好点,我补充一下。我确实同意 automapper 可能是一个更好的解决方案来转换视图模型而不是手动进行。
    • 哇,感谢您抽出宝贵时间为我做出精心编写的答案!
    【解决方案2】:

    tl;博士

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

    全文

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

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

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

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

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

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

    最后关于你的观点:

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-09-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多