
零基础入门python33Django QuerySet——从商品查询到数据库 SQL一、上一篇课后练习讲解删除被商品使用的分类会触发ProtectedError。这不是异常写错了而是on_deletemodels.PROTECT主动阻止破坏数据关系。上一篇课后练习完整答案上一篇练习的要求已落实到下面完整文件先运行项目测试再用 curl 对照状态码和数据库持久化结果答案要点Category/Product 模型使用 PROTECT、DecimalField、PositiveIntegerField 和唯一 slug迁移前检查 sqlmigrate。文件shop/models.py完整参考答案文件完整文件shop/models.pyfromdjango.dbimportmodelsclassCategory(models.Model):namemodels.CharField(max_length80,uniqueTrue)slugmodels.SlugField(uniqueTrue)classProduct(models.Model):categorymodels.ForeignKey(Category,on_deletemodels.PROTECT,related_nameproducts)namemodels.CharField(max_length120)slugmodels.SlugField(uniqueTrue)pricemodels.DecimalField(max_digits10,decimal_places2)stockmodels.PositiveIntegerField(default0)is_activemodels.BooleanField(defaultTrue)完整参考答案文件本篇对应的交付源码完整文件django-shop/shop/models.pyfromdjango.confimportsettingsfromdjango.dbimportmodelsclassCategory(models.Model):namemodels.CharField(max_length60,uniqueTrue)slugmodels.SlugField(max_length60,uniqueTrue)def__str__(self):returnself.nameclassProduct(models.Model):categorymodels.ForeignKey(Category,on_deletemodels.PROTECT,related_nameproducts)namemodels.CharField(max_length120)slugmodels.SlugField(max_length120,uniqueTrue)pricemodels.DecimalField(max_digits10,decimal_places2)stockmodels.PositiveIntegerField(default0)is_activemodels.BooleanField(defaultTrue)created_atmodels.DateTimeField(auto_now_addTrue)def__str__(self):returnself.nameclassOrder(models.Model):classStatus(models.TextChoices):PENDINGpending,待处理PAIDpaid,已支付CANCELLEDcancelled,已取消usermodels.ForeignKey(settings.AUTH_USER_MODEL,on_deletemodels.PROTECT,related_nameorders)statusmodels.CharField(max_length20,choicesStatus.choices,defaultStatus.PENDING)total_amountmodels.DecimalField(max_digits12,decimal_places2,default0)created_atmodels.DateTimeField(auto_now_addTrue)classOrderItem(models.Model):ordermodels.ForeignKey(Order,on_deletemodels.CASCADE,related_nameitems)productmodels.ForeignKey(Product,on_deletemodels.PROTECT)product_namemodels.CharField(max_length120)pricemodels.DecimalField(max_digits10,decimal_places2)quantitymodels.PositiveIntegerField()验收命令python -m pytest -qDjango 项目使用 python manage.py test。预期测试通过若失败先检查迁移、配置和事务回滚。二、本篇完成什么用 QuerySet 实现商品列表、分类筛选、关键词搜索和库存排序并理解 QuerySet 为什么是延迟执行的。三、常用查询products(Product.objects.filter(is_activeTrue).select_related(category).order_by(-created_at))filter只是构造查询真正访问数据库通常发生在迭代、list()或序列化时。select_related用 JOIN 一次加载外键分类避免列表中每个商品再发一次查询。搜索示例queryProduct.objects.filter(is_activeTrue)ifkeyword:queryquery.filter(name__icontainskeyword)ifcategory_id:queryquery.filter(category_idcategory_id)不要把用户输入拼进原生 SQLORM 参数化会正确处理特殊字符。分页前必须稳定排序否则商品新增时页码内容会跳动。四、验收在 shell 中分别执行全部商品、分类筛选、关键词搜索和库存降序查询并打开 Django SQL 日志观察查询次数。课后练习实现“库存小于 5 的商品”查询并写测试。项目增量QuerySet、JOIN 和 N1列表页显示分类名时循环访问 product.category 会产生 N1。使用 select_related 把外键 JOIN 到一次查询多对多关系使用 prefetch_related。productsProduct.objects.select_related(category).filter(is_activeTrue).order_by(-created_at)打开 Django connection.queries 或使用 assertNumQueries 记录查询次数不要为了“看起来 ORM”把所有商品先转成 list 再 Python 过滤。课后练习写一个详情页测试证明列表查询不会随商品数量增长。五、把 QuerySet 当作“延迟执行的查询计划”Product.objects.filter(...)不会马上访问数据库只有迭代、list()、count()或序列化时才执行。这个特性允许我们在不同条件下逐步构造查询但也意味着在循环里调用count()会产生很多 SQL。示例fromdjango.db.modelsimportQdefvisible_products(params):qsProduct.objects.filter(is_activeTrue)ifkeyword:params.get(q):qsqs.filter(Q(name__icontainskeyword)|Q(category__name__icontainskeyword))ifparams.get(low_stock):qsqs.filter(stock__lt5)returnqs.select_related(category).order_by(id)Q让名称和分类名使用 OR最终查询仍由数据库参数化特殊字符不会改变 SQL 结构。order_by(id)为分页提供稳定顺序。六、N1 实验用 assertNumQueries 说服自己fromdjango.testimportTestCaseclassProductQueryTests(TestCase):deftest_list_does_not_grow_queries(self):ProductFactory.create_batch(10)withself.assertNumQueries(1):list(Product.objects.select_related(category).values(name,category__name))去掉select_related后模板中逐个访问product.category.name会额外执行 10 次查询。多对多标签应使用prefetch_related它通常是两条 SQL而不是一条 JOIN选择哪种优化要看关系基数和返回字段。七、上一篇练习讲解、输出与故障排查上一篇的stock 0约束必须同时在模型验证和数据库中存在full_clean()不会自动在save()时执行所以服务层仍应主动调用。运行库存筛选python manage.py shell-cfrom shop.models import Product; print(list(Product.objects.filter(stock__lt5).values_list(name,stock)))若查询结果包含下架商品检查是否漏了is_activeTrue若 SQL 数量超标用 Django Debug Toolbar 或connection.queries定位循环里的隐式查询。八、本篇练习实现商品详情 API返回分类名和库存并写测试证明查询次数固定为 1再增加按价格区间筛选。下一篇将把这些字段配置到 Admin形成运营可用的后台。九、查看 ORM 生成的 SQL开发环境可以临时打开fromdjango.dbimportconnection qsProduct.objects.select_related(category).filter(stock__lt5)list(qs)print(connection.queries[-1][sql])看到的 SQL 应包含 JOIN 和参数占位符而不是把用户关键词直接拼进字符串。生产环境不要长期开启connection.queries它会保存每条 SQL 并增加内存开销改用数据库慢查询日志和采样 APM。把 QuerySet 写成函数后视图、导出任务和测试都能复用同一过滤逻辑。十、先写查询契约再优化给visible_products写契约测试下架商品永远不出现关键词同时匹配商品名和分类库存筛选与关键词可以组合结果按 id 稳定排序。优化select_related后重新运行assertNumQueries如果字段需求改变导致 JOIN 不再必要再删掉优化。性能优化应由测试和 SQL 证据驱动而不是凭“ORM 很慢”的印象。本篇结束完整模块文件下面是交付项目中真实存在的完整文件 django-shop/shop/models.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码复制单个函数会丢失上下文因此这里提供整份文件。fromdjango.confimportsettingsfromdjango.dbimportmodelsclassCategory(models.Model):namemodels.CharField(max_length60,uniqueTrue)slugmodels.SlugField(max_length60,uniqueTrue)def__str__(self):returnself.nameclassProduct(models.Model):categorymodels.ForeignKey(Category,on_deletemodels.PROTECT,related_nameproducts)namemodels.CharField(max_length120)slugmodels.SlugField(max_length120,uniqueTrue)pricemodels.DecimalField(max_digits10,decimal_places2)stockmodels.PositiveIntegerField(default0)is_activemodels.BooleanField(defaultTrue)created_atmodels.DateTimeField(auto_now_addTrue)def__str__(self):returnself.nameclassOrder(models.Model):classStatus(models.TextChoices):PENDINGpending,待处理PAIDpaid,已支付CANCELLEDcancelled,已取消usermodels.ForeignKey(settings.AUTH_USER_MODEL,on_deletemodels.PROTECT,related_nameorders)statusmodels.CharField(max_length20,choicesStatus.choices,defaultStatus.PENDING)total_amountmodels.DecimalField(max_digits12,decimal_places2,default0)created_atmodels.DateTimeField(auto_now_addTrue)classOrderItem(models.Model):ordermodels.ForeignKey(Order,on_deletemodels.CASCADE,related_nameitems)productmodels.ForeignKey(Product,on_deletemodels.PROTECT)product_namemodels.CharField(max_length120)pricemodels.DecimalField(max_digits10,decimal_places2)quantitymodels.PositiveIntegerField()