.net ORM,它允许我在不修改代码的情况下扩展我的代码(开放封闭原则)

Posted

技术标签:

【中文标题】.net ORM,它允许我在不修改代码的情况下扩展我的代码(开放封闭原则)【英文标题】:.net ORM that will allow me to extend my code without modifying it (Open Closed Principle) 【发布时间】:2018-10-29 22:44:49 【问题描述】:

免责声明:以下是我对问题的非常简化的描述。请在阅读时想象一些复杂的模块化应用程序(ERP、Visual Studio、Adobe Photoshop),这些应用程序多年来一直在发展,其功能将被添加和删除,希望它不会以意大利面条式代码结束。

假设我在数据库中有以下实体和相应的表

class Customer

    public int Id  get; set; 

然后我将使用一些 ORM,构建 DataContext 并创建我的应用程序

static void Main(string[] args)

    //Data Layer
    var context = new DataContext();
    IQueryable<Customer> customers = context.Customers;

    //GUI
    foreach (var customer in customers)
    foreach (var property in customer.GetType().GetProperties())
        Console.WriteLine($"property.Name:property.GetValue(customer)");

申请已完成,我的客户很满意,案件已结案。

半年后,我的客户要求我为客户添加名称。 我想在不接触以前的代码的情况下做到这一点。

所以首先我会在数据库中创建新的实体和对应的表并添加到ORM中(请忽略我正在修改相同的DataContext,这很容易修复)

class CustomerName

    public int Id  get; set; 
    public string Name  get; set; 

CustomerName 将向 Customer 添加新属性,但要获得完整的 Customer 信息,我们需要将它们连接在一起,所以让我们尝试修改我们的应用程序而不触及之前的代码

static void Main(string[] args)

    //Data Layer
    var context = new DataContext();
    IQueryable<Customer> customers = context.Customers;

     //new code that doesn't even compile
    customers = from c in customers
                join cn in context.CustomerNames on c.Id equals cn.Id
                select new c, cn; //<-- what should be here??

    //GUI
    foreach (var customer in customers)
    foreach (var property in customer.GetType().GetProperties())
        Console.WriteLine($"property.Name:property.GetValue(customer)");

如您所见,我不知道将我的信息从 join 映射到什么,以便它仍然是有效的 Customer 对象。

不,我不能使用继承。

为什么?

因为同时可能会要求其他开发人员提供功能来阻止客户并创建以下实体:

class BlockedCustomer

    public int Id  get; set; 
    public bool Blocked  get; set; 

他对客户名称一无所知,因此他可能只依赖于客户,并且在运行时我们的两个功能将导致如下结果:

static void Main(string[] args)

    //Data Layer
    var context = new DataContext();
    IQueryable<Customer> customers = context.Customers;

     //new code that doesn't even compile
    customers = from c in customers
                join cn in context.CustomerNames on c.Id equals cn.Id
                select new c, cn; //<-- what should be here??

    customers = from c in customers
        join b in context.BlockedCustomers on c.Id equals b.Id
        select new  c, b ; //<-- what should be here??

            //GUI
            foreach (var customer in customers)
    foreach (var property in customer.GetType().GetProperties())
        Console.WriteLine($"property.Name:property.GetValue(customer)");

我有 2 个想法如何解决它

    创建一些将从 Customer 继承的容器类,并在需要时将其转换/转换为 CustomerName 或 BlockedCustomer。

类似这样的:

class CustomerWith<T> : Customer

    private T Value;

    public CustomerWith(Customer c, T value) : base(c)
    
        Value = value;
    

然后

customers = from c in customers
            join cn in context.CustomerNames on c.Id equals cn.Id
            select new CustomerWith<CustomerName>(c, cn);
    使用 ConditionalWeakTable 存储(在数据层级别)与 Customer 关联的 CustomerName 和 BlockedCustomer,并修改(一次)UI 以了解此类情况

据我所知,不幸的是,这两种解决方案都需要我编写自己的 LINQ 映射器(包括更改跟踪),我想避免它。

    您知道任何知道如何处理此类要求的 ORM 吗? 或者也许有更好/更简单的解决方案来编写应用程序并且不违反开放/封闭原则?

编辑 - cmets 后的一些说明:

    我说的是与客户具有一对一关系的属性。通常此类属性会作为附加列添加到表中。 我只想向数据库发送一个 SQL 查询。因此,添加 20 个这样的新功能/属性/列不应该在 20 个查询中结束。 我展示的是我想到的应用程序的简化版本。我正在考虑应用程序哪些依赖项将以以下方式构建 [UI]-->[Business Logic] 当我将联接作为建议的解决方案展示时,可能不清楚它们不会在 [Data] 层中硬编码,而是 [Data] 层应该知道它应该调用一些可能想要扩展查询并将由 main() 模块注册。 Main() 模块是唯一一个知道所有依赖关系的模块,就这个问题而言,如何做并不重要。

