在Django中,ManyToManyField是常用的Field,实现数据库中多对多模型。
例如,人与团体的关系(一个人可加入多个团体,一个团体有多个人):
class Person(models.Model):
name = models.CharField(max_length=50)
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, related_name='groups')
但是,当我们通过group.members
(或person.groups
)获取一个组织的所有成员(或获取这个人参加的所有组织)时,拿到的数据是按照group_id (或person_id)排列的,有时这样的行为不符合预期:
比如前端展示这个团体的人,希望是按照加入顺序来展示,而非按照人的id排序。
试想,当团队人数很多,你加了一个人后,他不是出现在最后,而是钻在一群人的中央,你很可能找不到他了!甚至怀疑自己有没有添加成功。这种类似的场合很常见
解决方案(无需改变数据库结构)
Django在实现ManyToManyField时,会默默引入一张“中间表”,它默认包括3列:id
、model1_id
、model2_id
。以上面的Person和Group为例,这3列就是:id
、person_id
、group_id
。这个id
是自增索引,当然也意味着:建立的时间晚,id
相对就大。我们可以通过id
来实现“按添加时间排列”,而无需对数据库结构做任何改变!
为了实现它,我们获取数据的方式需要适当改变:
group1 = Group.objects.get(id=1)
# 之前的方式,数据按 person_id 排列
group1.members.all()
# 改变后,数据按添加时间排列
Group.members.through.objects.filter(group=group1).order_by('id').all()
原理是什么?
刚才提到,Django会默默引入一张“中间表”,这张“中间表”其实也是对应着一个隐藏的"Modal"的(生成顺序其实是Django先生成隐藏的Modal,再根据这个Modal生成那个数据库表)。Group.members.through
这句话其实是引用了那个隐藏的"Modal"。它后面的.objects
,就像对待普通Modal一样,对待它就可以了。
“隐藏的Modal”是什么?
在本文开头的例子,隐藏的"Modal"长这个样子:
class Person_Group(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
person = models.ForeignKey(Person, on_delete=models.CASCADE)
它生成的数据库表就是3列:id
、person_id
、group_id
。
其实,这张表你也可以自己定义出来,不让Django自动生成,只是你需要多提供一个参数throuth
给ManyToManyField:
class Person(models.Model):
name = models.CharField(max_length=50)
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Membership')
class Membership(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
person = models.ForeignKey(Person, on_delete=models.CASCADE)
invite_reason = models.CharField(max_length=64)
这里我们给中间表起了名字Membership
,名字随意起,through
参数跟名字是对应的就行。
为什么through是写的字符串'Membership'
,而非直接写Membership
?
因为Python解析器解析类的定义时,会把类的属性、方法都定义完,才会继续解析下一个类(在定义members时,Membership
尚未被解析,此时如果引用它会报错,所以Django面对这种问题的解决方案就是——先用字符串代替,这是很常见的做法)