【问题标题】:How do you break circular associations between entities?你如何打破实体之间的循环关联?
【发布时间】:2010-10-11 14:04:05
【问题描述】:

我是第一次访问该网站,如果标签错误或在其他地方得到回答,敬请见谅...

我在当前项目中不断遇到特殊情况,我想知道你们将如何处理它。模式是:具有一组子项的父项,并且父项具有一个或多个对子集合中特定项的引用,通常是“默认”子项。

一个更具体的例子:

public class SystemMenu 
{
    public IList<MenuItem> Items { get; private set; }
    public MenuItem DefaultItem { get; set; }
}

public class MenuItem
{
    public SystemMenu Parent { get; set; }
    public string Name { get; set; }
}

对我来说,这似乎是一种很好的关系建模方式,但由于循环关联,我无法在数据库中强制执行关系,并且 LINQ to SQL 崩溃了由于循环关联。即使我能绕过这个,这显然不是一个好主意。

我目前唯一的想法是在 MenuItem 上设置一个“IsDefault”标志:

public class SystemMenu 
{
    public IList<MenuItem> Items { get; private set; }
    public MenuItem DefaultItem 
    {
        get 
        {
            return Items.Single(x => x.IsDefault);
        }
        set
        {
            DefaultItem.IsDefault = false;
            value.DefaultItem = true;
        }
    }
}

public class MenuItem
{
    public SystemMenu Parent { get; set; }
    public string Name { get; set; }
    public bool IsDefault { get; set; }
}

有没有人处理过类似的事情并可以提供一些建议?

干杯!

编辑:感谢到目前为止的回复,也许“菜单”示例并不出色,我试图想出一些有代表性的东西,所以我不必深入了解我们不那么自我的细节-解释域模型!也许更好的例子是公司/员工关系:

public class Company
{
    public string Name { get; set; }
    public IList<Employee> Employees { get; private set; }
    public Employee ContactPerson { get; set; }
}

public class Employee
{
    public Company EmployedBy { get; set; }
    public string FullName { get; set; }
}

员工肯定需要引用他们的公司,每个公司只能有一个联系人。希望这能让我原来的观点更清楚一点!

