如何在代码中建模多对多关系?

32
假设数据库中有两个表:狗和老板。这是一个多对多的关系,因为一个老板可以拥有多只狗,一只狗也可以有多个主人。我和我的妻子都是Bobby的主人。
但是多对多是不被允许的,所以需要一个帮助表:DogsPerBoss。
如何在代码中进行建模?
老板类可以拥有一组狗。 狗类可以拥有一组老板。 --> 至少我认为应该这样做。也许有更好的解决方案吗?
那么,关于在帮助表中额外的数据应该放在老板类还是狗类中呢? 比如昵称 (我叫狗“好孩子”,我妻子叫他“小狗”)
我希望我的问题比较清楚? 是否有什么最佳实践可以实现这一点? 你能给我一些参考吗?
ORM(例如NHibernate)不是一个选项。

但是不允许多对多关系。为什么? - Greg Dean
3
@Greg 我理解Natrium的意思是关系型数据库需要一个链接表来表示多对多的关系,而不是“不允许”。 - Graham
2
不行,因为它涉及到多对多的问题,以及关系模型和对象模型在处理这个问题时所面临的困难。 - John Nicholas
12个回答

28

你为什么提到表格?你是在创建对象模型还是数据库模型?

对于对象模型而言,一只狗完全可以拥有一个 List<Owner>,而一个主人也可以拥有一个 List<Dog>。只有当关系上有属性时,你才需要一个中间类(UML称其为关联类)。此时,你将拥有一个名为 DogOwnership 的类,并附加额外的属性,每个主人和每只狗都会有一个 List<DogOwnership>。DogOwnership 将包含一只狗、一个主人和这些额外属性。


我谈论表格是因为数据存储在数据库中,使用表格进行存储。我想找出如何最好地将这些表格映射到对象。你的答案看起来很有前途,我会进一步调查研究。 - Natrium
在这种情况下,我建议先不要担心映射。首先进行设计,然后再从一个表示形式映射到另一个表示形式。 - John Saunders
我认为值得注意的是,没有任何属性的关系后来可能会变成具有一个或多个属性的关系。因此,重要的是不要让公共接口排除这种可能性(例如通过公开类型为List<Owner>和List<Dog>的属性)。 - psr
我只想补充一点,为了方便访问,最好创建一个关联类,其中包含静态的选择、插入和更新方法。你同意吗? - Igor

17
public class Boss
{
   private string name;
   private List<Hashtable> dogs;
   private int limit;

   public Boss(string name, int dogLimit)
   {
      this.name = name;
      this.dogs = new List<Hashtable>();
      this.limit = dogLimit; 
   }

   public string Name { get { return this.name; } }

   public void AddDog(string nickname, Dog dog)
   {
      if (!this.dogs.Contains(nickname) && !this.dogs.Count == limit)
      {
         this.dogs.Add(nickname, dog);
         dog.AddBoss(this);
      } 
   }

   public void RemoveDog(string nickname)
   {
       this.dogs.Remove(nickname);
       dog.RemoveBoss(this);
   }

   public void Hashtable Dogs { get { return this.dogs; } }
}

public class Dog
{
   private string name;
   private List<Boss> bosses;

   public Dog(string name)
   {
      this.name = name;
      this.bosses = new List<Boss>();
   }

   public string Name { get { return this.name; } }

   public void AddBoss(Boss boss)
   {
      if (!this.bosses.Contains(boss))
      {
          this.bosses.Add(boss);
      }
   }

   public void RemoveBoss(Boss boss)
   {
      this.bosses.Remove(boss);
   }  

   public ReadOnlyCollection<Boss> Bosses { get { return new ReadOnlyCollection<Boss>(this.bosses); } }
}

以上维护了老板可以拥有多只狗(有限制)和狗可以被多个老板拥有的关系。这也意味着当老板添加一条狗时,他们可以为该狗指定一个仅适用于该老板的昵称。这意味着其他老板可以添加相同的狗,但使用不同的昵称。
至于限制,我可能会将其作为App.Config值,在实例化老板对象之前读取。因此,一个小例子可能是:
var james = new Boss("James", ConfigurationManager.AppSettings["DogsPerBoss"]);
var joe = new Boss("Joe", ConfigurationManager.AppSettings["DogsPerBoss"]);

var benji = new Dog("Benji");
var pooch = new Dog("Pooch");

james.AddDog("Good boy", benji);
joe.AddDog("Doggy", benji);

james.AddDog("Rover", pooch);
joe.AddDog("Buddy", pooch);  // won't add as the preset limit has been reached.

你可以根据需要对此进行调整,但我认为你所寻找的基本原则已经在这里了。

  • 老板可以拥有多只狗,并设定限制
  • 狗可以有多个老板
  • 老板可以为同一只狗取不同的昵称。

这是关联而不是组合和聚合的示例吗? +1。 - w0051977
@w0051977 是的,因为这里没有真正的所有权(BossDog都有自己的生命周期,并且可以独立存在),所以需要更多的关联。 - James
谢谢。我今天在这里提出了一个问题:https://stackoverflow.com/questions/47367222/have-i-defined-a-composition-relationship-correctly?noredirect=1#comment81687209_47367222。请看一下。谢谢。 - w0051977

7

类似这样的内容; 尽管仍需要一些微调(例如将集合设为私有,添加一个只读的公共访问器来返回只读集合),但你应该能明白。

public class Dog
{
    public List<Boss> Bosses;

    public void AddBoss( Boss b )  
    {
        if( b != null && Bosses.Contains (b) == false )
        {
            Bosses.Add (b);
            b.AddDog (this);
        }
    }

