Django循环模型引用

30

我开始着手制作一个小型足球联赛管理网站(主要是为了学习目的),但无法理解Django模型之间的关系。为简单起见,假设我有两种对象 - 球员和球队。自然而然地,一个球员属于一个团队,所以在Player模型中需要一个ForeignKey(Team)。

class Team(models.Model):
    name = models.CharField()
class Player(models.Model):
    name = models.CharField()
    team = models.ForeignKey(Team)

我想让每个团队都有一个队长,这个队长应该是其中一名球员,因此在Team模型中,队长应该是一个ForeignKey(Player)。但是这样会创建一个循环依赖。

尽管我的Django经验有限,但这似乎是一个简单的问题,虽然我无法在概念上弄清楚我做错了什么。

8个回答

58

正如您在文档中所看到的,出于这个原因,可以将外键模型指定为字符串。

team = models.ForeignKey('Team')

2
让我来澄清一下。现在我有: class Team(models.Model): name = models.CharField(max_length=50, unique=True) captain = models.ForeignKey('Player', related_name='captain') class Player(models.Model): name = models.CharField(max_length=50) team = models.ForeignKey(Team)但是这会生成不同的SQL表。"team"表有"captain_id" integer NOT NULL,而"player"表有"team_id" integer NOT NULL REFERENCES "tm_team" ("id")。因此,似乎指定外键模型为字符串会产生不同的结果。 - exfizik
你必须解决循环依赖关系的问题,不是吗?这与Django无关,而与SQL有关。当两个表相互引用时,你如何插入某些内容呢? - middus
2
@middus 嗯,我不知道这种事情是怎么做的,所以才问的。如果唯一的方法是使用这样的变通方法,那我就接受了。然而,我原本以为会有一种不需要变通的方法。 - exfizik
这是官方的做法。正如middus所说,你不能在SQL层面上有循环依赖关系,因此Django必须在模型层面上伪造其中一个依赖关系。从开发者的角度来看,你无法区分两种关系,因为Django以相同的方式强制执行这两种关系。 - Blair
1
这对重构来说太糟糕了。 - Brackets

12

您可以在外键中使用完整的应用程序标签,以引用尚未定义的模型,并使用related_name避免名称冲突:

class Team(models.Model):
    captain = models.ForeignKey('myapp.Player', related_name="team_captain")

class Player(models.Model):
    team = models.ForeignKey(Team)

12

以下是解决该问题的另一种方法。我创建了一个额外的表来存储球员和团队之间的关系,而不是创建一个循环依赖。因此最终结果如下:

class Team(Model):
    name = CharField(max_length=50)

    def get_captain(self):
        return PlayerRole.objects.get(team=self).player

class Player(Model):
    first_name = CharField(max_length=50)
    last_name = CharField(max_length=50, blank=True)

    def get_team(self):
        return PlayerRole.objects.get(player=self).team

PLAYER_ROLES = (
    ("Regular", "Regular"),
    ("Captain", "Captain")
    )

class PlayerRole(Model):
    player = OneToOneField(Player, primary_key=True)
    team = ForeignKey(Team, null=True)
    role = CharField(max_length=20, choices=PLAYER_ROLES, default=PLAYER_ROLES[0][0])
    class Meta:
        unique_together = ("player", "team")

虽然在存储方面可能比建议的解决方法效率略低,但它避免了循环依赖关系,并保持了数据库结构的干净和清晰。

欢迎评论。


但在这种情况下,PlayserRole类将会放在一个不同的文件或应用程序中,对吧? - Gregory

5
这是您正在寻找的内容:
class Team(models.Model):
    name = models.CharField()
    captain = models.ForeignKey('Player')
class Player(models.Model):
    name = models.CharField()
    team = models.ForeignKey(Team)

1
这里的答案都不是很好 - 创建循环引用从来都不是一个好主意。想象一下,如果您的数据库崩溃了,您必须从头开始创建它 - 您将如何在创建团队之前创建球员,反之亦然?看看这里的问题:ForeignKey field with primary relationship,我几天前问过的一个问题。在球员上放置一个指定队长的布尔值,并放置一些预保存钩子,验证每个团队必须有一个且仅有一个队长。

4
如果你需要从头开始重新创建数据库,你只需关闭外键检查,插入数据,然后重新开启外键检查。Django的manage.py dumpdata输出默认执行此操作,因此从Django创建的任何备份都可以直接使用。由于模型天生具有循环引用,因此最好定义它们并让Django处理任何脏活累活,而不是手动使用钩子来完成 - 尤其是因为Django专门为处理此场景而设置。 - Blair
1
我仍然认为,我宁愿拥有一个干净的数据库,也不愿意存在循环关系 - 这对我来说似乎是相反的。 - Ben
我同意@Blair的观点。说“每个玩家必须有一个团队”和“每个团队必须有一些球员和一名队长”是完全合理的。这听起来可能不可行,但约束条件在事务结束时进行检查,因此您可以插入您的团队,插入您的球员,提交事务,然后数据库检查约束条件是否得到验证。这是常见的做法。 - Spycho
1
@Ben - 举个例子怎么样:我们有定义了部门经理的Department对象,以及属于部门的员工对象。现在我们有employee.department = ForeignKey(Department)和department.manager = ForeignKey(Employee)。这不是一个有效的循环关系吗? - Chris

1

在你的其他表格中包含球员/队伍列,有一个“队长”表格,并将队长作为Team的方法:

class Team(models.Model):
    name = models.CharField()
    def captain(self):
      [search Captain table]
      return thePlayer

class Player(models.Model):
    name = models.CharField()
    team = models.ForeignKey(Team)

class Captain(models.Model):
    player = models.ForeignKey(Player)
    team = models.ForeignKey(Team)

你需要检查一下,确保同一个团队中没有超过一个队长...但这样就不会有循环引用了。你也可能会得到一个不在他被标记为队长的团队中的队长。所以这种方式有一些需要注意的地方。


+1 是因为它完全避免了循环引用。出于简单起见,我会将 captain 设为属性。我还会添加一个 setter,并使 team 在 Captain 表中唯一。 - WhyNotHugo

1

虽然将同一模型引用两次没有问题,但也许有更好的方法来解决这个特定的问题。

Team模型中添加一个布尔值,以标识球员+团队组合是否为队长:

class Team(models.Model):
  player = models.ForeignKey(Player)
  name = models.CharField(max_length=50)
  is_captain = models.BooleanField(default=False)

查找团队的队长:

Team.objects.filter(is_captain=True)

个人而言,我不喜欢这种方法,因为搜索语义不合理(即,“团队”不是“队长”)。
另一种方法是识别每个球员的位置:
class Player(models.Model):
   name = models.CharField(max_length=50)
   position = models.IntegerField(choices=((1,'Captain'),(2,'Goal Keeper'))
   jersey = models.IntegerField()

   def is_captain(self):
     return self.position == 1

class Team(models.Model):
   name = models.CharField(max_length=50)
   player = models.ForeignKey(Player)

   def get_captain(self):
      return self.player if self.player.position == 1 else None

当你搜索时,这会更有意义:

Player.objects.filter(position=1)(返回所有队长)

Team.objects.get(pk=1).get_captain()(返回该团队的队长)

无论哪种情况,但是您必须进行一些预保存检查,以确保特定位置只有一个球员。


0

现在可以使用AppConfig功能导入模型:

Retailer = apps.get_model('retailers', 'Retailer')
retailer = Retailer.objects.get(id=id)            

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