ORM框架

前一篇笔记我们学习了如何定义Django ORM的数据模型,这篇笔记我们继续学习如何使用Django ORM进行增删改查操作。

Django Shell

Django Shell是交互式终端,它可以在不启动Web工程的情况下直接用交互式的Python语句操作数据,执行以下命令可以启动Django Shell。

python3 manage.py shell

结果如下图。

我们后续有关ORM的非事务操作都可以在Django Shell中测试,不过执行Python语句前别忘了导入数据模型,先执行from demo01.models import *

基础增删改查

瞬时态和持久态

和大多数ORM框架类似,Django ORM的数据对象可以分为两种状态,瞬时态和持久态。

瞬时态(Transient):在Django ORM中,当你创建一个模型实例但尚未将其保存到数据库时,该实例处于瞬时态。

持久态(Persistent):当调用save()方法将对象保存到数据库后,该对象变为持久化态。在这个状态下,对象的属性与数据库中的记录相对应。

# 刚刚创建的数据实例处于瞬时态
s = Student(stu_code='0001', name='汤姆', age=8, class_room=None)
# 调用save()保存后模型处于持久态
s.save()
# 我们可以打印s的主键值
print(s.pk)

注:pk是Django ORM中主键字段的引用(即Primary Key),也可以理解为它是主键的别名,这是因为Django ORM中主键字段名虽然默认为id但也并不是一定如此,我们对数据模型进行操作时,经常使用pk这个统一的别名获取主键。除非某个操作必须指定主键名,我们才会用具体的主键字段名。

查询数据

查询单条记录

前面我们保存了一条数据,这里我们再将其查询出来。get()操作用于返回单个对象,如果查询的结果没有对象或有多个对象将引发异常,我们通常都用它基于主键获取数据记录。下面例子中,我们查询主键为1的学生对象。

Student.objects.get(pk=1)

查询全部

如果想查询全部学生,可以使用如下代码。

Student.objects.all()

遍历QuerySet

all()操作返回的是一个QuerySet对象,它是可迭代的,我们可以使用for循环遍历它获取查询的全部结果。

students = Student.objects.all()
# 遍历QuerySet对象
print(*[obj.name for obj in students], sep="\n")

条件查询

filter()操作用于条件查询,下面例子我们使用name字段作为条件,查询符合条件的数据记录。

Student.objects.filter(name='汤姆')

链式调用

filter()操作的返回值也是QuerySet对象,其实QuerySet对象之后还可以进行filter()操作,这就会形成一种链式的代码结构,配合分支逻辑、聚合分组以及后面介绍的Q查询,我们其实可以轻松组装出复杂的动态查询逻辑。

Student.objects.filter(name='汤姆').filter(age=8)

排序

order_by()操作用于排序,下面例子我们按照age字段升序排序。

Student.objects.all().order_by('age')

如果需要逆序排序,我们需要在排序字段前加-符号。

Student.objects.all().order_by('-age')

聚合

Django提供了多种聚合函数,包括CountAvgMaxMinSum等,下面例子我们使用Count函数统计学生总数。首先我们从django.db.models引入相关的聚合函数。

from django.db.models import Count

下面具体执行了聚合操作,aggregate()操作中我们传入了一个命名参数total=Count('id'),其中total是我们自定义的聚合结果列名,Count()是聚合函数,'id'是聚合的数据模型字段,aggregate()操作的返回值是字典类型。

Student.objects.aggregate(total=Count('id'))

分组

Django ORM中annotate()操作用于分组,分组操作通常和聚合操作一起使用,下面例子中我们按教室对学生分组,统计每个教室中的学生数。

Student.objects.values('class_room').annotate(total=Count('id'))

代码中,values()用于生成一个字典查询集,允许指定要从数据库中检索的字段,它常用于指定分组字段,annotate()用于指定具体的分组操作,命名参数total=Count('id')用法与聚合操作类似。

Q查询

Django ORM中,Q查询用于组合生成复杂的WHERE查询条件子句,下面是一个例子,查询年龄为8或9岁的学生。

from django.db.models import Q
Student.objects.filter(Q(age=8) | Q(age=9))

对于OR条件,使用|操作符连接两个Q对象;对于AND条件,使用&操作符连接两个Q对象;对于NOT条件,使用~操作符,写法形如~Q(age=11)

更新数据

对于持久态的数据模型实例,我们修改某个字段后再次调用save()方法即可将修改保存到数据库。

tom = Student.objects.get(pk=1)
tom.age = 9
tom.save()

删除数据

对于持久态的数据模型实例,我们可以调用delete()方法将其删除。

