模型层-多表操作
多表操作
多表操作一般会涉及到数据库中常见的3种关系
- 一对一 OneToOne
- 多对多 ManyToMany
- 一对多 ForeignKey
接下来就是对初始的模型的准备
模型表创建
以图书管理系统为例, 可以很好的展现上面的三种关系.
from django.db import models
class Book(models.Model):
name = models.CharField(max_length=32)
price = models.FloatField()
pub_time = models.DateField(auto_now_add=True)
publish = models.ForeignKey(to='Publish')
authors = models.ManyToManyField(to='Author')
def __str__(self):
return self.name
class Publish(models.Model):
name = models.CharField(max_length=32)
addr = models.CharField(max_length=32)
pub_detail = models.OneToOneField(to='PublishDetail')
def __str__(self):
return self.name
class PublishDetail(models.Model):
email = models.EmailField()
class Author(models.Model):
name = models.CharField(max_length=32)
phone = models.CharField(max_length=20)
def __str__(self):
return self.name
上述表关系的简单分析:
- 书籍与作者是多对多的关系, 这里选择让Django帮我们创建第三张关系表
- 书籍与出版社是一对多的关系, 外键需要建在多的一方
- 出版社与出版社详情是一对一的关系, 外键需要建在查询频率高的表中.
- 在Django2.0之前建立外键关系, 默认是级联删除与更新
利用Navicat的逆向数据库到模型表关系的功能, 看出有以下关系.
注意点:
- 表的名称
myapp_modelName
,是根据 模型中的元数据自动生成的,也可以覆写为别的名称 - 模型表
id
字段是自动添加的 - 对于外键字段,Django 会在字段名上添加
"_id"
来创建数据库中的列名 - Django 会根据settings 中指定的数据库类型来使用相应的SQL 语句。
- 定义好模型之后,需要告诉Django _使用_这些模型。就是修改配置文件中的INSTALL_APPS中设置,在其中添加
models.py
所在应用的名称。 - 外键字段 ForeignKey 有一个 null=True 的设置(它允许外键接受空值 NULL),可以赋给它空值 None 。
添加数据
模型表已经创建好了, 需要为其快速添加数据
import os
import random
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "manytomany.settings")
import django
django.setup()
from app01 import models
# 由于有外键约束, 需要先添加出版社详细信息表, 只创建3个出版社
emails = ['110@qq.com', '120@126.com', '130@163.com']
models.PublishDetail.objects.bulk_create(
[models.PublishDetail(email=email) for email in emails]
)
details = models.PublishDetail.objects.all()
# 创建出版社, 对应于出版社详细信息
infos = [
{'name': '北方出版社', 'addr': '北方'},
{'name': '南方出版社', 'addr': '南方'},
{'name': '东方出版社', 'addr': '东方'},
]
publishes = [models.Publish(pub_detail=detail, **info) for info, detail in zip(infos, details)]
print(publishes)
models.Publish.objects.bulk_create(publishes)
publishes = list(models.Publish.objects.all())
# 创建书籍
infos = [
{'name': '三国演义', 'price': 15.3, 'pub_time': '2018-01-03', 'publish': random.choice(publishes)},
{'name': '水浒传', 'price': 24, 'pub_time': '2018-07-21', 'publish': random.choice(publishes)},
{'name': '西游记', 'price': 17.5, 'pub_time': '2017-01-03', 'publish': random.choice(publishes)},
{'name': '西厢记', 'price': 35.5, 'pub_time': '2019-09-15', 'publish': random.choice(publishes)},
{'name': '红楼梦', 'price': 40, 'pub_time': '2018-06-03', 'publish': random.choice(publishes)},
{'name': '聊斋', 'price': 15.3, 'pub_time': '2019-09-12', 'publish': random.choice(publishes)},
{'name': '封神演义', 'price': 20.9, 'pub_time': '2019-05-12', 'publish': random.choice(publishes)},
{'name': '论语', 'price': 45, 'pub_time': '2019-07-30', 'publish': random.choice(publishes)},
]
models.Book.objects.bulk_create(
[models.Book(**info) for info in infos]
)
# 创建作者表
infos = [
{'name': 'tank', 'phone': '110'},
{'name': 'jason', 'phone': '120'},
{'name': 'egon', 'phone': '130'},
{'name': 'root', 'phone': '140'},
]
models.Author.objects.bulk_create(
[models.Author(**info) for info in infos]
)
# 快速创建关系
authors = list(models.Author.objects.all())
books = list(models.Book.objects.all())
for book in books:
book.authors.add(*random.sample(authors, random.randint(1, len(authors))))
ps: 以上快速创建的过程中, 遇到了一个坑, 使用bulk_create
或 create
添加数据时, 返回的对象, 如果我们自己不指定id, 然后直接去使用, 与有外键关系的表创建新对象, 会直接报错. 最后添加成功时, 还是需要重新查询才能够得到有id的对象.
在了解跨表查询的语法之前, 还需要知道关联字段之间如何通过对象实现增删改的操作.
通过对象的增删改
"关联管理器"(RelatedManager)是在一对多或者多对多的关联上下文中使用的管理器。
它存在于下面两种情况:
- 外键关系的反向查询
- 多对多关联关系
简单来说就是在多对多表关系并且这一张多对多的关系表是有Django自动帮你建的情况下,下面的方法才可使用。当第三张关系表是我们自己创建的话, 并且manytomany字段的through
指定的是我们自己的表, 那么下面的方法都不可使用.
此外, 需要注意的是下面的方法都是需要获取到具体对象才可使用, 不要在QuerySet对象上使用下面这些方法.
方法
create()
创建一个关联对象,并自动写入数据库,且在第三张双方的关联表中自动新建上双方对应关系。
>>> author = models.Author.objects.filter(pk=2).first()
>>> author.book_set.create(name='book1', price=14.4, pub_time='2000-01-01', publish_id=2)
<Book: book1>
1. 先获取到作者对象
2. 由作者对象反向查询跨到书籍表
3. 新增一本书籍并保存
4. 到作者与书籍的第三张表中新增两者之间的多对多关系并保存
add()
把指定的model对象添加到第三张关联表中, 这是专门创建多对多关系的方法. 需要注意的是add方法是传入多个位置参数.
通过对象添加关系
>>> authors = models.Author.objects.all()
>>> models.Book.objects.last().authors.add(*authors)
通过对象id添加关系
>>> models.Book.objects.last().authors.add(*[1, 2])
set()
更新某个对象在第三张表中的关联对象。不同于上面的add是添加,set相当于重置, 这里要注意set方法需要传入的是一个可迭代对象.
models.Book.objects.last().authors.set([1, 2])
remove()
从关联对象集中移除执行的model对象(移除对象在第三张表中与某个关联对象的关系)
>>> book_obj = models.Book.objects.first()
>>> book_obj.authors.remove(3)
clear()
从关联对象集中移除一切对象。(移除所有与对象相关的关系信息)
>>> book_obj = models.Book.objects.first()
>>> book_obj.authors.clear()
注意:
对于ForeignKey对象,clear()和remove()方法仅在null=True时存在, 被关联对象也可以通过类似于publish.book_set.add/set
等方法跨表修改关系.
举个例子:
ForeignKey字段没设置null=True时,
class Book(models.Model):
title = models.CharField(max_length=32)
publisher = models.ForeignKey(to=Publisher)
没有clear()和remove()方法:
>>> models.Publisher.objects.first().book_set.clear()
Traceback (most recent call last):
File "<input>", line 1, in <module>
AttributeError: 'RelatedManager' object has no attribute 'clear'
当ForeignKey字段设置null=True时,
class Book(models.Model):
name = models.CharField(max_length=32)
publisher = models.ForeignKey(to=Class, null=True)
此时就有clear()和remove()方法:
>>> models.Publisher.objects.first().book_set.clear()
再次强调:
- 对于所有类型的关联字段,add()、create()、remove()和clear(),set()都会马上更新数据库。换句话说,在关联的任何一端,都不需要再调用save()方法。
查询
基于对象的跨表查询
正向查询: 通过建立有外键字段的表来跨到其他表来查询
1 查询名字为北方出版社的邮箱
>>> models.Publish.objects.filter(name='北方出版社').first().pub_detail.email
'110@qq.com'
2 查询书籍id=3的出版社的名字
>>> models.Book.objects.filter(id=3).first().publish.name
'东方出版社'
3 查询书籍id为7的作者的电话
>>> models.Book.objects.filter(id=7).first().authors.all()
<QuerySet [<Author: jason>, <Author: egon>]>
这里需要注意多对多需要all()来获取所有查询集对象
反向查询: 通过被关联表为起始来查询关联表的数据信息
1 查询邮箱号码为110@qq.com
的出版社的名字
>>> models.PublishDetail.objects.filter(email='110@qq.com').first().publish.name
'北方出版社'
2 查询出版社名字为南方出版社的所有书籍
models.Publish.objects.filter(name='南方出版社').first().book_set.all()
<QuerySet [<Book: 聊斋>]>
3 查询作者电话为110写的书籍名字
>>> models.Author.objects.filter(phone='110').first().book_set.all()
<QuerySet [<Book: 三国演义>, <Book: 红楼梦>, <Book: 聊斋>]>
通过上面的反向查询可以看出来, 多对多或一对多关系的反向查询需要通过表名小写_set
来跨表, 再通过all()
来获取查询结果.
当然你可以通过在 ForeignKey() 和ManyToManyField的定义中设置 related_name 的值来覆写 表名小写_set 的名称。例如,如果 Publish model 中做一下更改:
publish = ForeignKey(Book, related_name='bookList')
那么接下来就会如我们看到这般:
# 查询北方出版过的所有书籍
publish=Publish.objects.filter(name="人民出版社")
book_list=publish.bookList.all() # 与人民出版社关联的所有书籍对象集合
基于双下划线的跨表查询
Django 还提供了一种直观而高效的方式在查询(lookups)中表示关联关系,它能自动确认 SQL JOIN 联系。要做跨关系查询,就使用两个下划线来链接模型(model)间关联字段的名称,直到最终链接到你想要的model 为止.
它的查询规则与基于对象的查询规则类似, 正向查询按字段, 反向查询按表名小写.
1 查询北方出版社的邮箱地址
# 正向查询
>>> models.Publish.objects.filter(name='北方出版社').values('pub_detail__email')
<QuerySet [{'pub_detail__email': '110@qq.com'}]>
# 反向查询
>>> models.PublishDetail.objects.filter(publish__name='北方出版社').values('email')
<QuerySet [{'email': '110@qq.com'}]>
2 查询南方出版社出版的所有的书
# 正向查询
>>> models.Book.objects.filter(publish__name='南方出版社').values('name', 'price')
<QuerySet [{'name': '聊斋', 'price': 15.3}]>
# 反向查询
>>> models.Publish.objects.filter(name='南方出版社').values('book__name', 'book__price')
<QuerySet [{'book__name': '聊斋', 'book__price': 15.3}]>
3 查询作者名字为jason
的所有书
# 正向查询
>>> models.Book.objects.filter(authors__name='jason').values('name', 'price')
<QuerySet [{'name': '水浒传', 'price': 24.0}, {'name': '西游记', 'price': 17.5}, {'name': '西厢记', 'price': 35.5}, {'name': '红楼梦', 'price': 40.0}, {'name': '封神演义', 'price': 20.9}]>
# 反向查询
>>> models.Author.objects.filter(name='jason').values(书名=F('book__name'), 价格=F('book__price'))
<QuerySet [{'书名': '水浒传', '价格': 24.0}, {'书名': '西游记', '价格': 17.5}, {'书名': '西厢记', '价格': 35.5}, {'书名': '红楼梦', '价格': 40.0}, {'书名': '封神演义', '价格': 20.9}]>
当查询出来的名字太长的时候, 我们可以使用F类来为变量去别名.
聚合查询
聚合查询的原理与mysql中常用的5个聚合函数sum, avg, max, min, count
一样, 一般是作用于分组上的数据的. 它们的位置位于from django.db.models import Avg, Sum, Max, Min, Count
在Django中, 利用到的是QuerySet对象中的aggregate()方法, 它也是一个终止语句, 返回值是包含键值对的字典.
>>> models.Book.objects.all().aggregate(Avg('price'), Max('price'), Count('pk'), Avg('price'), Sum('price'))
{'price__avg': 26.687500000000004, 'price__max': 45.0, 'pk__count': 8, 'price__sum': 213.50000000000003}
分组查询
annotate()为调用的QuerySet
中每一个对象都生成一个独立的统计值(统计方法用聚合函数)。
总结 :跨表分组查询本质就是将关联表join成一张表,再按单表的思路进行分组查询。
查询的例子:
1 统计每一本书作者个数
>>> models.Book.objects.annotate(c=Count('authors')).values('name', 'c')
<QuerySet [{'name': '三国演义', 'c': 2}, {'name': '水浒传', 'c': 1}, {'name': '西游记', 'c': 3}, {'name': '西厢记', 'c': 1}, {'name': '红楼梦', 'c': 4}, {'name': '聊斋', 'c': 1}, {'name': '封神演义', 'c': 2}, {'name': '论语', 'c': 1}]>
2 统计每一个出版社的最便宜的书
>>> models.Publish.objects.annotate(price=Min('book__price')).values('name', 'price')
<QuerySet [{'name': '东方出版社', 'price': 15.3}, {'name': '北方出版社', 'price': 20.9}, {'name': '南方出版社', 'price': 15.3}]>
3 统计每一本以西
开头的书籍的作者个数
>>> models.Book.objects.filter(name__startswith='西').annotate(c=Count('authors')).values('name', 'c')
<QuerySet [{'name': '西游记', 'c': 3}, {'name': '西厢记', 'c': 1}]>
4 统计每一个作者的数的数目
>>> models.Author.objects.annotate(c=Count('book')).values('name', 'c')
<QuerySet [{'name': 'tank', 'c': 3}, {'name': 'jason', 'c': 5}, {'name': 'egon', 'c': 4}, {'name': 'root', 'c': 3}]>
5 统计不止一个作者的图书
>>> models.Book.objects.annotate(c=Count('authors')).filter(c__gt=1).values('name', 'c')
<QuerySet [{'name': '三国演义', 'c': 2}, {'name': '西游记', 'c': 3}, {'name': '红楼梦', 'c': 4}, {'name': '封神演义', 'c': 2}]>
6 根据一本图书作者数量的多少对查询集 QuerySet
进行排序
>>> models.Book.objects.annotate(c=Count('authors')).order_by('-c').values('name', 'c')
<QuerySet [{'name': '红楼梦', 'c': 4}, {'name': '西游记', 'c': 3}, {'name': '封神演义', 'c': 2}, {'name': '三国演义', 'c': 2}, {'name': '水浒传', 'c': 1}, {'name': '论语', 'c': 1}, {'name': '聊斋', 'c': 1}, {'name': '西厢记', 'c': 1}]>
7 查询各个作者出的书的总价格
models.Author.objects.annotate(s=Sum('book__price')).values('name', 's')
<QuerySet [{'name': 'tank', 's': 70.6}, {'name': 'jason', 's': 137.9}, {'name': 'egon', 's': 93.69999999999999}, {'name': 'root', 's': 102.5}]>
8 查询每个出版社的名称和书籍个数
>>> models.Publish.objects.annotate(c=Count('book')).values('name', 'c')
<QuerySet [{'name': '北方出版社', 'c': 2}, {'name': '南方出版社', 'c': 1}, {'name': '东方出版社', 'c': 5}]>
F查询
在Django中, 如果需要通过两个字段之间的比较, 那就必须要利用到F查询.
# 新的模型表
class Goods(models.Model):
sale = models.IntegerField()
storage = models.IntegerField()
name = models.CharField(max_length=32)
示例: 查询卖出数大于库存数的水果
>>> from django.db.models import F
>>> models.Goods.objects.filter(sale__gt=F('storage')).values('name', 'sale', 'name')
<QuerySet [{'name': '石榴', 'sale': 200}, {'name': '西瓜', 'sale': 50}, {'name': '桔子', 'sale': 999}, {'name': '葡萄', 'sale': 666}]>
示例2: 将每个商品的售货数增加50
>>> models.Goods.objects.update(sale=F('sale') + 50)
7
Django 支持 F() 对象之间以及 F() 对象和常数之间的加减乘除和取模的操作。基于此可以对表中的数值类型进行数学运算.
Q查询
Django默认支持的查询的条件之间是逻辑与的关系, 如果我们需要改成逻辑或操作, 那么只能通过Django提供的Q查询了.
示例1: 查询卖出数大于400的或库存大于1000的
>>> models.Goods.objects.filter(Q(sale__gt=400) | Q(storage__gt=1000)).values('sale', 'storage')
<QuerySet [{'sale': 1050, 'storage': 2000}, {'sale': 550, 'storage': 4000}, {'sale': 1049, 'storage': 800}, {'sale': 716, 'storage': 555}]>
示例2: 查询库存数小于100或库存数不大于500的商品名字
>>> models.Goods.objects.filter(Q(sale__lt=100) | ~Q(storage__gt=500)).values('name')
<QuerySet [{'name': '香蕉'}, {'name': '石榴'}, {'name': '西瓜'}]>
Q查询的与或非操作分别对应于符号 & | ~
. 使用他们可以完成比较复杂的查询操作