顾风jy头像
关注

Django ORM 表达式、事务与模型字段设计

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)

代码说明:

  • AutoFieldBigAutoField 是自增主键类型;后者范围更大。
  • IntegerFieldBigIntegerFieldPositiveIntegerField 用于不同范围的整数;手机号、身份证号等“数字字符标识”应用 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 写死在代码中。

九、字段设计与查询优化建议

  1. F() 处理库存、计数器等字段级计算,避免并发读改写覆盖。
  2. Q() 表达 OR、NOT 与动态条件,字段名和查找方式必须由服务端控制。
  3. 关键多步写入使用 transaction.atomic(),事务块要短小且只包含必要数据库操作。
  4. 金额用 DecimalField,标识号码用 CharField,业务时间不要滥用 auto_now
  5. 外键删除策略必须匹配业务规则;CASCADE 不等于“更安全”的默认值。
  6. 索引、select_related()prefetch_related()only()defer() 都应基于实际查询模式和测量结果使用。

这些规则将模型字段、数据库一致性和 ORM 查询连接起来,是编写可靠 Django 业务代码的重要基础。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2501_91671229/article/details/163631661

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--