你如何打破实体之间的循环关联?

时间:2009-03-05 18:27:54

标签: c# sql oop modeling

我第一次在网站上如果标记不正确或在其他地方得到回答而道歉......

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

更具体的例子:

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

对我而言,这似乎是建立关系的一种很好的干净方式,但由于循环关联而立即导致问题,因为循环外键而无法强制执行DB中的关系,并且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; }
}

员工肯定需要参考他们的公司,每个公司只能有一个ContactPerson。希望这能让我的原点更加清晰!

8 个答案:

答案 0 :(得分:25)

解决这个问题的诀窍是要意识到父母不需要知道孩子的所有方法,并且孩子不需要知道父母的所有方法。因此,您可以使用Interface Segregation Principle对其进行解耦。

简而言之,您为父级创建了一个只包含子级所需方法的接口。您还为子级创建了一个只有父级需要的方法的接口。然后,您让父级包含子接口的列表,并让子级指向父接口。我称之为 Flip Flob Pattern 因为UML图具有Eckles-Jordan触发器的几何形状(Sue me,我是一位老硬件工程师!)

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

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

有时,为了使分离恰到好处,你必须移动一些方法。可能存在您认为应该在SystemMenu中移动到MenuItem等的代码。但是通常该技术运行良好。

答案 1 :(得分:6)

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

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

答案 2 :(得分:3)

我真的没有看到你的问题。很明显,你正在使用C#,它将对象作为引用而不是实例。这意味着进行交叉引用甚至是自引用都是完美的。

在C ++和其他语言中,对象更复杂,那么你可能遇到问题,通常使用引用或指针来解决,但C#应该没问题。

很可能你的问题是你试图以某种方式跟踪所有引用,导致循环引用。 LINQ使用延迟加载来解决此问题。例如,LINQ在您引用之前不会加载公司或员工。您只需要避免进一步遵循这样的引用。

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

在公司示例中,您可能希望删除员工而不是公司,因此请将公司 - &gt;员工FK删除约束关系。如果有员工,这可以防止您删除公司,但是您可以删除员工而不删除公司。

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

例如:

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

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

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

答案 3 :(得分:2)

也许自我引用的GoF Composite模式是一个订单。菜单有一个叶子MenuItems的集合,两者都有一个共同的界面。这样你就可以用Menus和/或MenuItems组成一个菜单。该模式具有一个表,该表具有指向其自己的主键的外键。也适用于步行菜单。

答案 4 :(得分:1)

在代码中,您需要引用两种方式来引用两种方式。但是在数据库中,您只需要一种方法来使事情有效。由于连接工作的方式,您只需要在其中一个表中使用外键。当您考虑它时,数据库中的每个外键都可以翻转,并创建和创建循环引用。最好只选择一条记录,在这种情况下可能是父母带有外键的孩子,只是完成了。

答案 5 :(得分:1)

在域驱动的设计意义上,您可以选择避免实体之间的双向关系。选择一个“聚合根”来保存关系,并仅在从聚合根导航时使用另一个实体。我试图在可能的情况下避免双向关系。因为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; }
}

答案 6 :(得分:0)

您可以在数据库中强制执行外键,其中两个表相互引用。想到两种方式:

  1. 父项中的默认子列最初为null,仅在插入所有子行后才更新。
  2. 您将约束检查推迟到提交时间。这意味着您可以首先插入父项,并初始化对子项的引用,然后插入子项。延迟约束检查的一个问题是,您最终可能会在提交时抛出数据库异常,这在许多db框架中通常很不方便。此外,这意味着您需要在插入之前知道孩子的主键,这在您的设置中可能会很尴尬。
  3. 我在这里假设父菜单项位于一个表中,而子项位于不同的表中,但如果它们都在同一个表中,则相同的解决方案将起作用。

    许多DBMS支持延迟约束检查。虽然您没有提到您正在使用的DBMS,但您可能也会这样做

答案 7 :(得分:0)

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

引入了第三个名为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做任何太聪明的事情!还允许多种类型的“众所周知的联系人”,它们肯定会在下一个冲刺中出现(所以不是真的YAGNI!)。

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