drf查询优化小技巧

6 阅读3分钟

通过数据库别名来优化查询.

需求场景: 结合前端做统一的下拉,比如要单位数据(id, title)下拉表单.部门下拉数据(id, name)等等,这类都是从一个表中提取一两个字段给前端.那么前端为了使用方便,可能会要求说,后端给我统一返回(id, select_show),前端就可以使用同一个组件进行展示了.

我们通常的情况就是先直接从数据库查询出来前端需要的数据,然后再for循环统一处理:

from django.db import models
from django.db.models import F

# 公司/单位管理表
class Company(models.Model):
    name = models.CharField(max_length=50, unique=True)
    description = models.TextField(blank=True, null=True)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = '公司单位管理表'
        verbose_name_plural = '公司单位管理'
        indexes = [
            # 联合索引,提高select_show的效率
            models.Index(fields=['id', 'name'], name='idx_company_id_name'),
        ]
        
# 返回执行单位的下拉菜单数据.
class CompanySelectShowViewSet(GenericReadOnlyListModelViewSet):
    queryset = Company.objects.all()
    # serializer_class = CompanySerializer
    pagination_class = None
    authentication_classes = [JwtAuthentication, JwtParamAuthentication, DenyAuthentication]
    def list(self, request, *args, **kwargs):
        # --------- 方案1 ---------
        # 需要先查出来,再进行循环处理为前端需要的数据格式,这里需要两部步处理
        # queryset = Company.objects.values('id', 'name')
        # # SELECT `rbac_company`.`id`, `rbac_company`.`name` FROM `rbac_company`
        # data = [{"id": i['id'], "select_show": i['name']} for i in queryset]
        # return Response({"code": 0, "detail": "success","data": data})

        # --------- 方案2 ---------
        # 直接从数据库层面进行别名操作,性能更佳,不需要再进行循环处理
        data = Company.objects.annotate(select_show=F('name')).values('id', 'select_show')
        # SELECT `rbac_company`.`id`, `rbac_company`.`name` AS `select_show` FROM `rbac_company`
        # QuerySet 转 list 即可
        return Response({"code": 0, "detail": "success","data": list(data)})
        
        
# 上面的方案2还可以进行优化,下面的示例来自于另一个接口,基本原理是一样的.
# 实现思路就是在返回基本的id和select_show这两个字段的基础上
# 也可以通过请求参数来额外的获取表中的其它字段值
class SubProjectSelectShowViewSet(GenericReadOnlyListModelViewSet):
    queryset = SubProject.objects.all()
    pagination_class = None
    authentication_classes = [JwtAuthentication, JwtParamAuthentication, DenyAuthentication]
    # 允许哪些额外的透传字段
    ALLOWED_EXTRA_FIELDS = {
        f.name for f in SubProject._meta.get_fields()
        if isinstance(f, (models.CharField, models.IntegerField, models.ForeignKey, models.BooleanField))
        and f.name not in {'xxx', 'ooo'}  # 排除敏感字段
    }
    def list(self, request, *args, **kwargs):
         # 获取客户端请求的额外字段
        requested_fields = set(request.query_params.keys())
        # 过滤:只保留白名单中的字段
        extra_fields = requested_fields & self.ALLOWED_EXTRA_FIELDS  # 集合交集
        # 可选:记录非法字段(用于监控或告警)
        illegal_fields = requested_fields - self.ALLOWED_EXTRA_FIELDS
        if illegal_fields:
            # 可选:记录日志,但不报错(静默忽略)
            logger.warning(f"[验工计价] 请求子课题下拉列表接口,传递了错误的请求参数: {illegal_fields} from {request.META.get('REMOTE_ADDR')}")
        # 构建 values 字段列表
        fields_to_select = ['id', 'select_show']
        fields_to_select.extend(extra_fields)
        data = SubProject.objects.annotate(
            select_show=F('name')
        ).values(*fields_to_select)
        return Response({"code": 0, "detail": "success", "data": list(data)})

"""
请求示例:
/api/tunnelaccept/subproject/select/show/
返回:{"id": 317,"select_show": "2026-外部课题-子课题2"}

/api/tunnelaccept/subproject/select/show/?executing_units=1
返回:{"id": 317, "executing_units": 5,"select_show": "2026-外部课题-子课题2"}

/api/tunnelaccept/subproject/select/show/?parent_project=1&executing_units=1
返回:{"id": 317, "executing_units": 5,"select_show": "2026-外部课题-子课题2", "parent_project":163}
"""

如果被查询的表不是频繁改动的,可以通过结合redis进一步实现接口缓存,将性能优化做到极致.