零基础入门python39:DRF序列化器——输入校验和稳定输出
零基础入门python39DRF序列化器——输入校验和稳定输出一、上一篇课后练习讲解并发库存测试需要两个独立数据库连接SQLite 对行锁支持有限应在 PostgreSQL/MySQL 上验证select_for_update。无论测试数据库如何库存不足回滚测试必须保留。上一篇课后练习完整答案上一篇练习的要求已落实到下面完整文件先运行项目测试再用 curl 对照状态码和数据库持久化结果答案要点place_order 用 transaction.atomic 和 select_for_update 锁库存按商品 id 排序降低死锁任一失败全部回滚。文件shop/services.py完整参考答案文件完整文件shop/services.pyfromdecimalimportDecimalfromdjango.dbimporttransactionfromdjango.db.modelsimportFfrom.modelsimportOrder,OrderItem,Producttransaction.atomicdefplace_order(user,items):orderOrder.objects.create(useruser,statuspending,total0)totalDecimal(0)foriteminsorted(items,keylambdax:x[product_id]):productProduct.objects.select_for_update().get(pkitem[product_id],is_activeTrue)quantityint(item[quantity])ifquantity0orproduct.stockquantity:raiseValueError(out_of_stock)product.stockF(stock)-quantity;product.save(update_fields[stock])OrderItem.objects.create(orderorder,productproduct,product_nameproduct.name,unit_priceproduct.price,quantityquantity)totalproduct.price*quantity order.totaltotal;order.save(update_fields[total]);returnorder完整参考答案文件本篇对应的交付源码完整文件django-shop/shop/services.pyfromdecimalimportDecimalfromdjango.dbimporttransactionfromrest_framework.exceptionsimportValidationErrorfrom.modelsimportOrder,OrderItem,Producttransaction.atomicdefcreate_order(user,items):product_ids[item[product_id]foriteminitems]products{p.id:pforpinProduct.objects.select_for_update().filter(id__inproduct_ids,is_activeTrue)}orderOrder.objects.create(useruser)totalDecimal(0)foriteminitems:productproducts.get(item[product_id])quantityitem[quantity]ifnotproduct:raiseValidationError(f商品{item[product_id]}不存在)ifproduct.stockquantity:raiseValidationError(f{product.name}库存不足)OrderItem.objects.create(orderorder,productproduct,product_nameproduct.name,priceproduct.price,quantityquantity)product.stock-quantity product.save(update_fields(stock,))totalproduct.price*quantity order.total_amounttotal order.save(update_fields(total_amount,))returnorder验收命令python -m pytest -qDjango 项目使用 python manage.py test。预期测试通过若失败先检查迁移、配置和事务回滚。二、本篇完成什么使用 ModelSerializer 输出商品和订单使用普通 Serializer 校验结算输入。输入模型和输出模型职责不同不应为了省代码混成一个。三、序列化器代码classCheckoutItemSerializer(serializers.Serializer):product_idserializers.IntegerField(min_value1)quantityserializers.IntegerField(min_value1,max_value99)classCheckoutSerializer(serializers.Serializer):itemsCheckoutItemSerializer(manyTrue,allow_emptyFalse)allow_emptyFalse阻止空订单max_value阻止明显异常数量真实库存仍由 Service 在事务内验证。序列化器验证格式数据库验证并发事实。四、验收空 items、负 product_id、数量 0 和超过 99 都返回 400合法数据进入 Service。课后练习为订单项输出增加subtotal只读字段。项目增量DRF Serializer 负责接口边界Serializer 同时负责输入校验和输出字段不要直接返回 Model.dict否则内部字段会泄露。classProductSerializer(serializers.ModelSerializer):classMeta:modelProduct fields(id,name,price,stock,category)read_only_fields(id,)验收非法价格、未知分类和只读 id课后练习增加库存范围校验下一篇实现商品筛选 API。五、上一篇练习讲解快照字段应该只读订单项输出的product_name、unit_price是下单时快照客户端不能通过 PATCH 修改。subtotal可以由序列化器计算但不要从客户端接收classOrderItemSerializer(serializers.ModelSerializer):subtotalserializers.SerializerMethodField()classMeta:modelOrderItem fields(product_name,unit_price,quantity,subtotal)read_only_fieldsfieldsdefget_subtotal(self,obj):returnstr(obj.unit_price*obj.quantity)六、Serializer 是输入防火墙classProductSerializer(serializers.ModelSerializer):classMeta:modelProduct fields(id,name,slug,price,stock,category)read_only_fields(id,)defvalidate_price(self,value):ifvalue0:raiseserializers.ValidationError(价格必须大于 0)returnvalue.quantize(Decimal(0.01))defvalidate_stock(self,value):ifvalue0orvalue10_000:raiseserializers.ValidationError(库存超出范围)returnvalue字段校验在进入 service 前完成模型约束仍然保留因为脚本和 Admin 可能绕过 Serializer。未知字段默认返回 400不能静默丢弃否则客户端拼错prcie时会误以为价格已经更新。七、稳定输出和错误格式DRF 会把ValidationError转成{price: [价格必须大于 0]}。项目统一使用error_code和detail前端只依赖error_code中文提示可以变化。不要把 traceback 返回给客户端生产环境关闭DEBUG。八、验收和排错curl-Method Post http://127.0.0.1:8000/api/products/-ContentType application/json-Body{name:键盘,price:0,stock:2,category:1}预期 400响应指出price把id放进请求体应仍由服务端忽略或拒绝。TypeError说明 Decimal 与 float 混用DoesNotExist说明外键字段未通过PrimaryKeyRelatedField校验。下一篇会复用同一 Serializer 做商品列表筛选。九、本篇练习增加slug的格式校验和库存上限测试为订单项 Serializer 增加只读subtotal并证明客户端传入另一个 subtotal 不会改变响应。下一篇实现商品、分类 API 与筛选。十、Serializer 与 Model 的职责边界Serializer 负责 HTTP 输入输出Model 负责跨入口都必须成立的约束Service 负责多个模型之间的事务。例如价格大于 0 可以在 Serializer 快速提示也应在数据库 CheckConstraint 兜底扣库存不能放 Serializer 的validate()因为那一步没有行锁。把职责写清后Admin、命令行导入和 API 不会出现不同规则。序列化输出还要考虑关联查询订单列表使用select_related(user)和prefetch_related(items)Serializer 不应偷偷触发查询。可以在测试中包住self.assertNumQueries把序列化性能纳入接口契约数据量增加时先检查查询计划再决定是否加索引。本篇结束完整模块文件下面是交付项目中真实存在的完整文件 django-shop/shop/serializers.py。它覆盖本篇新增逻辑以及前文已经完成的依赖代码复制单个函数会丢失上下文因此这里提供整份文件。fromrest_frameworkimportserializersfrom.modelsimportCategory,Order,OrderItem,ProductclassCategorySerializer(serializers.ModelSerializer):classMeta:modelCategory;fields(id,name,slug)classProductSerializer(serializers.ModelSerializer):category_nameserializers.CharField(sourcecategory.name,read_onlyTrue)classMeta:modelProduct;fields(id,category,category_name,name,slug,price,stock)classOrderItemSerializer(serializers.ModelSerializer):classMeta:modelOrderItem;fields(product,product_name,price,quantity)classOrderSerializer(serializers.ModelSerializer):itemsOrderItemSerializer(manyTrue,read_onlyTrue)classMeta:modelOrder;fields(id,status,total_amount,created_at,items)classCheckoutItemSerializer(serializers.Serializer):product_idserializers.IntegerField(min_value1)quantityserializers.IntegerField(min_value1,max_value99)classCheckoutSerializer(serializers.Serializer):itemsCheckoutItemSerializer(manyTrue,allow_emptyFalse)