django:多对多关系中的额外字段是否有黑魔法?
这是来自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 个回答
也许你可以看看这个链接:https://docs.djangoproject.com/en/dev/topics/db/queries/,里面可能有你想要的答案。一般来说,连接(joins)是常用的操作。
这不是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 '