Django如何查询所有商品及其去重的尺寸颜色变体属性
我有如下数据库schema:
Django模型定义如下:
class Product(models.Model): name = models.CharField(max_length=150) # price and so on class Size(models.Model): value = models.CharField(max_length=20) class Color(models.Model): value = models.CharField(max_length=20) class Variation(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="variations") size = models.ForeignKey(Size, on_delete=models.CASCADE) color = models.ForeignKey(Color, on_delete=models.CASCADE)
目前可以通过product.variations获取商品的所有变体,还希望支持直接通过product.sizes、product.colors调用,获取该商品在变体表中关联的所有尺寸、颜色属性。
业务场景
需要实现商品卡片列表,每个卡片提供尺寸、颜色选项供用户选择后加入购物车,仅向用户展示当前商品实际关联的尺寸、颜色,而非全库所有的尺寸、颜色选项。
现存问题
变体记录中会存在属性重复的情况,例如某商品存在如下变体组合:
- 尺寸 - 40,颜色 - 红色
- 尺寸 - 42,颜色 - 绿色
- 尺寸 - 44,颜色 - 红色(重复出现红色)
- 尺寸 - 42(重复出现42),颜色 - 灰色
期望去重后展示结果:
尺寸:40、42、44
颜色:红色、绿色、灰色
当前实现返回带重复值的结果:
尺寸:40、42、44、42
颜色:红色、绿色、红色、灰色
当前查询代码如下,不知道如何修改实现去重:
products: QuerySet[Product] = ( Product.objects .prefetch_related("variations__size") .prefetch_related("variations__color") .all()[:15] )
目前在模板中遍历商品不会产生额外查询,模板代码如下:
{% for variation in product.variations.all %} {{ variation.size.value }} {% endfor %}
需求:在不产生额外N+1查询的前提下,实现查询所有商品及其去重后的尺寸、颜色变体属性,支持直接通过product.sizes、product.colors访问对应去重属性。
分两步实现,全程不会产生N+1查询:
第一步:补充模型方法实现内存去重
预加载完成后所有关联数据已经存在内存中,直接在Python层面去重即可,不会触发额外数据库查询。首先给Size、Color模型添加哈希和相等判断方法,保证按主键去重逻辑准确:
class Size(models.Model): value = models.CharField(max_length=20) def __hash__(self): return hash(self.pk) def __eq__(self, other): return isinstance(other, Size) and self.pk == other.pk class Color(models.Model): value = models.CharField(max_length=20) def __hash__(self): return hash(self.pk) def __eq__(self, other): return isinstance(other, Color) and self.pk == other.pk
然后给Product模型添加两个属性,直接从预加载的变体集合中提取去重后的属性:
class Product(models.Model): name = models.CharField(max_length=150) # price and so on @property def sizes(self): # 数据来自预加载缓存,不触发新的数据库查询 return list({v.size for v in self.variations.all()}) @property def colors(self): return list({v.color for v in self.variations.all()})
第二步:优化预加载逻辑
原有预加载写法已经可以正常工作,精简写法如下,执行时只会触发4次固定数量的SQL查询,和商品数量无关,完全规避N+1问题:
products: QuerySet[Product] = ( Product.objects .prefetch_related("variations", "variations__size", "variations__color") .all()[:15] )
4次查询分别为:
- 查询前15条商品记录
- 查询这15个商品关联的所有变体记录
- 查询这些变体关联的所有尺寸记录
- 查询这些变体关联的所有颜色记录
模板使用
直接调用新增的属性即可:
<!-- 渲染去重后的尺寸选项 --> {% for size in product.sizes %} <button class="size-option">{{ size.value }}</button> {% endfor %} <!-- 渲染去重后的颜色选项 --> {% for color in product.colors %} <button class="color-option">{{ color.value }}</button> {% endfor %}
如果需要按指定规则排序尺寸、颜色,直接在返回列表时加排序逻辑即可,同样不会触发额外查询,比如按属性值排序:
@property def sizes(self): return sorted( {v.size for v in self.variations.all()}, key=lambda x: x.value )
内容的提问来源于stack exchange,提问作者h3ckphy

