You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Django如何查询所有商品及其去重的尺寸颜色变体属性

问题背景

我有如下数据库schema:
database 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 08:51:34