    public void RemoveBoss( Boss b )
    {
         if( b !=null && Bosses.Contains (b) )
         {
             Bosses.Remove (b);
             b.RemoveDog (this);
         }
    }
}

public class Boss
{
    public List<Dog> Dogs;

    public void AddDog( Dog d )
    {
         if( d != null && Dogs.Contains (d) == false )
         {
              Dogs.Add(d);
              d.AddBoss(this);
         }
    }

    public void RemoveDog( Dog d )
    {
        if( d != null && Dogs.Contains(d) )
        {
            Dogs.Remove (d);
            d.RemoveBoss(this);
        }
    }
}

通过这种方式,您可以在代码中建模一对多关系,其中每只狗都知道他的主人,每个主人也都知道他所拥有的狗。

当需要在助手表中添加额外数据时,您还需要创建另一个类。


为什么这个被踩了?在踩的时候,踩的人也应该说一下为什么要踩。 - Frederik Gheysels
这是关联而不是组合和聚合的示例吗? +1 - w0051977

2
传统的多对多关系在匹配表中不会有额外的字段。
因为你拥有具有唯一信息的字段,我倾向于停止将这些关系视为多对多关系。
一旦你向匹配表添加信息,我认为你已经将它转化为了一个实体,因此需要一个自己表示它的对象。
此时,你可以开始拥有一个DogsName类来连接人和狗 - 两者都会包含对该对象的引用作为集合的一部分。
然而,无论你给狗起什么名字或拥有狗是相互独立的。
除了根据不同的人建模狗名字的关系之外,你还需要建模所有权关系。在内存中,这意味着两个对象都包含对另一个对象的列表。

1

我是否漏掉了什么,或者这个程序唯一需要的代码就是下面这样:

List<Bosses> BossList;

class Dog {}
class Boss { Dog[] Dogs; }

您不需要显式地建模双向关系。它在代码结构中是隐含的。可能有其他原因需要这样做,但通常拥有单向引用和遍历引用对象集合的方式就足够了。


1
如果您不需要记录昵称,那么Dog应该有一个Boss列表,而Boss应该有一个Dogs列表。
如果Dog和Boss之间的关系具有属性,例如昵称,在这种情况下,您应该创建一个类来表示该关系,并使Dog和Boss都持有该类型的列表。
我已经使用NHibernate一段时间了,发现它非常有用,可以缓解这种对象关系阻抗不匹配

1

这是数据库中经典的问题,即多对多关系无法正常工作,因此需要使用辅助表,而在对象世界中,多对多关系可以很好地工作。一旦关系具有属性,那么您应该创建一个新类来保存该信息。但是,如果您查看对象关系映射 - ORM - 您将节省大量时间来解决DB和Object之间的这些(以及许多其他)问题。


1
如果您有一个简单的多对多链接表,其中每个表在关系中都有外键,那么您将按照您建议的方式对其进行建模:老板拥有一组狗,而狗也拥有一组老板。
如果您有一个带有额外数据(例如昵称)的多对多关系,则应将其建模为两个一对多关系。创建一个实体,例如DogBoss,以便老板拥有一组DogBoss,而狗也拥有一组DogBoss。

0

我想我可能漏掉了什么。为什么不允许多对多关系?

public class Boss
{
    Dog[] dogs;
}

public class Dog
{
    Boss[] bosses;
}

1
我猜问题是:a)那是最好的方法吗?b)在哪里存储关系的元数据。 - Dirk Vollmar
1
“不允许”的部分在我看来是指您必须在数据库中创建一个额外的表才能建模n:m关系。” - Frederik Gheysels

0

每次我们需要考虑现实生活和我们的需求。在这种情况下,关键点是哪一个应该拥有另一个。

在现实生活中,狗和老板可能彼此没有关联。但是您的软件需求应该影响这种关系。

例如,如果您正在开发一款兽医患者管理软件,为治疗流浪狗的兽医,则患者(狗)-监护人(老板)关系应该如下: 老板必须至少有一只狗,而狗可能没有任何老板(那么老板ID就是此关系的外键),这意味着在您的设计中,狗类必须持有老板的集合。为什么?因为不能创建任何没有狗的老板实例。我们也可以通过数据逻辑来得出这个决策。让我们考虑当您尝试将狗和老板类保存到数据库中时。如果关系条件如上所述,在保存老板时,您应该将连接记录插入连接表中。
如果您正在为不治疗流浪狗的兽医开发该软件,则患者-父母关系需要如下: 一只狗必须至少有一个老板,老板必须至少有一只狗,并且我们需要考虑这种特殊关系情况。这意味着任何这些类的实例都不能在没有彼此的情况下创建。因此,我们需要定义这种依赖关系的类。当然,这种依赖关系将存储在连接表中。
-如果你的软件是为治疗流浪狗并被领养的老板而开发的,那么你的设计应该是这样的:任何狗都可能没有老板,任何老板也可能没有任何狗,直到被领养。在这种情况下,我们的OO设计需要关注这种特殊情况。这种情况有点类似于第一种情况。因此,我们可以将任何类的集合添加到另一个类中。但是,任何软件都需要像这样的需求来影响其他需求,例如报告。如果兽医关心老板领养了哪些狗,迟早他会要求一份报告,其中包括由谁领养了哪只狗。正如句子中所述,“(由老板领养的狗)”,如果狗类包含老板类的集合,那么会更好。

1
这确实是一个观点问题,我在提问后一段时间才注意到。 - Natrium

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接