在上一篇博客《【Django 016】Django2.2数据模型关系之外键一对多(ForeignKey)》中,我们了解了外键实现的一对多模型关系。这一篇我们来一起看看更复杂也更实用的第三种模型关系:多对多。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
文章目录
多对多使用场景
针对两个现有的表,通过第三张表来描述其组合关系,其中被描述的组合不能重复。例如两张表cake
和topping
分别表示蛋糕和装饰,一个蛋糕可以有很多种装饰,每种装饰又可以被放到多种蛋糕上。通过第三张表styles
来给每种蛋糕和装饰的组合起一个名字,并标明售价。
多对多SQL原理
在第三张表中,设计两列分别是指向两张表主键的外键,并且这两列的组合需要有唯一限制,或者直接设为主键。
多对多实例操作
Django给我们提供了自带的多对多实现方法,但是功能十分有限,生产环境需要自己来维护这个多对多关系表。我们就用Django自带的多对多表来理解基本原理和操作,然后试着自己创建一个自定义表。
Django自带的多对多
声明关系
Django通过models.ManyToManyField()
来实现多对多关系。
创建两个数据模型如下
class Cake(models.Model):
c_name = models.CharField(max_length=16, unique=True)
class Topping(models.Model):
t_name = models.CharField(max_length=16, unique=True)
t_cakes = models.ManyToManyField(Cake)
因为是多对多,所以在两个模型中任意一个模型来声明都可以,声明的表叫从表,被声明的表叫主表。
完成迁移以后发现,除了刚才定义的两张表,还多出来了一张表,其DDL如下
create table Four_topping_t_cakes
(
id int auto_increment
primary key,
topping_id int not null,
cake_id int not null,
constraint Four_topping_t_cakes_topping_id_cake_id_06274758_uniq
unique (topping_id, cake_id),
constraint Four_topping_t_cakes_cake_id_c167177d_fk_Four_cake_id
foreign key (cake_id) references Four_cake (id),
constraint Four_topping_t_cakes_topping_id_8569c521_fk_Four_topping_id
foreign key (topping_id) references Four_topping (id)
);
其中cake_id
是指向Cake
表主键的外键,topping_id
是指向Topping
表主键的外键,并且这两列的组合有unique
限制。
再反过来看Topping
这个表的DDL,发现并没有像之前一对一或者一对多那样为声明的属性单独创建一个字段
create table Four_topping
(
id int auto_increment
primary key,
t_name varchar(16) not null
);
添加数据
创建下面两个路由和view函数,分别用来添加cake和topping
path('addcake/', views.addcake, name='add_cake'),
path('addtopping/', views.addtopping, name='add_topping'),
def addcake(request):
name = request.GET.get('name')
cake = Cake()
try:
cake.c_name = name
cake.save()
except Exception as e:
return HttpResponse('Duplicate cake, please try another name')
return HttpResponse('Add cake {} successfully - ID:{}'.format(cake.c_name, str(cake.id)))
def addtopping(request):
name = request.GET.get('name')
topping = Topping()
try:
topping.t_name = name
topping.save()
except Exception as e:
return HttpResponse('Duplicate topping, please try another name')
return HttpResponse('Add topping {} successfully - ID:{}'.format(topping.t_name, str(topping.id)))
往两个表中添加多个数据备用。
然后创建一个路由和view函数用来进行添加组合关系
path('bind/', views.bind, name='bind'),
def bind(request):
cake_id = request.GET.get('cake_id')
topping_id = request.GET.get('topping_id')
topping_list = Topping.objects.filter(id=topping_id)
if topping_list.exists():
cake_list = Cake.objects.filter(id=cake_id)
if cake_list.exists():
cake = cake_list.first()
topping = topping_list.first()
topping.t_cakes.add(cake)
return HttpResponse('Bind successfully')
else:
return HttpResponse('Did not find the topping')
else:
return HttpResponse('Did not find the cake')
重点来了,和之前一对多或者一对一中直接将另一个表的实例拿来赋值给字段不同,这里使用了add()
方法来添加。此时访问类似http://127.0.0.1:8000/four/bind/?cake_id=5&topping_id=1
的url就可以进行绑定了。
这个add()
不仅可以添加单个实例,还可以一次性添加多个实例。修改一下上面的bind
方法,就可以完成多对多的一次性批量绑定。这里使用了*
来将list转换为多个参数。
def bind(request):
cake_ids = request.GET.getlist('cake_id')
topping_ids = request.GET.getlist('topping_id')
response = HttpResponse()
for topping_id in topping_ids:
topping_list = Topping.objects.filter(id=topping_id)
if topping_list.exists():
topping = topping_list.first()
try:
topping.t_cakes.add(*cake_ids)
response.write('topping {} bind successfully<br/>'.format(topping_id))
except Exception as e:
return HttpResponse('Add cake failed')
else:
return HttpResponse('Did not find the topping id {}'.format(topping_id))
response.flush()
return response
此时访问类似http://127.0.0.1:8000/four/bind/?cake_id=4&cake_id=5&topping_id=3&topping_id=1
的url就可以完成批量绑定。
关于这里的getlist,可以参考我的另一篇博客《【Django 010】Django2.2试图函数详解(二):通过HttpRequest对象获取GET和POST传递内容》
查询数据
这里也同样分为从表查询和主表查询
从表查询主表
和之前一对一和一对多一样,从表查询主表使用的是显性属性。
创建路由和view函数如下
path('check_cakes/', views.check_cakes, name='check_cakes'),
def check_cakes(request):
topping_id = request.GET.get('topping_id')
topping_list = Topping.objects.filter(id=topping_id)
if topping_list.exists():
topping = topping_list.first()
cakes = topping.t_cakes.all()
return render(request, 'four_cakes.html', context={'cakes': cakes})
else:
return HttpResponse('Did not find the topping id')
这里的topping.t_cakes
是一个Manager
子类,可以用类似objects
的操作返回QuerySet
类型数据。其中的h5页面如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Cakes</title>
</head>
<body>
<h2>Cakes:</h2>
<ul>
{% for cake in cakes %}
<li>{{ cake.c_name }}</li>
{% endfor %}
</ul>
</body>
</html>
之后就可以用类似http://127.0.0.1:8000/four/check_cakes/?topping_id=4
的查询语句去查询一个topping对应的所有cake了。
主表查询从表
主表查询从表,也是用从表名_set
的方式去访问。
创建路由和view函数如下
path('check_toppings/', views.check_toppings, name='check_toppings'),
def check_toppings(request):
cake_id = request.GET.get('cake_id')
cake_list = Cake.objects.filter(id=cake_id)
if cake_list.exists():
cake = cake_list.first()
toppings = cake.topping_set.all()
return render(request, 'four_toppings.html', context={'toppings': toppings})
else:
return HttpResponse('Did not find the cake id')
和一对多一样,这里的topping_set
也是一个Manager
的子类。
其中的h5页面如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Toppings</title>
</head>
<body>
<h2>Toppings:</h2>
<ul>
{% for topping in toppings %}
<li>{{ topping.t_name }}</li>
{% endfor %}
</ul>
</body>
</html>
之后就可以用类似http://127.0.0.1:8000/four/check_toppings/?cake_id=4
的查询语句去查询一个cake对应的所有topping了。
删除组合关系
前面添加数据的时候使用add()
方法来进行绑定,利用类似原理,也可以使用remove()
方法来进行解绑。
创建如下路由和view函数来删除绑定关系
path('delete_bind/', views.delete_bind, name='delete_bind'),
def delete_bind(request):
topping_id = request.GET.get('topping_id')
cake_id = request.GET.get('cake_id')
try:
topping = Topping.objects.get(id=topping_id)
cake = Cake.objects.get(id=cake_id)
topping.t_cakes.remove(cake)
except Exception as e:
return HttpResponse('Delete bind failed')
return HttpResponse('Delete successfully')
删除绑定关系只是会修改第三张表的记录,并不会改变原表的记录,这是和前面一对一和一对多最明显的区别。
之后就可以利用类似http://127.0.0.1:8000/four/delete_bind/?topping_id=2&cake_id=5
的url来进行解绑定了。
删除记录
多对多的关系虽然也有主表和从表,但是并没有之前一对一和一对多中on_delete
的区别。不管是主表还是从表,只要删除一条记录,与之相关的所有组合都会被相应删除。
创建如下路由和view函数来删除topping记录
path('delete_topping/', views.delete_topping, name='delete_topping'),
def delete_topping(request):
topping_id = request.GET.get('topping_id')
topping_list = Topping.objects.filter(id=topping_id)
if topping_list.exists():
topping = topping_list.first()
topping.delete()
return HttpResponse('Delete successfully')
else:
return HttpResponse('Did not find the topping id')
通过类似http://127.0.0.1:8000/four/delete_topping/?topping_id=1
的url来执行删除操作
创建如下路由和view函数来删除cake记录
path('delete_cake/', views.delete_cake, name='delete_cake'),
def delete_cake(request):
cake_id = request.GET.get('cake_id')
cake_list = Cake.objects.filter(id=cake_id)
if cake_list.exists():
cake = cake_list.first()
cake.delete()
return HttpResponse('Delete successfully')
else:
return HttpResponse('Did not find the cake id')
通过类似http://127.0.0.1:8000/four/delete_cake/?cake_id=1
的url来执行删除操作
自己维护一个多对多关系表
Django提供的多对多关系过于简单,实际开发中可以直接在Django基础上进行源码修改,或者直接套用其逻辑自己来维持一张关系表。这里用自己维护关系表的方法来实现。
创建关系
创建两个模型,分别是Book
和Article
,每本书都会有多个文章,每篇文章也可以发表在多本书中,从而形成多对多得关系。
class Book(models.Model):
b_name = models.CharField(max_length=16)
class Article(models.Model):
a_name = models.CharField(max_length=16)
然后模仿前面Django中第三张表的逻辑生成第三张表
class Publish(models.Model):
book_id = models.ForeignKey(Book, models.CASCADE)
article_id = models.ForeignKey(Article, models.CASCADE)
is_publish = models.BooleanField(default=True)
class Meta:
constraints = [
models.UniqueConstraint(fields=['book_id', 'article_id'], name='unique_publish'),
]
这里两个字段分别指向Book
和Article
的主键的外键,并且在Meta
中添加组合唯一的限制。同时根据自己需求添加了一个是否最后被发行的标志位,默认是发行。这就是自己维护一张表的好处,可以自定义字段。
关于Django2.2中新添加的constraints可以参考官方文档
其DDL如下
create table Four_publish
(
id int auto_increment
primary key,
is_publish tinyint(1) not null,
article_id_id int not null,
book_id_id int not null,
constraint unique_publish
unique (book_id_id, article_id_id),
constraint Four_publish_article_id_id_b81aa4a2_fk_Four_article_id
foreign key (article_id_id) references Four_article (id),
constraint Four_publish_book_id_id_15036214_fk_Four_book_id
foreign key (book_id_id) references Four_book (id)
);
迁移以后生成三张表,以备使用。
添加数据
手动在Book
和Article
中添加几条数据,然后创建下面的路由和view函数来添加绑定关系
path('add_publish/', views.add_publish, name='add_publish'),
def add_publish(request):
article_id = request.GET.get('article_id')
book_id = request.GET.get('book_id')
publish = Publish()
try:
publish.article_id = Article.objects.get(id=article_id)
publish.book_id = Book.objects.get(id=book_id)
publish.save()
return HttpResponse('Add successfully')
except Exception as e:
return HttpResponse('Failed')
之后就可以利用类似http://127.0.0.1:8000/four/add_publish/?article_id=2&book_id=1
来添加绑定关系了。
删除数据
如果只是删除单条绑定关系,只需要删除第三张表中的单条记录即可,这里就不演示了。主要是想看看如果删除其中一张表的一条数据,关系表中对应的组合关系是不是全部会自动删除。
创建下面的路由和view函数
path('delete_article/', views.delete_article, name='delete_article'),
def delete_article(request):
article_id = request.GET.get('article_id')
article = Article.objects.get(id=article_id)
article.delete()
return HttpResponse('Delete Successfully')
经过测试,删除一条Article记录,其对应的所有组合关系在第三张表中全部被自动删除了,和Django自带的现象一致。
总结
多对多的模型关系了解完,三种模型关系就全部了解完毕。单张表,多张表都了解了,下一篇我们来一起看看模型的继承,将面向对象特性发挥到极致。