Django ORM 表达式、事务与模型字段设计
本章关注 ORM 中三个容易影响正确性的问题:如何在数据库中直接比较或计算字段值,如何保证多步写入要么全部成功要么全部撤销,以及如何选择合适的模型字段与约束。掌握这些内容后,可以减少并发更新丢失、数据半写入和字段设计不当等问题。
一、F 表达式:让数据库完成字段比较和计算
F() 表达式表示“数据库中某个字段当前的值”。它不是 Python 中已经取出的具体数值,因此可在一次 SQL 语句内完成字段比较、加减运算与字符串拼接,避免“先读、后改、再存”带来的额外查询和并发覆盖风险。
以下模型用库存、销量与价格演示 F 表达式:
# shop/models.py
from django.db import models
class Book(models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
sale = models.PositiveIntegerField(default=0)
stock = models.PositiveIntegerField(default=0)
比较两个字段
例如,查询销量大于库存的图书,不应把所有记录读取到 Python 后再用循环比较,而应让数据库在 WHERE 条件中完成比较。
from django.db.models import F
overstocked_sales = Book.objects.filter(sale__gt=F("stock"))
代码说明:F("stock") 表示 stock 字段本身。ORM 会生成类似 WHERE sale > stock 的 SQL。与先 Book.objects.all() 再在 Python 中筛选相比,这种写法传输的数据更少,也能让数据库利用查询优化能力。
原子递增或批量更新
库存和计数器是 F 表达式的典型场景。若两个请求同时读取库存 10,各自再保存 9,其中一次更新会丢失;在数据库内执行 stock = stock - 1 则能避免这种“读-改-写”竞争。
from django.db.models import F
# 全部图书涨价 500
Book.objects.update(price=F("price") + 500)
# 仅减少库存大于 0 的图书,返回受影响行数
updated_count = Book.objects.filter(pk=1, stock__gt=0).update(
stock=F("stock") - 1,
sale=F("sale") + 1,
)
if updated_count == 0:
print("库存不足或图书不存在")
代码说明:QuerySet.update() 直接执行 SQL,不会调用模型实例的 save(),也不会触发基于 save() 的自定义逻辑。库存扣减需要把 stock__gt=0 写在同一条更新条件中,才能在并发下保持正确。
若将 F 表达式赋给单个实例,保存后该实例属性仍可能保留 F 对象。需要继续使用新值时,应刷新对象。
book = Book.objects.get(pk=1)
book.sale = F("sale") + 1
book.save(update_fields=["sale"])
book.refresh_from_db(fields=["sale"])
print(book.sale)
字符串拼接与数据库函数
不同数据库对字符串 + 的语义不同。拼接文本时,应使用 Django 提供的数据库函数 Concat() 与 Value(),而不是依赖特定数据库行为。
from django.db.models import F, Value
from django.db.models.functions import Concat
Book.objects.filter(sale__gte=5000).update(
name=Concat(Value("爆款-"), F("name")),
)
代码说明:Value("爆款-") 将普通 Python 字符串包装成 SQL 常量;F("name") 引用原字段值。重复执行这类语句会重复添加前缀,生产代码应增加明确条件,或使用独立状态字段记录是否已标记。
二、Q 对象:构造复杂查询条件
同一个 filter() 中以多个关键字参数传入的条件默认是 AND 关系。Q() 对象用于表达 OR、NOT、动态组合条件等更复杂的查询逻辑。
from django.db.models import Q
# AND:两个写法等价
result = Book.objects.filter(sale__gt=5000, stock__lt=12000)
result = Book.objects.filter(Q(sale__gt=5000) & Q(stock__lt=12000))
# OR:满足任一条件即可
result = Book.objects.filter(Q(sale__gt=5000) | Q(stock__lt=12000))
# NOT:排除指定条件
result = Book.objects.filter(~Q(name__icontains="测试"))
代码说明:&、|、~ 分别表示 AND、OR、NOT。使用括号明确优先级,例如 Q(a=1) | (Q(b=2) & Q(c=3))。在 filter() 中混用 Q 对象和关键字参数时,Q 对象应放在关键字参数之前。
根据用户输入动态组合条件
搜索页面经常只接收部分筛选项。可以从空 Q() 开始,按实际输入逐步用 &= 或 |= 组合条件,但字段名必须来自白名单,绝不能让用户直接控制 ORM 查找表达式。
from django.db.models import Q
def search_books(keyword=None, min_price=None, max_price=None):
condition = Q()
if keyword:
condition &= Q(name__icontains=keyword)
if min_price is not None:
condition &= Q(price__gte=min_price)
if max_price is not None:
condition &= Q(price__lte=max_price)
return Book.objects.filter(condition).order_by("name")
代码说明:空 Q() 相当于不添加限制条件。用户输入要先完成类型、长度与业务范围校验;ORM 会对值进行参数化处理,但不意味着可以信任任意输入或暴露任意字段。
三、事务:保证多步写入的一致性
事务将多条数据库操作视为一个整体:全部成功时提交,发生异常时回滚。它适合转账、创建订单并扣库存、批量导入等“不能只完成一半”的操作。
使用 transaction.atomic()
局部事务最常用也最清晰的写法是上下文管理器 transaction.atomic()。块内抛出的异常会导致其中的数据库修改回滚;只有异常离开事务块并被正确处理后,才会提交。
from django.db import transaction
from django.db.models import F
from django.http import HttpResponseBadRequest
from django.shortcuts import redirect
from .models import Book, Order
def create_order(request, book_id):
try:
with transaction.atomic():
updated_count = Book.objects.filter(pk=book_id, stock__gt=0).update(
stock=F("stock") - 1,
sale=F("sale") + 1,
)
if updated_count == 0:
return HttpResponseBadRequest("库存不足或商品不存在")
Order.objects.create(book_id=book_id, user=request.user)
except Exception:
# 实际项目应记录异常日志;不要向用户返回内部堆栈。
return HttpResponseBadRequest("创建订单失败")
return redirect("order-success")
代码说明:with transaction.atomic() 不需要手动调用 commit() 或 rollback()。若块内异常没有被吞掉,Django 会自动回滚。示例中库存扣减和订单创建处于同一事务,任何一步失败都不会留下“库存已减但订单没创建”的半完成状态。
嵌套事务与保存点
嵌套的 atomic() 默认使用保存点。可以捕获内层异常并继续执行外层事务;但应在内层事务块外捕获异常,避免在同一个已标记回滚的事务块内继续查询。
from django.db import IntegrityError, transaction
with transaction.atomic():
create_order_header()
try:
with transaction.atomic():
create_optional_coupon_record()
except IntegrityError:
# 内层保存点已回滚,外层事务仍可继续。
record_coupon_error()
create_order_audit_log()
代码说明:通常无需手动调用 transaction.savepoint()。只有在非常特殊的底层控制需求下才使用保存点 API;错误地在回滚后继续提交同一保存点,反而容易造成难以理解的事务状态。
ATOMIC_REQUESTS 的取舍
在数据库配置中设置 ATOMIC_REQUESTS=True,会让每个请求默认包裹在事务中。它能减少遗漏事务的风险,但也会让长请求持有事务更久,对流式响应和高并发路径不一定合适。
# settings.py
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "shop_db",
"ATOMIC_REQUESTS": True,
}
}
代码说明:全局事务不是所有项目的默认最佳选择。对关键写入路径显式使用 atomic() 往往更容易看出事务边界。需要排除某个视图时,可使用 @transaction.non_atomic_requests,但应清楚理解其数据一致性影响。
四、主键与常用数值、文本字段
Django 会为未显式声明主键的模型添加 id。新项目的默认主键类型由 DEFAULT_AUTO_FIELD 控制,常见为 BigAutoField。业务编号若需要特定格式,应使用独立字段并加唯一约束,而不要重载主键含义。
from django.db import models
class Product(models.Model):
sku = models.CharField(max_length=32, unique=True)
name = models.CharField(max_length=255)
stock = models.PositiveIntegerField(default=0)
price = models.DecimalField(max_digits=10, decimal_places=2)
description = models.TextField(blank=True)
代码说明:
AutoField与BigAutoField是自增主键类型;后者范围更大。IntegerField、BigIntegerField、PositiveIntegerField用于不同范围的整数;手机号、身份证号等“数字字符标识”应用CharField保存,不能做数学运算。- 金额使用
DecimalField,避免二进制浮点数精度误差。max_digits是总位数,decimal_places是小数位数。 TextField适合长文本;数据库仍可能有实际容量限制,应结合业务控制输入长度。
五、日期、文件与布尔字段
from django.db import models
class Article(models.Model):
title = models.CharField(max_length=200)
published_on = models.DateField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
attachment = models.FileField(upload_to="attachments/%Y/%m/", blank=True)
is_published = models.BooleanField(default=False)
代码说明:
DateField只保存日期;DateTimeField保存日期和时间。时区策略应由USE_TZ与应用配置统一管理。auto_now_add只在创建时写入,auto_now在实例保存时更新。它们不适合需要手动控制的业务时间,例如“实际支付时间”。FileField的数据库列保存文件路径,文件本体交给配置的存储后端。upload_to应是相对媒体根目录的路径,不要写绝对路径或用户可控路径。BooleanField表示二值状态。现代 Django 中通常不再使用已弃用的NullBooleanField;需要三态时可使用BooleanField(null=True)并明确处理None。
六、关系字段与 on_delete 策略
ForeignKey 用于一对多关系,放在“多”的一方;OneToOneField 用于一对一关系,常用于将可选或敏感资料从主表拆出。外键关联的删除行为必须显式指定。
class Category(models.Model):
name = models.CharField(max_length=64)
class Item(models.Model):
category = models.ForeignKey(
Category,
on_delete=models.PROTECT,
related_name="items",
)
class UserProfile(models.Model):
user = models.OneToOneField(
"auth.User",
on_delete=models.CASCADE,
related_name="profile",
)
on_delete 策略 | 删除被关联对象时的行为 |
|---|---|
CASCADE | 删除关联记录 |
PROTECT | 阻止删除并抛出 ProtectedError |
SET_NULL | 将外键设为 NULL,字段必须 null=True |
SET_DEFAULT | 设为默认值,字段必须配置 default |
SET(value) | 设为指定值或函数返回值 |
DO_NOTHING | 不做 ORM 级处理,可能由数据库报完整性错误 |
代码说明:to_field 可让外键关联目标表的非主键唯一字段,但会增加维护成本;绝大多数场景应关联默认主键。db_index 可以为普通字段手动创建索引,但外键通常已自动建立索引,添加索引前应先观察真实查询和数据库执行计划。
七、null、blank、default 与 unique
字段参数分别影响数据库约束、Django 校验与默认数据,不能互相混淆。
class Customer(models.Model):
email = models.EmailField(unique=True)
nickname = models.CharField(max_length=32, blank=True, default="")
birthday = models.DateField(null=True, blank=True)
source = models.CharField(max_length=32, default="website")
| 参数 | 主要作用 |
|---|---|
null=True | 数据库允许保存 NULL |
blank=True | 表单与模型校验允许空值 |
default=... | 未提供值时使用默认值 |
unique=True | 数据库层要求字段值唯一 |
代码说明:字符串字段通常倾向使用空字符串而非 NULL,因此常写成 blank=True, default="";日期、外键等字段是否使用 NULL 应根据业务“未知或不存在”的语义决定。unique=True 仍可能在并发请求中触发 IntegrityError,创建记录时需要适当处理异常。
八、choices:保存稳定值,展示友好文本
choices 适合枚举值数量少、变动不频繁的字段,例如支付方式和订单状态。数据库保存稳定的内部值,页面和后台显示可读文本。
from django.db import models
class Order(models.Model):
class PaymentMethod(models.IntegerChoices):
WECHAT = 1, "微信支付"
ALIPAY = 2, "支付宝"
CARD = 3, "银行卡"
payment_method = models.PositiveSmallIntegerField(
choices=PaymentMethod.choices,
default=PaymentMethod.WECHAT,
)
order = Order.objects.get(pk=1)
print(order.payment_method) # 1
print(order.get_payment_method_display()) # 微信支付
代码说明:get_<字段名>_display() 返回当前已保存值对应的显示文本,不会返回“所有可选项”。若选项需要由管理员动态维护、需要排序或附带描述,应建立独立数据表,而不是把 choices 写死在代码中。
九、字段设计与查询优化建议
- 用
F()处理库存、计数器等字段级计算,避免并发读改写覆盖。 - 用
Q()表达 OR、NOT 与动态条件,字段名和查找方式必须由服务端控制。 - 关键多步写入使用
transaction.atomic(),事务块要短小且只包含必要数据库操作。 - 金额用
DecimalField,标识号码用CharField,业务时间不要滥用auto_now。 - 外键删除策略必须匹配业务规则;
CASCADE不等于“更安全”的默认值。 - 索引、
select_related()、prefetch_related()、only()、defer()都应基于实际查询模式和测量结果使用。
这些规则将模型字段、数据库一致性和 ORM 查询连接起来,是编写可靠 Django 业务代码的重要基础。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2501_91671229/article/details/163631661



