多表操作
多表操作 |
一、概述
在实际应用中,不可能把应用的所有数据都放在一张表中,所以我们需要将数据存放于不同的表中,然后基于foreign key建立表之间的关系。
表之间存在三中关系:一对一、一对多、多对多。
二、创建模型
实例:我们来假定下面这些概念,字段和关系
作者模型:一个作者有姓名,年龄。
作者详细模型:把作者的详情放到详情表,包含性别,email地址和出生日期,作者详情模型和作者模型之间是一对一的关系(one-to-one)(类似于每个人和他的身份证之间的关系),在大多数情况下我们没有必要将他们拆分成两张表,这里只是引出一对一的概念。
出版商模型:出版商有名称,地址。
书籍模型:书籍有书名和出版日期和价格,一本书可能会有多个作者,一个作者也可以写多本书,所以作者和书籍的关系就是多对多的关联关系(many-to-many),一本书只应该由一个出版商出版,所以出版商和书籍是一对多关联关系(one-to-many)。
from django.db import models # Create your models here. class Book(models.Model): #书籍模型 id = models.AutoField(primary_key=True) name = models.CharField(max_length=20) price = models.FloatField() pub_date = models.DateField() publish = models.ForeignKey("Publish") #建立与出版社多对一的关系,注意加引号 authors = models.ManyToManyField("Author") #自动建立与作者多对多的关系 def __str__(self): return self.name class Author(models.Model): #作者模型 name = models.CharField(max_length=32) age = models.IntegerField(default=20) def __str__(self): return self.name #class AuthorDetail(models.Model): #作者详细模型 #sex = models.BooleanField(max_length=1, choices=((0, '男'),(1, '女'),)) #email = models.EmailField() #address = models.CharField(max_length=50) #birthday = models.DateField() #author = models.OneToOneField(Author) class Publish(models.Model): #出版社模型 name = models.CharField(max_length=32) city = models.CharField(max_length=32) def __str__(self): return self.name
分析代码:
(1)每个数据模型都是django.db.models.Model的子类,它的父类Model包含了所有必要的和数据库交互的方法。并提供了一个简介漂亮的定义数据库字段的语法。
(2)每个模型相当于单个数据库表(多对多关系例外,会多生成一张关系表),每个属性也是这个表中的字段。属性名就是字段名,它的类型(例如CharField)相当于数据库的字段类型(例如varchar)。大家可以留意下其它的类型都和数据库里的什么字段对应。
(3)模型之间的三种关系:一对一,一对多,多对多。
- 一对一:实质就是在主外键(author_id就是foreign key)的关系基础上,给外键加了一个UNIQUE=True的属性;
- 一对多:就是主外键关系;(foreign key)
- 多对多:(ManyToManyField) 自动创建第三张表(当然我们也可以自己创建第三张表:两个foreign key)。
三、多表关系以及参数
ForeignKey(ForeignObject) # ForeignObject(RelatedField) to, # 要进行关联的表名 to_field=None, # 要关联的表中的字段名称 on_delete=None, # 当删除关联表中的数据时,当前表与其关联的行的行为 - models.CASCADE,删除关联数据,与之关联也删除 - models.DO_NOTHING,删除关联数据,引发错误IntegrityError - models.PROTECT,删除关联数据,引发错误ProtectedError - models.SET_NULL,删除关联数据,与之关联的值设置为null(前提FK字段需要设置为可空) - models.SET_DEFAULT,删除关联数据,与之关联的值设置为默认值(前提FK字段需要设置默认值) - models.SET,删除关联数据, a. 与之关联的值设置为指定值,设置:models.SET(值) b. 与之关联的值设置为可执行对象的返回值,设置:models.SET(可执行对象) def func(): return 10 class MyModel(models.Model): user = models.ForeignKey( to="User", to_field="id" on_delete=models.SET(func),) related_name=None, # 反向操作时,使用的字段名,用于代替 【表名_set】 如: obj.表名_set.all() related_query_name=None, # 反向操作时,使用的连接前缀,用于替换【表名】 如: models.UserGroup.objects.filter(表名__字段名=1).values('表名__字段名') limit_choices_to=None, # 在Admin或ModelForm中显示关联数据时,提供的条件: # 如: - limit_choices_to={'nid__gt': 5} - limit_choices_to=lambda : {'nid__gt': 5} from django.db.models import Q - limit_choices_to=Q(nid__gt=10) - limit_choices_to=Q(nid=8) | Q(nid__gt=10) - limit_choices_to=lambda : Q(Q(nid=8) | Q(nid__gt=10)) & Q(caption='root') db_constraint=True # 是否在数据库中创建外键约束 parent_link=False # 在Admin中是否显示关联数据 OneToOneField(ForeignKey) to, # 要进行关联的表名 to_field=None # 要关联的表中的字段名称 on_delete=None, # 当删除关联表中的数据时,当前表与其关联的行的行为 ###### 对于一对一 ###### # 1. 一对一其实就是 一对多 + 唯一索引 # 2.当两个类之间有继承关系时,默认会创建一个一对一字段 # 如下会在A表中额外增加一个c_ptr_id列且唯一: class C(models.Model): nid = models.AutoField(primary_key=True) part = models.CharField(max_length=12) class A(C): id = models.AutoField(primary_key=True) code = models.CharField(max_length=1) ManyToManyField(RelatedField) to, # 要进行关联的表名 related_name=None, # 反向操作时,使用的字段名,用于代替 【表名_set】 如: obj.表名_set.all() related_query_name=None, # 反向操作时,使用的连接前缀,用于替换【表名】 如: models.UserGroup.objects.filter(表名__字段名=1).values('表名__字段名') limit_choices_to=None, # 在Admin或ModelForm中显示关联数据时,提供的条件: # 如: - limit_choices_to={'nid__gt': 5} - limit_choices_to=lambda : {'nid__gt': 5} from django.db.models import Q - limit_choices_to=Q(nid__gt=10) - limit_choices_to=Q(nid=8) | Q(nid__gt=10) - limit_choices_to=lambda : Q(Q(nid=8) | Q(nid__gt=10)) & Q(caption='root') symmetrical=None, # 仅用于多对多自关联时,symmetrical用于指定内部是否创建反向操作的字段 # 做如下操作时,不同的symmetrical会有不同的可选字段 models.BB.objects.filter(...) # 可选字段有:code, id, m1 class BB(models.Model): code = models.CharField(max_length=12) m1 = models.ManyToManyField('self',symmetrical=True) # 可选字段有: bb, code, id, m1 class BB(models.Model): code = models.CharField(max_length=12) m1 = models.ManyToManyField('self',symmetrical=False) through=None, # 自定义第三张表时,使用字段用于指定关系表 through_fields=None, # 自定义第三张表时,使用字段用于指定关系表中那些字段做多对多关系表 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) db_constraint=True, # 是否在数据库中创建外键约束 db_table=None, # 默认创建第三张表时,数据库中表的名称
四、添加记录
1.一对多
#一对多(ForeignKey): #方式一: 由于绑定一对多的字段,比如publish,存到数据库中的字段名叫publish_id,所以我们可以直接给这个 #字段设定对应值:Book.objects.create(name="python",price="99",pub_date="2019-10-11",publish_id=2) #这里的2是指为该book对象绑定了Publish表中id=2的行对象,Publish表中必须存在id=2的数据 #方式二: #<1> 先获取要绑定的Publisher对象: publish_obj=Publish.objects.filter(name="人民出版社")[0] #<2>将 publish_id=2 改为 publish=publish_obj Book.objects.create(name="PHP",price="45",pub_date="2019-04-05",publish=publish_obj)
2.多对多
#多对多(ManyToManyField()): book_obj = Book.objects.get(id=1) #author_objs = Author.objects.get(id=2) #book_obj.authors.add(author_objs) author_objs = Author.objects.all() book_obj.authors.add(*author_objs) #添加多对多关系 #注意: 如果第三张表是通过models.ManyToManyField()自动创建的,那么绑定关系只有上面一种方式 #如果第三张表是自己创建的: class Book_Author(models.Model): book = models.ForeignKey("Book") author = models.ForeignKey("Author") class Meta: unique_together=[('book','author'),] #联合唯一 #那么添加方式为: Book_Author.objects.create(book_id=2,author_id=3)
五、删除记录
1.一对多
Book.objects.filter(id=1).delete()
我们表面上删除了一条信息,实际却删除了不止一条,因为我们删除的这本书在Book_authors表中有一些条相关信息,这种删除方式就是django默认的级联删除。
2.多对多
#正向 book = Book.objects.filter(id=1) book.author.clear() #清空与book中id=1 关联的所有数据 book.author.remove(2) #可以为id book.author.remove(*[1,2,3,4]) #可以为列表,前面加* #反向 author = Author.objects.filter(id=1) author.book_set.clear() #清空与author中id=1 关联的所有数据
六、修改记录
#假设python的作者原来为alex,要求设置为liu、lisi author_obj1 = Author.objects.get(name="liu") author_obj2 = Author.objects.get(name="lisi") book_obj = Book.objects.get(name="python") book_obj.authors.set([author_obj1,author_obj2]) #多个作者对象放在列表里
七、查询记录
ORM下多表关联查询时有一个重要的概念:在使用模型类进行多表关联查询时,如果确定两张表存在关联关系,那么在选取一个表作为起始(基表),可以 跨表引用来自另外一张中的字段值,这就存在正向与反向之分,如果关联字段存在基表中,称为正向查询,反之反向查询。
为了便于下面多表查询,我们首先在表中添加一些数据
(1)book表
(2)author表
(3)publish表
(4)book_author表
1.基于对象的跨表查询
#-----------------------一对多------------------------- #查询书籍为python的出版社信息 book_obj = Book.objects.get(name="python") #一对多:book_obj.publish--------->一定是一个对象 print(book_obj.publish.name) #book_obj.publish 书籍对象对应的出版社对象 print(book_obj.publish.city) #查询人民出版社出过的所有书籍名字和价格 #方式一(正向查询) pub_obj = Publish.objects.get(name="人民出版社") ret = Book.objects.filter(publish=pub_obj).values("name","price") print(ret) #方式二(反向查询) pub_obj = Publish.objects.get(name="人民出版社") pub_obj.book_set.all().values("name","price") #是一个Queryset对象,里面是字典形式 #pub_obj.book_set.all() #是一个Queryset对象,里面是一个对象形式 #pub_obj.book_set.all().values_list("name","price") #是一个Queryset对象,里面是元组形式 #-------------------------多对多------------------------------ #正向查找 book_obj = Book.objects.get(id=3) print(book_obj.authors.all()) #反向查找 author_obj = Author.objects.get(id=2) print(author_obj.book_set.all())
2.基于双下划线的跨表查询
#------------------------一对多-------------------------- #查询人民出版社出过的所有书籍名字和价格 # ret = Book.objects.filter(publish__name="人民出版社").values("name","price") # print(ret) # #查询书籍为python的出版社信息 # #方式一(反向查找) # ret2 = Publish.objects.filter(book__name="python").values("name","city") # print(ret2) # #方式二(正向查找) # ret3 = Book.objects.filter(name="python").values("publish__name","publish__city") # print(ret3) # #查询北京的出版社出过的书籍 # ret4 = Book.objects.filter(publish__city="北京").values("name","price") # print(ret4) # #查询价格50到100之间的出版社信息 # ret5 = Book.objects.filter(price__range=[50,100]).values("publish__name","publish__city") # print(ret5) #-----------------------------多对多------------------------------ #每个作者出过书的总价格(反向查找) # ret = Book.objects.values("authors__name").annotate(Sum("price")) # print(ret) #每个出版社最便宜的书籍价格(正向查询) # ret = Publish.objects.values("name").annotate(Min("book__price")) # print(ret) # b= Book.objects.filter(name="GO",price=99) # print(b) # ret = Book.objects.filter(Q(price=66)|Q(name="GO")) # print(ret) # ret = Book.objects.filter(Q(price=66),name="GO") # print(ret) #Book.objects.all().update(price=F("price")+10)
八、惰性机制
所谓惰性机制:Publisher.objects.all()或者.filter()等都只是返回了一个QuerySet(查询结果集对象),它并不会马上执行sql,而是当调用QuerySet的时候才执行。
QuerySet特点:
- 可迭代
- 可切片
#objs=models.Book.objects.all()#[obj1,obj2,ob3...] #QuerySet: 可迭代 # for obj in objs:#每一obj就是一个行对象 # print("obj:",obj) # QuerySet: 可切片 # print(objs[1]) # print(objs[1:4]) # print(objs[::-1])
<1>Django的queryset是惰性的 Django的queryset对应于数据库的若干记录(row),通过可选的查询来过滤。例如,下面的代码会得 到数据库中名字为‘Dave’的所有的人:person_set = Person.objects.filter(first_name="Dave") 上面的代码并没有运行任何的数据库查询。你可以使用person_set,给它加上一些过滤条件,或者将它传给某个函数, 这些操作都不会发送给数据库。这是对的,因为数据库查询是显著影响web应用性能的因素之一。 <2>要真正从数据库获得数据,你可以遍历queryset或者使用if queryset,总之你用到数据时就会执行sql. 为了验证这些,需要在settings里加入 LOGGING(验证方式) obj=models.Book.objects.filter(id=3) # for i in obj: # print(i) # if obj: # print("ok") <3>queryset是具有cache的 当你遍历queryset时,所有匹配的记录会从数据库获取,然后转换成Django的model。这被称为执行 (evaluation).这些model会保存在queryset内置的cache中,这样如果你再次遍历这个queryset, 你不需要重复运行通用的查询。 obj=models.Book.objects.filter(id=3) # for i in obj: # print(i) ## models.Book.objects.filter(id=3).update(title="GO") ## obj_new=models.Book.objects.filter(id=3) # for i in obj: # print(i) #LOGGING只会打印一次 <4> 简单的使用if语句进行判断也会完全执行整个queryset并且把数据放入cache,虽然你并不需要这些 数据!为了避免这个,可以用exists()方法来检查是否有数据: obj = Book.objects.filter(id=4) # exists()的检查可以避免数据放入queryset的cache。 if obj.exists(): print("hello world!") <5>当queryset非常巨大时,cache会成为问题 处理成千上万的记录时,将它们一次装入内存是很浪费的。更糟糕的是,巨大的queryset可能会锁住系统 进程,让你的程序濒临崩溃。要避免在遍历数据的同时产生queryset cache,可以使用iterator()方法 来获取数据,处理完数据就将其丢弃。 objs = Book.objects.all().iterator() # iterator()可以一次只从数据库获取少量数据,这样可以节省内存 for obj in objs: print(obj.name) #BUT,再次遍历没有打印,因为迭代器已经在上一次遍历(next)到最后一次了,没得遍历了 for obj in objs: print(obj.name) #当然,使用iterator()方法来防止生成cache,意味着遍历同一个queryset时会重复执行查询。所以使 #用iterator()的时候要当心,确保你的代码在操作一个大的queryset时没有重复执行查询 总结: queryset的cache是用于减少程序对数据库的查询,在通常的使用下会保证只有在需要的时候才会查询数据库。 使用exists()和iterator()方法可以优化程序对内存的使用。不过,由于它们并不会生成queryset cache,可能 会造成额外的数据库查询。
def select_related(self, *fields) model.tb.objects.all().select_related() model.tb.objects.all().select_related('外键字段') model.tb.objects.all().select_related('外键字段__外键字段')
from app01 import models def test(request): #不带select_related person_list = models.Person.objects.all() # 相当于执行select * from person #带select_related person_list = models.Person.objects.all().select_related('ut') # 相当于执行select * from person left join usertype on person.ut_id = usertype.id for row in person_list: print(row.user,row.id,row.ut_id) # SQL请求 print(row.ut.title)
当不带select_related时,当要用到查询跨表字段,每一次循环就要向数据库发送一次请求。实际生产中每个表的数据肯定是成千上万的,所以传统的操作对数据库的性能影响很大!
当带有select_related时,它已经做好了join关联查询,所有当要用到查询跨表字段,只需要向数据库发送一次请求即可。
总结:
- select_related主要针一对一和多对一关系进行优化。
- select_related使用SQL的JOIN语句进行优化,通过减少SQL查询的次数来进行优化、提高性能。
- 可以通过可变长参数指定需要select_related的字段名。也可以通过使用双下划线“__”连接字段名来实现指定的递归查询(也就是外键的外键,多层连表查询)。没有指定的字段不会缓存,没有指定的深度不会缓存,如果要访问的话Django会再次进行SQL查询。
2.prefetch_related
prefetch_related()和select_related()的设计目的很相似,都是为了减少SQL查询的数量,但是实现的方式不一样。后者是通过JOIN语句,在SQL查询内解决问题。但是对于多对多关系,使用SQL语句解决就显得有些不太明智,因为JOIN得到的表将会很长,会导致SQL语句运行时间的增加和内存占用的增加。prefetch_related()的解决方法是,分别查询每个表,然后用Python处理他们之间的关系!
def prefetch_related(self, *lookups) models.UserInfo.objects.prefetch_related('外键字段')
实例:
models.UserInfo.objects.prefetch_related('ut') # select * from userinfo # 计算获取到的所有用户的用户类型ID [1,] # select * from usertype where id in [1,]
第一步:先拿到userinfo表的所有数据;第二步:通过select .. from ... where ... in 。这样通过分别发起两次请求,获取了userinfo表以及和book表相关联的usertype表的数据(并不是所有usertype表数据,只有和book表相关联数据!),然后通过Python处理数据的对应关联。
总结:
- prefetch_related主要针一对多和多对多关系进行优化。
- prefetch_related通过分别获取各个表的内容,然后用Python处理他们之间的关系来进行优化。
综合总结:
- select_related是通过join来关联多表,一次获取数据,存放在内存中,但如果关联的表太多,会严重影响数据库性能。
- prefetch_related是通过分表,先获取各个表的数据,存放在内存中,然后通过Python处理他们之间的关联。