• 模型层-多表操作


    多表操作

    多表操作一般会涉及到数据库中常见的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_createcreate添加数据时, 返回的对象, 如果我们自己不指定id, 然后直接去使用, 与有外键关系的表创建新对象, 会直接报错. 最后添加成功时, 还是需要重新查询才能够得到有id的对象.

    在了解跨表查询的语法之前, 还需要知道关联字段之间如何通过对象实现增删改的操作.

    通过对象的增删改

    "关联管理器"(RelatedManager)是在一对多或者多对多的关联上下文中使用的管理器。

    它存在于下面两种情况:

    1. 外键关系的反向查询
    2. 多对多关联关系

    简单来说就是在多对多表关系并且这一张多对多的关系表是有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()
    

    再次强调:

    1. 对于所有类型的关联字段,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查询的与或非操作分别对应于符号 & | ~. 使用他们可以完成比较复杂的查询操作

  • 相关阅读:
    git基本报错处理
    上传本地文件到gitee
    Pycharm Process finished with exit code -1073741819 (0xC0000005)
    linux ubuntu 安装后的配置
    Anaconda 和 jupyter notebook入门
    LaTeX公式小结--持续更新中
    markdown基本语法
    Python数据类型的整理
    linux磁盘分区与挂载
    第一章linux系统概述
  • 原文地址:https://www.cnblogs.com/yscl/p/11604668.html
Copyright © 2020-2023  润新知