本章关注 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 业务代码的重要基础。
到此这篇关于django orm 表达式、事务与模型字段设计方法的文章就介绍到这了,更多相关django orm 表达式内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论