多表操作

多表操作

一、概述

在实际应用中,不可能把应用的所有数据都放在一张表中,所以我们需要将数据存放于不同的表中,然后基于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,              # 默认创建第三张表时,数据库中表的名称
View Code

四、添加记录

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,可能
会造成额外的数据库查询。
Query的高效使用
 九、提高性能
select_related和prefetch_related 可以很好的减少数据库请求的次数,从而提高性能。
1.select_related
select_related通过多表join关联查询,一次性获得所有数据,通过降低数据库查询次数来提升性能,但关联表不能太多,因为join操作本来就比较消耗性能。
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处理他们之间的关联。
posted @ 2019-10-27 22:42  流浪代码  阅读(176)  评论(0编辑  收藏  举报