【问题讨论】:

    标签: c# sql oop modeling


    【解决方案1】:

    解决这个问题的诀窍是要意识到父母不需要知道孩子的所有方法,孩子也不需要知道父母的所有方法。因此,您可以使用Interface Segregation Principle 将它们解耦。

    简而言之,您为父级创建了一个接口,其中只有子级需要的那些方法。您还为子级创建了一个接口,该接口仅具有父级需要的那些方法。然后你让父接口包含一个子接口列表,你让子接口指向父接口。我称其为 触发器模式,因为 UML 图具有 Eckles-Jordan 触发器的几何结构(请注意,我是一名老硬件工程师!)

      |ISystemMenu|<-+    +->|IMenuItem|
              A    1  \  / *     A
              |        \/        |
              |        /\        |
              |       /  \       |
              |      /    \      |
              |     /      \     |
        |SystemMenu|        |MenuItem|
    

    请注意,此图中没有循环。你不能从一堂课开始,然后按照箭头回到你的起点。

    有时,为了使分离恰到好处,您必须移动一些方法。可能有您认为应该在 SystemMenu 中移动到 MenuItem 等的代码。但总的来说,该技术效果很好。

    【讨论】:

    • 像往常一样,鲍勃叔叔讲道理——横向思维的一个非常简单的应用。 (P.S. 我的意见绝不会因我也是“鲍勃叔叔”这一事实而产生偏见)
    • 要了解鲍勃大叔,首先要了解(成为)鲍勃大叔的意义
    • 这似乎是一个简单问题的复杂解决方案。接口驱动的编程工作得很好,但是在没有必要的时候污染命名空间有什么意义呢?只要您不无限地遵循它们,在 C# 中进行循环重新引用就没有问题。
    • 我不会使用接口来表示实体。您的菜单应指向特定的菜单项,而不是代表菜单项的内容。
    • 非常好,希望我能及时阅读并实施它!尽管像 Paco 一样,我并不完全习惯于使用接口来表示实体(它在这个项目中被严重滥用,并且一直是一个痛点......)
    【解决方案2】:

    您的解决方案似乎很合理。

    要考虑的另一件事是内存中的对象不必与数据库模式完全匹配。在数据库中,您可以使用带有子属性的更简单的模式,但在内存中,您可以优化事物并让父对象具有对子对象的引用。

    【讨论】:

      【解决方案3】:

      我真的没有看到你的问题。显然,您使用的是 C#,它将对象作为引用而不是实例。这意味着具有交叉引用甚至自引用是完全可以的。

      在 C++ 和其他对象组合更多的语言中,您可能会遇到问题,通常使用引用或指针来解决,但 C# 应该没问题。

      您的问题很可能是您试图以某种方式跟踪所有引用,从而导致循环引用。 LINQ 使用延迟加载来解决这个问题。例如,在您引用 Company 或 Employee 之前,LINQ 不会加载它。您只需要避免遵循此类参考超过一级即可。

      但是,您不能真正将两个表添加为彼此的外键,否则您将永远无法删除任何记录,因为删除员工需要先删除公司,但您不能不删除公司删除员工。通常,在这种情况下,您只会将一个用作真正的外键,另一个将只是一个伪 FK(即,用作 FK 但未启用约束的一个)。你必须决定哪个是更重要的关系。

      在公司示例中,您可能希望删除员工而不是公司,因此将 company->employee FK 设置为约束关系。这可以防止您在有员工时删除公司,但您可以在不删除公司的情况下删除员工。

      此外,避免在这些情况下在构造函数中创建新对象。例如,如果您的 Employee 对象创建了一个新的 Company 对象,其中包括为该员工创建的新员工对象,它最终会耗尽内存。相反,将已经创建的对象传递给构造函数,或者在构造之后设置它们,可能使用初始化方法。

      例如:

      Company c = GetCompany("ACME Widgets");
      c.AddEmployee(new Employee("Bill"));
      

      然后,在 AddEmployee 中,您设置公司

      public void AddEmployee(Employee e)
      {
          Employees.Add(e);
          e.Company = this;
      }
      

      【讨论】:

        【解决方案4】:

        也许自我引用的 GoF 复合模式是这里的命令。一个 Menu 有一个叶子 MenuItem 的集合,它们都有一个共同的接口。这样你就可以用菜单和/或菜单项组成一个菜单。该模式有一个带有外键的表,该外键指向它自己的主键。这种方式也适用于步行菜单。

        【讨论】:

          【解决方案5】:

          在代码中,您需要同时引用两种方式来引用事物。但是在数据库中,你只需要引用一种方式就可以了。由于连接的工作方式,您只需要在其中一个表中拥有外键。仔细想想,数据库中的每个外键都可以翻转,并创建和创建循环引用。最好只选择一条记录,在这种情况下,可能是子项与父项具有外键,然后就完成了。

          【讨论】:

            【解决方案6】:

            在领域驱动的设计意义上,您可以选择尽可能避免实体之间的双向关系。选择一个“聚合根”来保存关系,仅在从聚合根导航时使用另一个实体。我尽量避免双向关系。因为 YAGNI,它会让你问“先是先有鸡还是先有蛋?”的问题。有时您仍然需要双向关联,然后选择前面提到的解决方案之一。

            /// This is the aggregate root
            public class Company
            {
                public string Name { get; set; }
                public IList<Employee> Employees { get; private set; }
                public Employee ContactPerson { get; set; }
            }
            
            /// This isn't    
            public class Employee
            {
                public string FullName { get; set; }
            }
            

            【讨论】:

              【解决方案7】:

              您可以在两个表相互引用的数据库中强制执行外键。我想到了两种方法:

              1. 父项中的默认子列最初为 null,并且仅在插入所有子行后才更新。
              2. 您将约束检查推迟到提交时间。这意味着您可以先插入对子项的初始引用中断的父项,然后再插入子项。延迟约束检查的一个问题是,您最终可能会在提交时抛出数据库异常,这在许多数据库框架中通常很不方便。此外,这意味着您需要在插入子项之前知道子项的主键,这在您的设置中可能会很尴尬。

              我在这里假设父菜单项位于一个表中,子项位于另一个表中,但如果它们都位于同一个表中,则相同的解决方案将起作用。

              许多 DBMS 支持延迟约束检查。尽管您没有提及您使用的是哪个 DBMS,但您的可能也是如此

              【讨论】:

                【解决方案8】:

                感谢所有回答的人,一些非常有趣的方法!最后我不得不匆忙完成一些事情,所以这就是我想出的:

                引入了第三个实体WellKnownContact 和对应的WellKnownContactType 枚举:

                public class Company
                {
                    public string Name { get; set; }
                    public IList<Employee> Employees { get; private set; }
                    private IList<WellKnownEmployee> WellKnownEmployees { get; private set; }
                    public Employee ContactPerson
                    {
                        get
                        {
                            return WellKnownEmployees.SingleOrDefault(x => x.Type == WellKnownEmployeeType.ContactPerson);
                        }
                        set
                        {                
                            if (ContactPerson != null) 
                            {
                                // Remove existing WellKnownContact of type ContactPerson
                            }
                
                            // Add new WellKnownContact of type ContactPerson
                        }
                    }
                }
                
                public class Employee
                {
                    public Company EmployedBy { get; set; }
                    public string FullName { get; set; }
                }
                
                public class WellKnownEmployee
                {
                    public Company Company { get; set; }
                    public Employee Employee { get; set; }
                    public WellKnownEmployeeType Type { get; set; }
                }
                
                public enum WellKnownEmployeeType
                {
                    Uninitialised,
                    ContactPerson
                }
                

                感觉有点麻烦,但解决了循环引用问题,并且干净地映射到数据库,这样就省去了尝试让 LINQ to SQL 做任何太聪明的事情!还允许多种类型的“知名联系人”,这肯定会在下一个 sprint 中出现(所以不是真正的 YAGNI!)。

                有趣的是,与我们真正处理的相当抽象的实体相比,一旦我想出了人为的公司/员工示例,它就更容易思考了。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2013-10-28
                  • 1970-01-01
                  • 2021-04-23
                  • 1970-01-01
                  • 1970-01-01
                  • 2013-03-20
                  相关资源
                  最近更新 更多