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

GraphQL Django:含自定义字段的Type如何在解析器中缓存

问题

使用graphene-django结合Redis缓存时遇到问题:缓存的AdDetails查询集不包含自定义的价格相关字段,因为缓存的是模型实例而非对应的AdDetailsType对象,再次查询时仍需重新计算自定义字段,耗时较长。尝试[AdDetailsType(ad) for ad in result]转换后所有字段值为None,序列化方法也未解决。

相关代码

模型定义

class AdDetails(models.Model):
    id = models.IntegerField(primary_key=True)
    dealer = models.ForeignKey('DealerLookup', models.DO_NOTHING, db_column='dealer')
    county = models.ForeignKey('CountyLookup', models.DO_NOTHING, db_column='county')
    vehicle_type = models.ForeignKey('VehicleTypeLookup', models.DO_NOTHING, db_column='vehicle_type')
    href = models.TextField()
    make = models.ForeignKey('MakeLookup', models.DO_NOTHING, db_column='make')
    model = models.ForeignKey('ModelLookup', models.DO_NOTHING, db_column='model')
    year = models.ForeignKey('YearLookup', models.DO_NOTHING, db_column='year')
    mileage_km = models.IntegerField(blank=True, null=True)
    fuel_type = models.ForeignKey('FuelTypeLookup', models.DO_NOTHING, db_column='fuel_type')
    transmission = models.ForeignKey('TransmissionLookup', models.DO_NOTHING, db_column='transmission', blank=True, null=True)
    engine_size = models.ForeignKey('EngineSizeLookup', models.DO_NOTHING, db_column='engine_size', blank=True, null=True)
    colour = models.ForeignKey('ColourLookup', models.DO_NOTHING, db_column='colour', blank=True, null=True)
    for_sale_date = models.DateField()
    sold_date = models.DateField(blank=True, null=True)

    class Meta:
        managed = False
        db_table = 'ad_details'

AdDetailsType定义

class AdDetailsType(DjangoObjectType):
    price = graphene.Float()
    previous_price = graphene.Float()
    price_movement = graphene.Float()

    def resolve_price(self, info):
        price_history = self.pricehistory_set.order_by('-date').first()
        if price_history:
            return price_history.price
        return None

    def resolve_previous_price(self, info):
        price_history = self.pricehistory_set.filter(price__gt=0).order_by('date').first()
        if price_history:
            return price_history.price
        return None

    def resolve_price_movement(self, info):
        price = self.pricehistory_set.order_by('-date').first()
        prev_price = self.pricehistory_set.filter(price__gt=0).order_by('date').first()

        if price is not None and prev_price is not None:
            return price.price - prev_price.price
        else:
            return None

    class Meta:
        model = AdDetails
        fields = "__all__"

当前解析器代码

class Query(graphene.ObjectType):
    stock_details = graphene.List(AdDetailsType, dealer_id=graphene.Int())

    def resolve_stock_details(self, info, dealer_id):
        cache_key = f"stock_details_{dealer_id}"
        cached_result = cache.get(cache_key)
        if cached_result is not None:
            return cached_result

        result = AdDetails.objects.filter(dealer=dealer_id, sold_date__isnull=True).order_by('for_sale_date')
        
        # Cache the query result with a timeout of 10 days
        cache.set(cache_key, result, timeout=864000)

        return result

解决方案

方案一:缓存包含自定义字段的序列化数据

直接计算好所有自定义字段的值,把完整数据序列化为JSON存入缓存,取出后直接构造AdDetailsType返回,无需再执行resolve方法。

修改后的解析器代码:

import json
from django.core.serializers.json import DjangoJSONEncoder

class Query(graphene.ObjectType):
    stock_details = graphene.List(AdDetailsType, dealer_id=graphene.Int())

    def resolve_stock_details(self, info, dealer_id):
        cache_key = f"stock_details_{dealer_id}"
        cached_result = cache.get(cache_key)
        if cached_result is not None:
            # 反序列化缓存数据并构造AdDetailsType实例
            return [AdDetailsType(**item) for item in json.loads(cached_result)]

        # 预取pricehistory集合,避免N+1查询
        result = AdDetails.objects.filter(dealer=dealer_id, sold_date__isnull=True)\
                                  .order_by('for_sale_date')\
                                  .prefetch_related('pricehistory_set')

        # 构造包含模型字段和自定义字段的字典列表
        serialized_data = []
        for ad in result:
            # 提取模型字段数据
            ad_data = {field.name: getattr(ad, field.name) for field in AdDetails._meta.fields}
            # 计算自定义字段值
            latest_price = ad.pricehistory_set.order_by('-date').first()
            first_valid_price = ad.pricehistory_set.filter(price__gt=0).order_by('date').first()
            
            ad_data['price'] = latest_price.price if latest_price else None
            ad_data['previous_price'] = first_valid_price.price if first_valid_price else None
            ad_data['price_movement'] = (latest_price.price - first_valid_price.price) if (latest_price and first_valid_price) else None
            
            serialized_data.append(ad_data)
        
        # 序列化并存入Redis缓存
        cache.set(cache_key, json.dumps(serialized_data, cls=DjangoJSONEncoder), timeout=864000)

        return [AdDetailsType(**item) for item in serialized_data]

方案二:预取关联数据,缓存模型实例(优化DB查询)

如果不想直接缓存序列化后的Type对象,可以预取pricehistory_set关联数据,减少resolve阶段的数据库查询次数。即使缓存的是模型实例,后续resolve时也无需再次查询数据库,大幅提升性能。

修改后的解析器代码:

class Query(graphene.ObjectType):
    stock_details = graphene.List(AdDetailsType, dealer_id=graphene.Int())

    def resolve_stock_details(self, info, dealer_id):
        cache_key = f"stock_details_{dealer_id}"
        cached_result = cache.get(cache_key)
        if cached_result is not None:
            return cached_result

        # 预取pricehistory集合,避免resolve时的N+1查询
        result = AdDetails.objects.filter(dealer=dealer_id, sold_date__isnull=True)\
                                  .order_by('for_sale_date')\
                                  .prefetch_related('pricehistory_set')
        
        # 缓存模型实例查询集
        cache.set(cache_key, result, timeout=864000)

        return result

关于直接转换AdDetailsType失败的原因

AdDetailsType(ad)无法正确赋值是因为DjangoObjectType的初始化需要遵循graphene内部的字段绑定机制,直接传入模型实例不会自动映射所有字段。正确的做法要么是通过序列化后的字典数据构造,要么让graphene自动处理预取了关联数据的模型实例。

内容的提问来源于stack exchange,提问作者Robben

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 15:06:27