前一篇笔记我们学习了如何定义Django ORM的数据模型,这篇笔记我们继续学习如何使用Django ORM进行增删改查操作。
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()
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提供了多种聚合函数,包括Count
、Avg
、Max
、Min
、Sum
等,下面例子我们使用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')
用法与聚合操作类似。
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.OneToOneField
和models.ForeignKey
的on_delete
参数,关联删除操作都涉及数据模型字段定义中的on_delete
参数,下面我们介绍几种常见的关联删除操作。
models.DO_NOTHING:关联记录被删除时,外键字段仍保持不变,不执行任何操作。这也意味着如果你删除了一个对象,而仍然有其他对象引用它,数据库将保持这种状态,数据产生了不一致性。当然,有时其实我们的业务逻辑是允许这种情况存在的,我们可能会从其它角度处理引用关系,或者在应用层面进行相应的检查和处理。
models.CASCADE:关联记录被删除时,所有引用该对象的对象也会被删除。例如,我们删除一个教室对象,所有属于该教室的学生对象也会被删除。
models.PROTECT:阻止删除关联对象,如果存在引用该对象的其他对象,将抛出异常。
models.SET_NULL和models.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支持传入模型中的关联模型字段名__关联模型的字段名
命名参数用于实现根据关联对象过滤的功能。
由于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')
在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">« 第一页</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 }}">最后一页 »</a>
{% endif %}
</span>
</div>
</body>
</html>
其中,page_objects
是可迭代的,我们也可以通过page_objects.object_list
属性获取QuerySet
对象再迭代;page_objects.has_previous
、page_objects.previous_page_number
、page_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
的记录在嵌套事务中被保存,但由于外层事务回滚,这个记录也会被撤销。