# 这里s是持久态数据模型实例
s.delete()

当然,有时我们需要实现“条件删除”,这可以基于QuerySet对象实现。这其实属于一种批量操作,后文还会有相关介绍。

Student.objects.filter(age=9).delete()

注意,以上代码并没有真的先查询再删除,Django ORM会自动处理这种情况,在底层生成DELETE FROM ... WHERE ...语句。

懒加载

Django ORM在查询时永远都是懒加载的,查询并不会立即执行,而是在你实际需要数据时才执行,这样可以提高性能和减少不必要的数据库查询。下面两行代码能够说明这一特性,第一行虽然看上去我们获取了QuerySet,但实际上代码执行到这一行时并没真的查询数据库,而是第二行计算数据总记录数时,SQL才真的被生成和执行。

# 这行代码并没有真的查询数据库
students = Student.objects.all()
# 触发查询数据库
len(students)

关联关系插入、更新和删除

前面我们进行的都是所谓的单表操作,没有涉及多表关联的情况,Django ORM中多表关联操作也非常简单,下面例子中我们插入学生和教室的关联关系。

room = ClassRoom.objects.get(pk=1)
lucy = Student(stu_code='0004', name='露西', age=11, class_room=room)
lucy.save()

代码中,主键为1的教室对象是从数据库中查询得到的持久态对象,随后我们新建了学生对象,并将教室对象赋值给学生的class_room字段,这样在保存学生对象时,Django ORM就会自动处理关联关系,将学生和教室关联起来。设置关联关系时,我们需要保证对方处于持久态,这样Django才知道如何维护数据库中的外键关联。对于更新操作也是类似的,只需要修改关联关系对应的数据模型字段即可。

至于删除操作就稍微复杂一点了,下面代码我们删除了一个教室对象,然而关联该教室对象的学生对象如何处理呢?

room = ClassRoom.objects.get(pk=1)
room.delete()

前面我们介绍数据模型定义时提到过models.OneToOneFieldmodels.ForeignKeyon_delete参数,关联删除操作都涉及数据模型字段定义中的on_delete参数,下面我们介绍几种常见的关联删除操作。

models.DO_NOTHING:关联记录被删除时,外键字段仍保持不变,不执行任何操作。这也意味着如果你删除了一个对象,而仍然有其他对象引用它,数据库将保持这种状态,数据产生了不一致性。当然,有时其实我们的业务逻辑是允许这种情况存在的,我们可能会从其它角度处理引用关系,或者在应用层面进行相应的检查和处理。

models.CASCADE:关联记录被删除时,所有引用该对象的对象也会被删除。例如,我们删除一个教室对象,所有属于该教室的学生对象也会被删除。

models.PROTECT:阻止删除关联对象,如果存在引用该对象的其他对象,将抛出异常。

models.SET_NULLmodels.SET_DEFAULT:前者在关联记录被删除时将外键字段置为NULL,后者将外键字段置为默认值。需要注意的是,这两种方式要求外键字段允许为NULL或者有默认值。

以上几种关联删除操作都比较常用,我们需要根据我们的业务场景选择合适的操作。

回到最开始的问题,前面我们定义过学生对象中的教室关联关系,外键字段定义中on_delete=models.DO_NOTHING表示关联记录被删除时,外键字段仍保持不变,不执行任何操作。也就是说,如果教室对象被删除,关联该教室对象的学生对象并不会被删除,学生对象中的教室字段保持不变。

class_room = models.ForeignKey(ClassRoom, null=True, on_delete=models.DO_NOTHING, verbose_name='所在教室')

关联查询

关联查询相对复杂一些,下面我们介绍几种常见的关联查询方式。

根据外键查询

下面例子中,我们查询了学生对象,并获取了关联的教室对象。

tom = Student.objects.get(pk=1)
tom.class_room

反向查询

假如现在我们有教室对象,但我们也知道一对多关系中,外键在“多”的一方维护,此时如何获取所有关联的学生对象呢?Django ORM也提供了相应的写法。

room = ClassRoom.objects.get(pk=1)
room.student_set.all()

Django ORM会自动维护一个反向管理器,名字为<模型类名小写>_set,通过这个反向管理器我们可以方便地获取关联的对象集合。上面代码会得到学生对象的QuerySet。此外,这个名字也可以用related_name手动指定,下面是一个例子。

class_room = models.ForeignKey(ClassRoom, null=True, on_delete=models.DO_NOTHING, related_name='students', verbose_name='所在教室')

上述代码将默认的反向管理器名从student_set覆盖为了students

根据关联对象过滤

