django:多对多关系中的额外字段是否有黑魔法?

1 投票
2 回答
541 浏览
提问于 2025-04-16 23:58

这是来自Django官方文档的内容:https://docs.djangoproject.com/en/dev/topics/db/models/#extra-fields-on-many-to-many-relationships

模型大概是这样的:

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __unicode__(self):
        return self.name

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

    def __unicode__(self):
        return self.name

class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

最后,它给出了一个这样的例子:

# Find all the members of the Beatles that joined after 1 Jan 1961
>>> Person.objects.filter(
...     group__name='The Beatles',
...     membership__date_joined__gt=date(1961,1,1))
[<Person: Ringo Starr]

我想知道,Person模型是怎么知道group和membership这些属性的,因为它们并没有在这个模型里定义。难道这只是many-to-many关系和through的魔力,还是Django里某种通用的特性呢?

如果我要实现同样的查询,我觉得下面的代码更自然(从Django的ORM角度来看,而不是从业务角度):

Membership.objects.filter(group__name='The Beatles', date_joined__gt=date(1961,1,1))).select_related('person')

补充 我又读了一遍文档,发现这种反向查询是通用的。在这一段中,https://docs.djangoproject.com/en/dev/topics/db/queries/#lookups-that-span-relationships提到:

它也可以反向工作。要引用“反向”关系,只需使用模型的小写名称。

我之前从来没有用过这种反向查询。所以第一次看到的时候,真的让我很震惊。抱歉这个讨论看起来有点傻。但我希望这能帮助那些跳过了文中那一小段的人。

2 个回答

1

也许你可以看看这个链接:https://docs.djangoproject.com/en/dev/topics/db/queries/,里面可能有你想要的答案。一般来说,连接(joins)是常用的操作。

2

这不是ManyToMany字段的魔法,而是Django ORM(对象关系映射)在处理外键(包括ManyToMany关系)时的通用特性。

比如,如果你有

class Musician(models.Model):
    name = models.CharField(max_length=128)
    band = models.ForeignKey("Band")

class Band(models.Model):
    name = models.CharField(max_length=128)
    genre = models.CharField(max_length=50)

你可以这样做:Musician.objects.filter(band__name='The Beatles')。这个查询可以通过django-debug-toolbar查看,或者在django的命令行中用:

from django.db import connection
connection.queries

这个查询会变成一个复杂的JOIN语句。ORM生成的SQL查询大概是这样的:

SELECT "appname_musician"."id", "appname_musician"."name", "appname_musician"."band_id" 
    FROM "appname_musician" 
    INNER JOIN "appname_band" ON ("appname_musician"."band_id" = "appname_band"."id") 
    WHERE "appname_band"."name" = "The Beatles";

补充说明:如果你执行了Band.objects.filter(musician__name = 'Ringo Starr'),ORM会把它转换成:

SELECT "appname_band"."id", "appname_band"."name", "appname_band"."genre"
    FROM "appname_band" 
    INNER JOIN "appname_musician" ON ("appname_musician"."band_id" = "appname_band"."id") 
    WHERE "appname_musician"."name" = "Ringo Starr";

ORM知道这些关系,并能把你的ORM查询转换成合适的SQL。即使Band对象里没有musician对象,你仍然可以给Django一个明确的请求:我想要所有的乐队对象,前提是有音乐家和这个乐队关联,名字是'Ringo Starr'。

我觉得你可能在想对象封装的问题(比如,乐队没有音乐家,怎么能根据这个过滤呢?)。其实这并不是黑魔法,因为过滤请求是清晰明确的——它接受的不是Band对象的成员,而是过滤的命令。例如,你可以这样做Band.objects(name__icontains = "The"),即使Band里没有name__icontains这个对象。ORM会把你的请求转换成SQL,这样执行起来很简单。


补充说明2:你的最后一个例子:

class Musician(models.Model):
    name = models.CharField(max_length=128)
    initial_band = models.ForeignKey("Band", related_name = 'initial_band_members')
    final_band = models.ForeignKey("Band", related_name = 'final_band_members')

class Band(models.Model):
    name = models.CharField(max_length=128)
    genre = models.CharField(max_length=50)

try: ## create some objects
    cream,_ = Band.objects.get_or_create(name="Cream", genre="Classic Rock")
    derek, _ = Band.objects.get_or_create(name="Derek and the Dominos", genre="Classic Rock")
    beatles, _ = Band.objects.get_or_create(name="The Beatles", genre="Classic Rock")
    wings, _ = Band.objects.get_or_create(name="Wings", genre="Classic Rock")
    Musician.objects.get_or_create(name="Eric Clapton", initial_band=cream, final_band=derek)
    Musician.objects.get_or_create(name="Paul McCartney", initial_band=beatles, final_band=wings)
    Musician.objects.get_or_create(name="John Lennon", initial_band=beatles, final_band=beatles)

except:
    pass

那么像Band.objects.filter(musician__name='Paul McCartney')这样的查询就会出错(FieldError),但像Band.objects.filter(initial_band_members__name='Paul McCartney')这样的查询会返回The Beatles,并且生成的SQL如下(使用sqlite作为数据库;具体命令取决于数据库后端):

SELECT "testing_band"."id", "testing_band"."name", "testing_band"."genre" 
  FROM "testing_band" 
  INNER JOIN "testing_musician" ON ("testing_band"."id" = "testing_musician"."final_band_id") 
  WHERE "testing_musician"."name" = Paul McCartney '

撰写回答