【问题讨论】:

一种解决方案是拥有一个包含父客户表的扩展属性的子表:CustomerProperties 表。它将有一个外键(客户 ID)、一个功能 ID(键)、一个属性名称(一个附加键)和一个值(需要解析的字符串)。另一种解决方案是使用多个子表,每个子表都特定于您要添加的功能类型。 CustomerPurchases、CustomerContactDetails 等。 我和两者都合作过。第一个很容易实现,但是它将所有这些特征的数据混合在一个表中(尽管很容易用特征 ID 过滤)。第二个允许您使用强类型类,而无需自定义解析逻辑的开销。 @RyanPierceWilliams 谢谢。您的第二个解决方案不完全是我在问题中描述的以及我想要实现的解决方案。是的,我想要强类型的类,但我说的是一对一的关系。您的示例似乎是真正独立的东西,可以在 UI 中单独显示(一个客户对多个 X),而我正在谈论应该在同一个客户列表/卡片上显示的属性,而您的第一个在另一边我'我害怕 Linq/SQL 会如何搜索每一列 - 闻起来像 Linq 不会处理它,我需要构建纯 SQL 来做到这一点。 真正的问题是混淆了数据类和业务实体。您不讨厌在业务模型和数据模型中使用相同的类。事实上你不应该,除非是非常简单的问题。一旦模型变得过于复杂,您需要将它们分离,以便每个模型都可以独立发展 顺便说一句,您也不应该在 LINQ 中使用联接,根据关系生成联接是 ORM 的工作。如果您使用连接,则意味着 EF 上下文配置缺少关系。 Customer 应该有一个 CustomerNames 集合。应该配置CustomerName 本身,以便context.Customers.Where(c=&gt;c.Id=whatever) 将同时加载客户相关名称 【参考方案1】:

我在数据库中有以下实体和对应的表

我认为这个基本前提是大多数问题的根源。

需要明确的是,基于底层关系数据库架构的应用程序架构本质上并没有什么坏处。有时,利益相关者只需要一个简单的 CRUD 应用程序。

尽管如此,重要的是要意识到,无论何时做出选择,应用程序架构都将具有内在的关系(即非面向对象)。

完全规范化的关系数据库的特点是关系。表通过外键与其他表相关。这些关系由数据库引擎强制执行。通常,在完全规范化的数据库中,(几乎)所有表都(可传递地)连接到所有其他表。换句话说,一切都是相连的。

当事物相互连接时,在编程中我们通常称之为耦合,这是我们想要避免的。

您希望将应用程序架构划分为多个模块,但仍将其基于一个所有内容都耦合在一起的数据库。我不知道这样做的实际方法;我觉得不可能。

ORM 只会让情况变得更糟。正如 Ted Neward 十多年前告诉我们的那样,ORMs is the Vietnam war of computer science。几年前我已经放弃了它们,因为它们只会让一切变得更糟,包括软件架构。

设计复杂模块化系统的一种更有前途的方法是 CQRS(参见例如my own attempt at explaining the concept back in 2011)。

这将使您能够将您的域事件(例如命名客户阻止客户视为不仅封装如何的命令,还有为什么会发生一些事情。

那么,在读取方面,您可以通过事务数据的(持久)投影来提供数据。然而,在 CQRS 架构中,事务数据通常更适合事件溯源或文档数据库。

这种架构非常适合微服务,但您也可以通过这种方式编写更大的模块化应用程序。

关系数据库(如 OOD)在一段时间内听起来是个好主意,但最终我的经验是这两种想法都是死胡同。它们不能帮助我们降低复杂性。它们不会让事情更容易维护。

【讨论】:

以上是关于.net ORM,它允许我在不修改代码的情况下扩展我的代码(开放封闭原则)的主要内容,如果未能解决你的问题,请参考以下文章

允许在不修改底层容器的情况下排序和删除项目的类(可能是代理)的命名

Django - 如何在不修改的情况下扩展 3rd 方模型

是否可以在不复制所有代码的情况下复制 iOS 应用程序?

如何在不创建 django 项目的情况下使用 Django 1.8.5 ORM?

SignInAsAuthenticationType 是不是允许我在不覆盖现有声明的情况下获取 OAuth 令牌?

是否可以在不使用整个框架的情况下在 PHP 中为 ORM 安装 Kohana 库?