假如我们要查询某教室的所有学生,一种方式是先查询教室对象再获取关联的学生,但如果我们就执意要直接查询学生对象,Django ORM提供的filter()操作也支持这种过滤方式。

Student.objects.filter(class_room__room_code='101')

filter支持传入模型中的关联模型字段名__关联模型的字段名命名参数用于实现根据关联对象过滤的功能。

JOIN查询

由于Django ORM具有懒加载特性,我们查询关联对象时,并不会立即执行SQL语句,而是在我们访问关联对象时才会执行SQL语句,这也意味着前面根据外键查询章节的写法其实会发出两条SQL语句,大多数场景下这种写法是够用的,但一些极端情况我们可能不得不用一条SQL语句完成关联查询,这就需要用到select_related()prefetch_related()操作,它们会在底层生成单条JOIN语句。

select_related()用于一对一和一对多查询场景,下面例子中实际底层会执行JOIN查询,我们再访问class_room字段时,其实数据已经查询完成了。

students = Student.objects.select_related('class_room').all()

prefetch_related()也是类似的,不过它用于多对多场景。

teachers = Teacher.objects.prefetch_related('students').all()

由于执行了单条JOIN语句,select_related()prefetch_related()操作指定的字段就不是懒加载的,而是立刻查询出来了。

批量操作

对于类似循环插入多条数据、删除多条数据等场景,优化为批量操作可以大幅提升性能,下面例子执行了批量插入操作。

from django.shortcuts import render
from demo01.models import *


def batch_insert(request):
    desks = [
        Desk(desk_code='D01'),
        Desk(desk_code='D02'),
        Desk(desk_code='D03'),
    ]
    Desk.objects.bulk_create(desks)
    return render(request, 'success.html')

对于批量更新和删除,我们可以直接在查询结果的结果集上调用update()delete()操作,它们会在数据库层面进行批量更新或删除。

from django.db.models import Q
from django.shortcuts import render
from demo01.models import *


def batch_delete(request):
    desks = Desk.objects.filter(Q(desk_code='D01') | Q(desk_code='D02') | Q(desk_code='D03'))
    desks.delete()
    return render(request, 'success.html')

使用原生SQL语句

在OLAP场景下如数据看板、报表等,使用ORM框架可能并不是一个好的方案,基于ORM框架实现OLAP场景给功能和性能都不理想。对于熟悉SQL语言的开发人员来说,这类场景一般我们都是手写SQL比较简单轻松。Django其实也支持直接使用SQL进行查询,这里介绍一种最通用的方法。下面例子函数中,我们执行了一条SELECT语句。

from django.db import connection


def execute_raw_sql():
    with connection.cursor() as cursor:
        cursor.execute("SELECT * FROM demo01_desk WHERE desk_code = %s", ['0001'])
        rows = cursor.fetchall()
        for r in rows:
            print(r)

我们首先获取了游标cursor,然后使用cursor.execute()进行查询,最终使用cursor.fetchall()获取查询的数据,返回结果是一个元组的列表。上面我们直接打印了元组,实际开发中,我们也可以遍历该列表并通过元组的下标获取字段值。

查询结果分页

Django ORM封装了Paginator分页器工具类。下面是一段使用Paginator的例子代码,其中blog_list是通过ORM查询得出的文章集合,我们基于这个QuerySet创建了分页大小为10的分页对象。

from django.core.paginator import Paginator
from django.shortcuts import render
from demo01.models import *


def query_desk_by_page(request):
    cur_page = request.GET.get('page', 1)

    desk_list = Desk.objects.all().order_by('id')
    paginator = Paginator(desk_list, 10)
    page_objects = paginator.page(cur_page)

    return render(request, 'desk.html', {
        'paginator': paginator,
        'page_objects': page_objects
    })

page()方法返回的是一个分页对象,其中包含了分页相关的所有信息,下面模板页面能够说明分页对象的完整用法。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Desk List</title>
</head>
<body>
{% for obj in page_objects %}
    <p>{{ obj.desk_code }}</p>
{% endfor %}

<div class="pagination">
    <span class="step-links">
        {% if page_objects.has_previous %}
            <a href="/demo01/desk?page=1">&laquo; 第一页</a>
            <a href="/demo01/desk?page={{ page_objects.previous_page_number }}">上一页</a>
        {% endif %}

        <span class="current">
            第 {{ page_objects.number }} 页,共 {{ paginator.num_pages }} 页
        </span>

        {% if page_objects.has_next %}
            <a href="/demo01/desk?page={{ page_objects.next_page_number }}">下一页</a>
            <a href="/demo01/desk?page={{ paginator.num_pages }}">最后一页 &raquo;</a>
        {% endif %}
    </span>
