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

Django模型管理器中访问请求实现默认租户过滤的可行性探讨

问题描述

我的代码库中有一个模型被300多个地方调用,要求该模型默认必须通过company_filter进行过滤,无例外情况。我需要从请求中获取tenant_id和tenant_type这两个值(每个请求都能拿到),先通过tenant_id过滤Company模型,再把company、tenant_id和tenant_type传入过滤器来过滤目标模型的查询集。当前方案看起来可行,但我担心会出现内存泄漏或者不同用户请求混淆的问题,想问下这个方案是否有效,有没有更优的实现方式?

中间件代码

import threading

_thread_local = threading.local()


class RequestMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        _thread_local.request = request

        response = self.get_response(request)

        # 视图返回后释放内存
        del _thread_local.request

        return response


def get_request():
    return getattr(_thread_local, 'request', None)

模型管理器代码

class TariffrateCompanyManger(models.Manager):
    # 默认按公司过滤费率
    def get_queryset(self):
        from config.middleware import get_request

        request = get_request()

        company = Company.objects.get_company_tenant_id(request.tenant_id)

        company_filter = tariff_company_filter(company, request.tenant_id, request.tenant_type)

        if request:
            qs = super().get_queryset().filter(company_filter)
        else:
            qs = super().get_queryset()

        return qs

方案分析与优化建议

当前方案的有效性

你的方案不会出现请求混淆问题:threading.local()是线程隔离的,每个请求在独立线程中处理,各自的request对象只会绑定到当前线程的本地存储,不同用户的请求数据完全隔离,不会串用。

关于内存泄漏:你在中间件中做了del _thread_local.request的清理操作,但存在一个漏洞——如果请求处理过程中抛出异常,del语句会因为代码中断而无法执行,线程本地存储里的request可能无法及时清理。可以把清理逻辑放到finally块中,确保无论请求是否成功都会执行:

import threading

_thread_local = threading.local()


class RequestMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        _thread_local.request = request
        try:
            response = self.get_response(request)
        finally:
            # 无论请求是否正常完成,都清理线程本地存储的request
            del _thread_local.request
        return response


def get_request():
    return getattr(_thread_local, 'request', None)

另外,模型管理器代码有潜在风险:如果get_request()返回None(比如非请求上下文调用,如后台任务、命令行),直接访问request.tenant_id会抛出AttributeError。同时你要求无例外过滤,所以必须处理参数缺失或非请求场景,避免返回未过滤的数据:

class TariffrateCompanyManger(models.Manager):
    # 默认按公司过滤费率
    def get_queryset(self):
        from config.middleware import get_request

        request = get_request()
        qs = super().get_queryset()

        if request:
            # 先校验请求是否包含必要参数
            if hasattr(request, 'tenant_id') and hasattr(request, 'tenant_type'):
                company = Company.objects.get_company_tenant_id(request.tenant_id)
                company_filter = tariff_company_filter(company, request.tenant_id, request.tenant_type)
                qs = qs.filter(company_filter)
            else:
                # 参数缺失时抛出异常,避免未过滤数据泄露
                raise ValueError("请求缺少tenant_id或tenant_type,无法应用公司过滤")
        else:
            # 非请求上下文强制抛出异常,确保只有过滤后的查询集能被使用
            raise RuntimeError("非请求上下文调用,请手动指定过滤条件")

        return qs

更优实现方式

1. 减少重复查询,提升性能

每次调用get_queryset都查询Company模型会增加数据库开销,可以在中间件中提前查询并缓存到线程本地存储:

import threading
from django.core.exceptions import ObjectDoesNotExist

_thread_local = threading.local()


class RequestMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        _thread_local.request = request
        try:
            # 提前查询并缓存company
            if hasattr(request, 'tenant_id'):
                try:
                    _thread_local.company = Company.objects.get_company_tenant_id(request.tenant_id)
                except ObjectDoesNotExist:
                    _thread_local.company = None
            response = self.get_response(request)
        finally:
            # 清理所有线程本地存储的变量
            for attr in ['request', 'company']:
                if hasattr(_thread_local, attr):
                    delattr(_thread_local, attr)
        return response


def get_request():
    return getattr(_thread_local, 'request', None)

def get_current_company():
    return getattr(_thread_local, 'company', None)

修改模型管理器直接使用缓存:

class TariffrateCompanyManger(models.Manager):
    # 默认按公司过滤费率
    def get_queryset(self):
        from config.middleware import get_request, get_current_company

        request = get_request()
        qs = super().get_queryset()

        if request:
            if hasattr(request, 'tenant_id') and hasattr(request, 'tenant_type'):
                company = get_current_company()
                if not company:
                    raise ValueError("不存在对应tenant_id的公司")
                company_filter = tariff_company_filter(company, request.tenant_id, request.tenant_type)
                qs = qs.filter(company_filter)
            else:
                raise ValueError("请求缺少tenant_id或tenant_type,无法应用公司过滤")
        else:
            raise RuntimeError("非请求上下文调用,请手动指定过滤条件")

        return qs

2. 降低耦合度(可选)

模型管理器属于数据层,依赖请求上下文会增加代码耦合度。如果后续有精力优化300个调用点,可以考虑:

  • 定义全局过滤函数,在视图/服务层统一应用过滤逻辑
  • 使用自定义查询集结合上下文管理器,将过滤参数与请求生命周期绑定

但考虑到你现有调用点数量,当前基于线程本地存储的方案是成本最低的选择。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 22:24:53