我们都知道对于ManyToMany字段,Django采用的是第三张中间表的方式。通过这第三张表,来关联ManyToMany的双方。下面我们根据一个具体的例子,详细解说中间表的使用。
首先,模型是这样的:
class Person(models.Model): name = models.CharField(max_length=128) def __str__(self): return self.name class Group(models.Model): name = models.CharField(max_length=128) members = models.ManyToManyField(Person) def __str__(self): return self.name
在Group模型中,通过members字段,以ManyToMany方式与Person模型建立了关系。
让我们到数据库内看一下实际的内容,Django为我们创建了三张数据表,其中的app1是应用名。
然后我在数据库中添加了下面的Person对象:
再添加下面的Group对象:
让我们来看看,中间表是个什么样子的:
首先有一列id,这是Django默认添加的,没什么好说的。然后是Group和Person的id列,这是默认情况下,Django关联两张表的方式。如果你要设置关联的列,可以使用to_field参数。
可见在中间表中,并不是将两张表的数据都保存在一起,而是通过id的关联进行映射。
一般情况,普通的多对多已经够用,无需自己创建第三张关系表。但是某些情况可能更复杂一点,比如如果你想保存某个人加入某个分组的时间呢?想保存进组的原因呢?
Django提供了一个through
参数,用于指定中间模型,你可以将类似进组时间,邀请原因等其他字段放在这个中间模型内。例子如下:
from django.db import models class Person(models.Model): name = models.CharField(max_length=128) def __str__(self): return self.name class Group(models.Model): name = models.CharField(max_length=128) members = models.ManyToManyField(Person, through='Membership') def __str__(self): return self.name class Membership(models.Model): person = models.ForeignKey(Person, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE) date_joined = models.DateField() # 进组时间 invite_reason = models.CharField(max_length=64) # 邀请原因
在中间表中,我们至少要编写两个外键字段,分别指向关联的两个模型。在本例中就是‘Person’和‘group’。 这里,我们额外增加了‘date_joined’字段,用于保存人员进组的时间,‘invite_reason’字段用于保存邀请进组的原因。
下面我们依然在数据库中实际查看一下(应用名为app2):
注意中间表的名字已经变成“app2_membership”了。
Person和Group没有变化。
但是中间表就截然不同了!它完美的保存了我们需要的内容。
针对上面的中间表,下面是一些使用例子(以欧洲著名的甲壳虫乐队成员为例):
>>> ringo = Person.objects.create(name="Ringo Starr") >>> paul = Person.objects.create(name="Paul McCartney") >>> beatles = Group.objects.create(name="The Beatles") >>> m1 = Membership(person=ringo, group=beatles, ... date_joined=date(1962, 8, 16), ... invite_reason="Needed a new drummer.") >>> m1.save() >>> beatles.members.all() <QuerySet [<Person: Ringo Starr>]> >>> ringo.group_set.all() <QuerySet [<Group: The Beatles>]> >>> m2 = Membership.objects.create(person=paul, group=beatles, ... date_joined=date(1960, 8, 1), ... invite_reason="Wanted to form a band.") >>> beatles.members.all() <QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>]>
可以使用 add(), create(), 或 set() 创建关联对象,只需指定 through_defaults
参数:
>>> beatles.members.add(john, through_defaults={'date_joined': date(1960, 8, 1)}) >>> beatles.members.create(name="George Harrison", through_defaults={'date_joined': date(1960, 8, 1)}) >>> beatles.members.set([john, paul, ringo, george], through_defaults={'date_joined': date(1960, 8, 1)})
也可以直接创建中间模型实例。并且如果自定义中间模型没有强制设定 (model1, model2)
对的唯一性,调用 remove()
方法会删除所有中间模型的实例:
>>> Membership.objects.create(person=ringo, group=beatles, ... date_joined=date(1968, 9, 4), ... invite_reason="You've been gone for a month and we miss you.") >>> beatles.members.all() <QuerySet [<Person: Ringo Starr>, <Person: Paul McCartney>, <Person: Ringo Starr>]> >>> # remove方法同时删除了两个 Ringo Starr >>> beatles.members.remove(ringo) >>> beatles.members.all() <QuerySet [<Person: Paul McCartney>]>
clear()方法能清空所有的多对多关系。
>>> # 甲壳虫乐队解散了 >>> beatles.members.clear() >>> # 删除了中间模型的对象 >>> Membership.objects.all() <QuerySet []>
一旦你通过创建中间模型实例的方法建立了多对多的关联,你立刻就可以像普通的多对多那样进行查询操作:
# 查找组内有Paul这个人的所有的组(以Paul开头的名字) >>> Group.objects.filter(members__name__startswith='Paul') <QuerySet [<Group: The Beatles>]>
可以使用中间模型的属性进行查询:
# 查找甲壳虫乐队中加入日期在1961年1月1日之后的成员 >>> Person.objects.filter( ... group__name='The Beatles', ... membership__date_joined__gt=date(1961,1,1)) <QuerySet [<Person: Ringo Starr]>
可以像普通模型一样使用中间模型:
>>> ringos_membership = Membership.objects.get(group=beatles, person=ringo) >>> ringos_membership.date_joined datetime.date(1962, 8, 16) >>> ringos_membership.invite_reason 'Needed a new drummer.'
>>> ringos_membership = ringo.membership_set.get(group=beatles) >>> ringos_membership.date_joined datetime.date(1962, 8, 16) >>> ringos_membership.invite_reason 'Needed a new drummer.'
这一部分内容,需要结合后面的模型query,如果暂时看不懂,没有关系。
对于中间表,有一点要注意(在前面章节已经介绍过,再次重申一下),默认情况下,中间模型只能包含一个指向源模型的外键关系,上面例子中,也就是在Membership中只能有Person和Group外键关系各一个,不能多。否则,你必须显式的通过ManyToManyField.through_fields
参数指定关联的对象。参考下面的例子:
from django.db import models 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', through_fields=('group', 'person'), ) class Membership(models.Model): group = models.ForeignKey(Group, on_delete=models.CASCADE) person = models.ForeignKey(Person, on_delete=models.CASCADE) inviter = models.ForeignKey( Person, on_delete=models.CASCADE, related_name="membership_invites", ) invite_reason = models.CharField(max_length=64)
>>> m2 = Membership.objects.create(person=paul, group=beatles, date_joined=date(1960, 8, 1),invite_reason="Wanted to form a band.") Traceback (most recent call last): File "<console>", line 1, in <module> NameError: name 'date' is not defined 博主还有关注吗?这里报错name 'date' is not defined 是什么原因啊?百度了很久也没有解决,M1也是报这个错,我是给DateField加了字段(auto_now_add=True)才成功的
我这里遇到的问题是:我跟上面博主一样多对多时自定义了一张中间表,through和through_fields都加上了,没啥问题,但当我需要用到django自带的admin后台编辑多对多时,把表中 (menu_field = models.ManyToManyField(to=Menu, through="MenuUser", through_fields=('user', 'menu'))) menu_field 字段添加到fieldsets时,报错:[(admin.E013) The value of 'fieldsets[2][1]["fields"]' cannot include the ManyToManyField 'menu_field', because that field manually specifies a relationship model.],诉求解答
我把解决方法,放在本节教程的最后面了。
感谢博主,但展现的方式还是和一对多嵌套的方式一样,我真正需要的是filter_horizontal这样的形式展现,我想应该只能使用自动的M2M了
这个bug不知能否有大神修复一下
members = models.ManyToManyField(Person, through='Membership')自定义中间表,members添加through,以及新建class Membership之后,执行makemigrations与migrate会报错, 报错信息:ValueError: Cannot alter field myapp.Group.members into myapp.Group.members - they are not compatible types (you cannot alter to or from M2M fields, or add or remove through= on M 2M fields) 查找了一些解决办法例如https://stackoverflow.com/questions/26927705/django-migration-error-you-cannot-alter-to-or-from-m2m-fields-or-add-or-remove操作之后并没有解决问题,依旧会报一样的错误,刘老师有空可以帮忙看看吗
同问这个
使用DB Navigator成功连接sqlite3后,更新了models使用py manage.py migrate命令报错 django.db.utils.OperationalError: database is locked 将sqlite3断开连接后 使用命令不会报错成功运行,刘老师,我这样子是不是每一次使用migrate命令都必须断开sqlite3
最后这个显示指定关联这个,蒙圈
我的理解是:在中间表中,有两个字段的外键都指向Person(因为加入的成员必须是人,邀请人也必须是人)。所以在多对多关系指定的时候,Django并不知道他应该关联的是哪两个字段(其实应该是加入的的成员和组之间的关联),因此必须显示指定。 --------------------------------------------------------------------------------------------------------- 也就是说,“邀请人”只不过是“人-组”多对多关系的一个附带的“关系信息”,每一条这样的关系都可以添加一个附加的“邀请人”。 --------------------------------------------------------------------------------------------------------- 其次,我感觉为了避免这样的问题,可以养成习惯:不管“关联关系”表中是否有歧义外键(两个字段都外联Person),最好都在定义多对多关系时,加上through_fields=('group', 'person')。
老师好,我建了两个表,一个是BOOK,一个是PHOTO,相当于是BOOK包含PHOTO的意思,就是一本图书包含多张图片,我希望的是在添加BOOK的同时添加PHOTO,结果在ADMIN后台的时候,添加BOOK新记录的时候,里面的PHOTO字段会自动添加来自PHOTO表的所有记录,,请问一下,有什么办法解决这个问题吗,我想新加BOOK记录的时候,PHOTO这个记录是空的 class Photo(models.Model): name=models.CharField(max_length=20,verbose_name='书名') title=models.CharField(max_length=20) imgurl = models.ImageField(upload_to='/',verbose_name='图片地址') class Book(models.Model): name=models.CharField(max_length=20,verbose_name='书名') content=models.CharField(max_length=20,verbose_name='内容') photo = models.ManyToManyField(Photo, verbose_name='图片表')
一般的情况是先有PHOTO再有BOOK,我这儿是相反了,先有了BOOK后有了PHOTO,
从你的模型来看是不会出现这种情况的。多对多字段默认是空的。再检查一下别的代码吧。
中间表不能在admin里面看到他具体的字段吗?我试着注册后,发现在admin里,点开一个中间模型对象,就会显示访问出错诶
中间表也可以查看。你再检查下自己的代码。
请问博主,数据表的可视化是在哪里看的?
pycharm内置的一个数据库可视化工具,一半停留在界面右边侧边栏的上部。
博主,您好!我在进行第一步“默认中间表”时遇到了一个问题,我在postgresql中为person和group添加了几个对象,但是中间表group_members并未自动生成表项,请问是什么原因啊?小白求教。。。。
是我忘记添加了。。。
我也遇到了这个问题,请问该怎么解决呢
from django.db import models # Create your models here. class Person(models.Model): name = models.CharField(max_length=128) def __str__(self): return self.name class Group(models.Model): name = models.CharField(max_length=128) members = models.ManyToManyField(Person,through='Membership') def __str__(self): return self.name class Membership(models.Model): person = models.ForeignKey(Person,on_delete=models.CASCADE) group = models.ForeignKey(Group,on_delete=models.CASCADE) data_join=models.DateField() invite_reason = models.CharField(max_length=200)
ValueError: Cannot alter field mo.Group.members into mo.Group.members - they are not compatible types (you cannot alter to or from M2M fields, or add or remove through= on M2M field s)
111
pycharm中有一个数据库访问工具,需要额外安装一些插件。
from datetime import date 然后下面的才能运行 m2 = Membership.objects.create(person=paul, group=beatles,date_joined=date(1960, 8, 1),invite_reason="Wanted to form a band.")
谢谢 我也遇到这个问题
ringo.membership_set.get(group=beatles) 这个是怎么实现的,明明会提示'Membership' object has no attribute 'membership_set'
ringo应该是Person
请问博主,如果我的需要建立多对多关系的两个表均有几千甚至上万条数据时,该如何添加数据呢?
如果是类似excel、cvs等格式化数据,那方便得很,有现成的工具。 如果是非格式化数据,甚至不完整的数据,那就自己想办法了,最差的情况就是手工录入。
博主你好,我是EXCEL格式的数据 请问应该用什么工具呢
pip install django-import-export 了解下
可能练习的比较少,有没有比较系统的django的orm的crud操作练习一下
这里看起来挺复杂的,不过还不算模糊,算是初识吧,看懂了,用的不熟