</div>
</body>
</html>

其中,page_objects是可迭代的,我们也可以通过page_objects.object_list属性获取QuerySet对象再迭代;page_objects.has_previouspage_objects.previous_page_numberpage_objects.next_page_number通常用于控制分页按钮的显示,它们分别是是否有前一页、前一页的页码和后一页的页码;paginator.num_pages是总页数,page_objects.number是当前页码。

事务控制

Django中,默认的事务行为是自动提交,即每执行一个ORM操作都立即提交(这和完全没开启事务的行为有些类似)。下面例子代码会向数据库插入一条记录,后两条插入数据的代码由于异常而被中断。

from django.shortcuts import render
from demo01.models import *


def transaction_test(request):
    d = Desk(desk_code='T01')
    d.save()
    raise Exception('test')
    d = Desk(desk_code='T02')
    d.save()
    d = Desk(desk_code='T03')
    d.save()
    return render(request, 'success.html')

这种默认行为可能不符合我们的需求,我们也可以手动对事务进行控制。

手动控制事务

Django ORM中我们可以使用transaction模块手动控制事务。transaction.atomic装饰器会创建一个上下文管理器,用于将一组数据库操作包装在一个事务中。只要在装饰器修饰的范围内,所有的数据库操作都会在同一个事务中执行,它们要么全部成功,要么全部失败。

from django.db import transaction
from django.shortcuts import render

from demo01.models import *


@transaction.atomic
def trans_test(request):
    d = Desk(desk_code='T01')
    d.save()
    # 通过抛出异常测试事务回滚
    # raise Exception('test')
    d = Desk(desk_code='T02')
    d.save()
    d = Desk(desk_code='T03')
    d.save()
    return render(request, 'success.html')

除了使用@transaction.atomic装饰器,下面代码中我们直接获取了transaction.atomic()上下文管理器,它和前面代码等价,事务的范围则是with代码块内部。

from django.db import transaction
from django.shortcuts import render

from demo01.models import *


def trans_test(request):
    with transaction.atomic():
        d = Desk(desk_code='T01')
        d.save()
        # 通过抛出异常测试事务回滚
        # raise Exception('test')
        d = Desk(desk_code='T02')
        d.save()
        d = Desk(desk_code='T03')
        d.save()
    return render(request, 'success.html')

Django ORM中,如果一个事务函数调用另一个函数,另一个函数中的数据库操作也会在同一个事务中执行,这种行为被称为事务传播。所有事务都处于同一个事务,根据事务的ACID特性,它们只会全部成功或全部失败。

嵌套事务

如果在一个事务中手动开启了另一个事务,这将产生嵌套事务,这种情况下,调用的内层代码实际上会在一个子事务中执行,其内部如果发生异常,它只会回滚内部的事务操作,如果使用try...except处理了异常,那么外部事务仍能正常提交。但如果外层事务异常,那么所有事务操作都会回滚。

下面代码会成功插入T01、T02、T03三条记录,而T04由于异常被回滚,不会插入到数据库中。

from django.db import transaction
from django.shortcuts import render

from demo01.models import *


def trans_test(request):
    with transaction.atomic():
        try:
            insert_with_nested_transaction()
        except Exception as e:
            print(e)
        d = Desk(desk_code='T01')
        d.save()
        d = Desk(desk_code='T02')
        d.save()
        d = Desk(desk_code='T03')
        d.save()
    return render(request, 'success.html')


def insert_with_nested_transaction():
    with transaction.atomic():
        d = Desk(desk_code='T04')
        # 通过抛出异常测试事务回滚
        raise Exception('test exception')
        d.save()

不过在嵌套事务下,如果外层事务回滚,内层子事务也会回滚。下面例子代码中,虽然子事务成功执行,但仍然回滚了。

from django.db import transaction
from django.shortcuts import render

from demo01.models import *


def trans_test(request):
    with transaction.atomic():
        insert_with_nested_transaction()
        # 通过抛出异常测试事务回滚
        raise Exception('test')
        d = Desk(desk_code='T01')
        d.save()
        d = Desk(desk_code='T02')
        d.save()
        d = Desk(desk_code='T03')
        d.save()
    return render(request, 'success.html')


def insert_with_nested_transaction():
    with transaction.atomic():
        d = Desk(desk_code='T04')
        d.save()

代码中,由于外层事务被回滚,嵌套事务的提交不会影响外层事务的状态。虽然T04的记录在嵌套事务中被保存,但由于外层事务回滚,这个记录也会被撤销。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap