
Django ORM 表达式、事务与模型字段设计本章关注 ORM 中三个容易影响正确性的问题如何在数据库中直接比较或计算字段值如何保证多步写入要么全部成功要么全部撤销以及如何选择合适的模型字段与约束。掌握这些内容后可以减少并发更新丢失、数据半写入和字段设计不当等问题。一、F 表达式让数据库完成字段比较和计算F()表达式表示“数据库中某个字段当前的值”。它不是 Python 中已经取出的具体数值因此可在一次 SQL 语句内完成字段比较、加减运算与字符串拼接避免“先读、后改、再存”带来的额外查询和并发覆盖风险。以下模型用库存、销量与价格演示 F 表达式# shop/models.pyfromdjango.dbimportmodelsclassBook(models.Model):namemodels.CharField(max_length255)pricemodels.DecimalField(max_digits10,decimal_places2)salemodels.PositiveIntegerField(default0)stockmodels.PositiveIntegerField(default0)比较两个字段例如查询销量大于库存的图书不应把所有记录读取到 Python 后再用循环比较而应让数据库在WHERE条件中完成比较。fromdjango.db.modelsimportF overstocked_salesBook.objects.filter(sale__gtF(stock))代码说明F(stock)表示stock字段本身。ORM 会生成类似WHERE sale stock的 SQL。与先Book.objects.all()再在 Python 中筛选相比这种写法传输的数据更少也能让数据库利用查询优化能力。原子递增或批量更新库存和计数器是 F 表达式的典型场景。若两个请求同时读取库存10各自再保存9其中一次更新会丢失在数据库内执行stock stock - 1则能避免这种“读-改-写”竞争。fromdjango.db.modelsimportF# 全部图书涨价 500Book.objects.update(priceF(price)500)# 仅减少库存大于 0 的图书返回受影响行数updated_countBook.objects.filter(pk1,stock__gt0).update(stockF(stock)-1,saleF(sale)1,)ifupdated_count0:print(库存不足或图书不存在)代码说明QuerySet.update()直接执行 SQL不会调用模型实例的save()也不会触发基于save()的自定义逻辑。库存扣减需要把stock__gt0写在同一条更新条件中才能在并发下保持正确。若将 F 表达式赋给单个实例保存后该实例属性仍可能保留 F 对象。需要继续使用新值时应刷新对象。bookBook.objects.get(pk1)book.saleF(sale)1book.save(update_fields[sale])book.refresh_from_db(fields[sale])print(book.sale)字符串拼接与数据库函数不同数据库对字符串的语义不同。拼接文本时应使用 Django 提供的数据库函数Concat()与Value()而不是依赖特定数据库行为。fromdjango.db.modelsimportF,Valuefromdjango.db.models.functionsimportConcat Book.objects.filter(sale__gte5000).update(nameConcat(Value(爆款-),F(name)),)代码说明Value(爆款-)将普通 Python 字符串包装成 SQL 常量F(name)引用原字段值。重复执行这类语句会重复添加前缀生产代码应增加明确条件或使用独立状态字段记录是否已标记。二、Q 对象构造复杂查询条件同一个filter()中以多个关键字参数传入的条件默认是 AND 关系。Q()对象用于表达 OR、NOT、动态组合条件等更复杂的查询逻辑。fromdjango.db.modelsimportQ# AND两个写法等价resultBook.objects.filter(sale__gt5000,stock__lt12000)resultBook.objects.filter(Q(sale__gt5000)Q(stock__lt12000))# OR满足任一条件即可resultBook.objects.filter(Q(sale__gt5000)|Q(stock__lt12000))# NOT排除指定条件resultBook.objects.filter(~Q(name__icontains测试))代码说明、|、~分别表示 AND、OR、NOT。使用括号明确优先级例如Q(a1) | (Q(b2) Q(c3))。在filter()中混用 Q 对象和关键字参数时Q 对象应放在关键字参数之前。根据用户输入动态组合条件搜索页面经常只接收部分筛选项。可以从空Q()开始按实际输入逐步用或|组合条件但字段名必须来自白名单绝不能让用户直接控制 ORM 查找表达式。fromdjango.db.modelsimportQdefsearch_books(keywordNone,min_priceNone,max_priceNone):conditionQ()ifkeyword:conditionQ(name__icontainskeyword)ifmin_priceisnotNone:conditionQ(price__gtemin_price)ifmax_priceisnotNone:conditionQ(price__ltemax_price)returnBook.objects.filter(condition).order_by(name)代码说明空Q()相当于不添加限制条件。用户输入要先完成类型、长度与业务范围校验ORM 会对值进行参数化处理但不意味着可以信任任意输入或暴露任意字段。三、事务保证多步写入的一致性事务将多条数据库操作视为一个整体全部成功时提交发生异常时回滚。它适合转账、创建订单并扣库存、批量导入等“不能只完成一半”的操作。使用 transaction.atomic()局部事务最常用也最清晰的写法是上下文管理器transaction.atomic()。块内抛出的异常会导致其中的数据库修改回滚只有异常离开事务块并被正确处理后才会提交。fromdjango.dbimporttransactionfromdjango.db.modelsimportFfromdjango.httpimportHttpResponseBadRequestfromdjango.shortcutsimportredirectfrom.modelsimportBook,Orderdefcreate_order(request,book_id):try:withtransaction.atomic():updated_countBook.objects.filter(pkbook_id,stock__gt0).update(stockF(stock)-1,saleF(sale)1,)ifupdated_count0:returnHttpResponseBadRequest(库存不足或商品不存在)Order.objects.create(book_idbook_id,userrequest.user)exceptException:# 实际项目应记录异常日志不要向用户返回内部堆栈。returnHttpResponseBadRequest(创建订单失败)returnredirect(order-success)代码说明with transaction.atomic()不需要手动调用commit()或rollback()。若块内异常没有被吞掉Django 会自动回滚。示例中库存扣减和订单创建处于同一事务任何一步失败都不会留下“库存已减但订单没创建”的半完成状态。嵌套事务与保存点嵌套的atomic()默认使用保存点。可以捕获内层异常并继续执行外层事务但应在内层事务块外捕获异常避免在同一个已标记回滚的事务块内继续查询。fromdjango.dbimportIntegrityError,transactionwithtransaction.atomic():create_order_header()try:withtransaction.atomic():create_optional_coupon_record()exceptIntegrityError:# 内层保存点已回滚外层事务仍可继续。record_coupon_error()create_order_audit_log()代码说明通常无需手动调用transaction.savepoint()。只有在非常特殊的底层控制需求下才使用保存点 API错误地在回滚后继续提交同一保存点反而容易造成难以理解的事务状态。ATOMIC_REQUESTS 的取舍在数据库配置中设置ATOMIC_REQUESTSTrue会让每个请求默认包裹在事务中。它能减少遗漏事务的风险但也会让长请求持有事务更久对流式响应和高并发路径不一定合适。# settings.pyDATABASES{default:{ENGINE:django.db.backends.mysql,NAME:shop_db,ATOMIC_REQUESTS:True,}}代码说明全局事务不是所有项目的默认最佳选择。对关键写入路径显式使用atomic()往往更容易看出事务边界。需要排除某个视图时可使用transaction.non_atomic_requests但应清楚理解其数据一致性影响。四、主键与常用数值、文本字段Django 会为未显式声明主键的模型添加id。新项目的默认主键类型由DEFAULT_AUTO_FIELD控制常见为BigAutoField。业务编号若需要特定格式应使用独立字段并加唯一约束而不要重载主键含义。fromdjango.dbimportmodelsclassProduct(models.Model):skumodels.CharField(max_length32,uniqueTrue)namemodels.CharField(max_length255)stockmodels.PositiveIntegerField(default0)pricemodels.DecimalField(max_digits10,decimal_places2)descriptionmodels.TextField(blankTrue)代码说明AutoField与BigAutoField是自增主键类型后者范围更大。IntegerField、BigIntegerField、PositiveIntegerField用于不同范围的整数手机号、身份证号等“数字字符标识”应用CharField保存不能做数学运算。金额使用DecimalField避免二进制浮点数精度误差。max_digits是总位数decimal_places是小数位数。TextField适合长文本数据库仍可能有实际容量限制应结合业务控制输入长度。五、日期、文件与布尔字段fromdjango.dbimportmodelsclassArticle(models.Model):titlemodels.CharField(max_length200)published_onmodels.DateField(nullTrue,blankTrue)created_atmodels.DateTimeField(auto_now_addTrue)updated_atmodels.DateTimeField(auto_nowTrue)attachmentmodels.FileField(upload_toattachments/%Y/%m/,blankTrue)is_publishedmodels.BooleanField(defaultFalse)代码说明DateField只保存日期DateTimeField保存日期和时间。时区策略应由USE_TZ与应用配置统一管理。auto_now_add只在创建时写入auto_now在实例保存时更新。它们不适合需要手动控制的业务时间例如“实际支付时间”。FileField的数据库列保存文件路径文件本体交给配置的存储后端。upload_to应是相对媒体根目录的路径不要写绝对路径或用户可控路径。BooleanField表示二值状态。现代 Django 中通常不再使用已弃用的NullBooleanField需要三态时可使用BooleanField(nullTrue)并明确处理None。六、关系字段与 on_delete 策略ForeignKey用于一对多关系放在“多”的一方OneToOneField用于一对一关系常用于将可选或敏感资料从主表拆出。外键关联的删除行为必须显式指定。classCategory(models.Model):namemodels.CharField(max_length64)classItem(models.Model):categorymodels.ForeignKey(Category,on_deletemodels.PROTECT,related_nameitems,)classUserProfile(models.Model):usermodels.OneToOneField(auth.User,on_deletemodels.CASCADE,related_nameprofile,)on_delete策略删除被关联对象时的行为CASCADE删除关联记录PROTECT阻止删除并抛出ProtectedErrorSET_NULL将外键设为NULL字段必须nullTrueSET_DEFAULT设为默认值字段必须配置defaultSET(value)设为指定值或函数返回值DO_NOTHING不做 ORM 级处理可能由数据库报完整性错误代码说明to_field可让外键关联目标表的非主键唯一字段但会增加维护成本绝大多数场景应关联默认主键。db_index可以为普通字段手动创建索引但外键通常已自动建立索引添加索引前应先观察真实查询和数据库执行计划。七、null、blank、default 与 unique字段参数分别影响数据库约束、Django 校验与默认数据不能互相混淆。classCustomer(models.Model):emailmodels.EmailField(uniqueTrue)nicknamemodels.CharField(max_length32,blankTrue,default)birthdaymodels.DateField(nullTrue,blankTrue)sourcemodels.CharField(max_length32,defaultwebsite)参数主要作用nullTrue数据库允许保存NULLblankTrue表单与模型校验允许空值default...未提供值时使用默认值uniqueTrue数据库层要求字段值唯一代码说明字符串字段通常倾向使用空字符串而非NULL因此常写成blankTrue, default日期、外键等字段是否使用NULL应根据业务“未知或不存在”的语义决定。uniqueTrue仍可能在并发请求中触发IntegrityError创建记录时需要适当处理异常。八、choices保存稳定值展示友好文本choices适合枚举值数量少、变动不频繁的字段例如支付方式和订单状态。数据库保存稳定的内部值页面和后台显示可读文本。fromdjango.dbimportmodelsclassOrder(models.Model):classPaymentMethod(models.IntegerChoices):WECHAT1,微信支付ALIPAY2,支付宝CARD3,银行卡payment_methodmodels.PositiveSmallIntegerField(choicesPaymentMethod.choices,defaultPaymentMethod.WECHAT,)orderOrder.objects.get(pk1)print(order.payment_method)# 1print(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 业务代码的重要基础。