Django REST Framework 缓存错误

Posted

技术标签:

【中文标题】Django REST Framework 缓存错误【英文标题】:Django REST Framework caching bug 【发布时间】:2016-02-23 03:40:51 【问题描述】:

TL;DR

我正在寻找一种方法来在请求后清除缓存,或者在运行测试时完全禁用它。 Django REST Framework 似乎缓存了结果,我需要一种解决方法。 p>

长版和代码

好吧,当我不断测试它时,结果证明它的行为非常奇怪。最后,我让它工作了,但我真的不喜欢我的解决方法,并且以知识的名义,我必须找出为什么会发生这种情况以及如何正确解决这个问题。

所以,我有一个 APITestCase 类声明如下:

class UserTests(APITestCase):

在这个类中,我的user-list 视图有一个测试函数,因为我有一个取决于权限的自定义查询集。澄清事情:

超级用户可以获得整个用户列表(返回 4 个实例), 员工看不到超级用户(返回 3 个实例), 普通用户只能得到 1 个结果,他们自己的用户(返回 1 个实例)

有效的测试功能版本:

def test_user_querysets(self):

    url = reverse('user-list')

    # Creating a user
    user = User(username='user', password=self.password)
    user.set_password(self.password)
    user.save()

    # Creating a second user
    user2 = User(username='user2', password=self.password)
    user2.set_password(self.password)
    user2.save()

    # Creating a staff user
    staff_user = User(username='staff_user', password=self.password, is_staff=True)
    staff_user.set_password(self.password)
    staff_user.save()

    # Creating a superuser
    superuser = User(username='superuser', password=self.password, is_staff=True, is_superuser=True)
    superuser.set_password(self.password)
    superuser.save()



    # SUPERUSER

    self.client.logout()
    self.client.login(username=superuser.username, password=self.password)

    response = self.client.get(url)

    # HTTP_200_OK
    self.assertEqual(response.status_code, status.HTTP_200_OK)

    # All users contained in list
    self.assertEqual(response.data['extras']['total_results'], 4)



    # STAFF USER

    self.client.logout()
    self.client.login(username=staff_user.username, password=self.password)

    response = self.client.get(url)

    # HTTP_200_OK
    self.assertEqual(response.status_code, status.HTTP_200_OK)

    # Superuser cannot be contained in list
    self.assertEqual(response.data['extras']['total_results'], 3)



    # REGULAR USER

    self.client.logout()
    self.client.login(username=user2.username, password=self.password)

    response = self.client.get(url)

    # HTTP_200_OK
    self.assertEqual(response.status_code, status.HTTP_200_OK)

    # Only 1 user can be returned
    self.assertEqual(response.data['extras']['total_results'], 1)

    # User returned is current user
    self.assertEqual(response.data['users'][0]['username'], user2.username)

如您所见,我按以下顺序测试用户权限:超级用户、员工、普通用户。这行得通,所以...

有趣的事:

如果我更改测试的顺序,从普通用户、员工、超级用户开始,测试会失败。第一个请求的响应被缓存,然后我以员工用户身份登录时得到相同的响应,因此结果数再次为1。

不起作用的版本:

和之前一模一样,只是测试顺序倒了

def test_user_querysets(self):

    url = reverse('user-list')

    # Creating a user
    user = User(username='user', password=self.password)
    user.set_password(self.password)
    user.save()

    # Creating a second user
    user2 = User(username='user2', password=self.password)
    user2.set_password(self.password)
    user2.save()

    # Creating a staff user
    staff_user = User(username='staff_user', password=self.password, is_staff=True)
    staff_user.set_password(self.password)
    staff_user.save()

    # Creating a superuser
    superuser = User(username='superuser', password=self.password, is_staff=True, is_superuser=True)
    superuser.set_password(self.password)
    superuser.save()



    # REGULAR USER

    self.client.logout()
    self.client.login(username=user2.username, password=self.password)

    response = self.client.get(url)

    # HTTP_200_OK
    self.assertEqual(response.status_code, status.HTTP_200_OK)

    # Only 1 user can be returned
    self.assertEqual(response.data['extras']['total_results'], 1)

    # User returned is current user
    self.assertEqual(response.data['users'][0]['username'], user2.username)



    # STAFF USER

    self.client.logout()
    self.client.login(username=staff_user.username, password=self.password)

    response = self.client.get(url)

    # HTTP_200_OK
    self.assertEqual(response.status_code, status.HTTP_200_OK)

    # Superuser cannot be contained in list
    self.assertEqual(response.data['extras']['total_results'], 3)



    # SUPERUSER

    self.client.logout()
    self.client.login(username=superuser.username, password=self.password)

    response = self.client.get(url)

    # HTTP_200_OK
    self.assertEqual(response.status_code, status.HTTP_200_OK)

    # All users contained in list
    self.assertEqual(response.data['extras']['total_results'], 4)

我在 python 2.7 中使用以下软件包版本:

Django==1.8.6
djangorestframework==3.3.1
Markdown==2.6.4
mysql-python==1.2.5
wheel==0.24.0

更新

我使用的是默认的 django 缓存,这意味着我没有在 django 设置中添加任何关于缓存的内容。

按照建议,我尝试禁用默认的 Django 缓存:

CACHES = 
    'default': 
        'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
    


REST_FRAMEWORK = 
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.SessionAuthentication',
    )

问题仍然存在。

虽然我不认为问题出在此处,但这是我的 UserViewSet:

api.py(重要部分)

class UserViewSet(  
    mixins.RetrieveModelMixin, 
    mixins.UpdateModelMixin,
    mixins.ListModelMixin,
    viewsets.GenericViewSet
):
    queryset = User.objects.all()
    serializer_class = UserExpenseSerializer
    permission_classes = (IsAuthenticated, )
    allowed_methods = ('GET', 'PATCH', 'OPTIONS', 'HEAD')

    def get_serializer_class(self):
        if self.action == 'retrieve':
            return UserExpenseSerializer
        return UserSerializer

    def get_queryset(self):
        if(self.action == 'list'):
            return User.objects.all()
        if self.request.user.is_superuser:
            return User.objects.all()
        if self.request.user.is_staff:
            return User.objects.exclude(is_superuser=True)
        return User.objects.filter(pk = self.request.user.id)

    def list(self, request):
        filter_obj = UsersFilter(self.request)
        users = filter_obj.do_query()
        extras = filter_obj.get_extras()
        serializer = UserSerializer(users, context='request' : request, many=True)
        return Response('users' : serializer.data, 'extras' : extras, views.status.HTTP_200_OK)

filters.py

class UsersFilter:
    offset = 0
    limit = 50
    count = 0
    total_pages = 0
    filter_params = 

    def __init__(self, request):

        if not request.user.is_superuser:
            self.filter_params['is_superuser'] = False

        if (not request.user.is_superuser and not request.user.is_staff):
            self.filter_params['pk'] = request.user.id

        # Read query params
        rpp = request.query_params.get('rpp') or 50
        page = request.query_params.get('page') or 1
        search_string = request.query_params.get('search')

        # Validate

        self.rpp = int(rpp) or 50
        self.page = int(page) or 1

        # Set filter
        set_if_not_none(self.filter_params, 'username__contains', search_string)

        # Count total results

        self.count = User.objects.filter(**self.filter_params).count()
        self.total_pages = int(self.count / self.rpp) + 1

        # Set limits
        self.offset = (self.page - 1) * self.rpp
        self.limit = self.page * self.rpp

    def get_filter_params(self):
        return self.filter_params

    def get_offset(self):
        return self.offset

    def get_limit(self):
        return self.limit

    def do_query(self):
        users = User.objects.filter(**self.filter_params)[self.offset:self.limit]
        return users

    def get_query_info(self):
        query_info = 
            'total_results' : self.count,
            'results_per_page' : self.rpp,
            'current_page' : self.page,
            'total_pages' : self.total_pages
        
        return query_info

更新 2

正如 Linovia 所指出的,问题不是缓存或任何其他 DRF 问题,而是过滤器。这是固定的过滤器类:

class UsersFilter:

    def __init__(self, request):

        self.filter_params = 
        self.offset = 0
        self.limit = 50
        self.count = 0
        self.total_pages = 0
        self.extras = 

        if not request.user.is_superuser:
        # and so long...

【问题讨论】:

如果self.clientdjango.test.Client 并且您作为不同的用户进行交互,为什么不创建一个新的Client()?无论如何,这应该更接近于正在发生的事情的模型。 Client 实例是与您的站点的独立浏览器交互。也许这就是 Django 感到困惑的原因。 我确实试过了,同样的问题出现了。它与 DRF 缓存有关。另一个有趣的事情是,如果我在普通用户之前测试员工用户,则测试通过。让我大吃一惊。 您确定要限制您查询的url 的结果吗?你的queryset 参数或get_queryset() 函数在那个视图上是什么样子的? 完全确定,在可浏览的 api 中检查了 50 次 :)。我现在将粘贴查询集。 事实上我做不到,它还包括一个数据过滤功能,用几个query_params自定义查询集。但是,这部分工作正常,因为我已经对其进行了很多次测试,并且该应用程序实际上可以按我的意愿工作。而且,正如我所说,如果我首先请求一个 staff_user ,则测试有效:))) 【参考方案1】:

实际上,您创建了一个新用户,该用户应该有 2 个用户,并且您针对 3 断言长度。即使没有缓存也无法工作。

编辑: 所以你实际上有你的问题,因为在类级别使用可变对象。

这是邪恶的代码:

class UsersFilter:
    filter_params = 

    def __init__(self, request):
        if not request.user.is_superuser:
            self.filter_params['is_superuser'] = False

实际上应该是:

class UsersFilter:
    def __init__(self, request):
        filter_params = 
        if not request.user.is_superuser:
            self.filter_params['is_superuser'] = False

否则,UsersFilter.filter_params 将从一个请求发送到另一个请求,并且永远不会重置。有关这方面的更多详细信息,请参阅http://www.toptal.com/python/python-class-attributes-an-overly-thorough-guide。

【讨论】:

那不是问题,我没有放整个代码...在此之前创建了2个普通用户,然后此代码sn-p开始。问题不是一些小错误,我知道这是经过 2 小时的测试和尝试不同方法后的缓存。我会马上更新sn-p。 问题更新了,有需要的可以看一下。 奇怪的行为。这感觉就像您已经在模块或类级别上设置了查询集,并在此之后进行过滤,这适用于第一个示例,因为它将拥有所有用户,而不是第二个(QS 只有一个项目)。跨度> 它工作正常,因为您从最宽的过滤器变成了最小的过滤器。因此,每个新查询都“隐藏”了前一个查询 啊,是的,我现在觉得自己很笨:)【参考方案2】:

您可以通过将其添加到您的 settings.py 来禁用调试模式下的缓存

if DEBUG:
    CACHES = 
        'default': 
            'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
        
    

https://docs.djangoproject.com/en/dev/topics/cache/?from=olddocs/#dummy-caching-for-development

然后,您可以通过在 settings.py 中切换 DEBUG 或使用单独的开发/测试和 /deploy settings.py 文件来禁用缓存。


如果您想避免单独的文件或切换,则可以使用 override_settings 装饰器将 DEBUG 设置为 true 以进行某些测试:

from django.test.utils import override_settings
from django.test import TestCase
from django.conf import settings

class MyTest(TestCase):
    @override_settings(DEBUG=True)
    def test_debug(self):
        self.assertTrue(settings.DEBUG)

【讨论】:

是的,这在文档中,但是只有在我运行测试而不每次都编辑 settings.py 文件时,如何才能禁用它? 您可以添加if 'test' in sys.argv:,这样只会在运行python manage.py test 时定义。过去我也使用过if DEBUG: if DEBUG 听起来是个不错的解决方案,能否请您提供更详细的答案以及确切的代码示例,如何在代码执行中更改缓存引擎,以便我可以测试并接受它? 支持此信息,但不幸的是它并没有解决问题(我什至省略了 if 子句,只是禁用了缓存)。我想这与 Django 没有任何关系,而是 DRF 中的一些错误。我一遍又一遍地阅读文档,没有关于禁用缓存的内容,只有那些默认情况下似乎禁用的限制类。好奇怪……

以上是关于Django REST Framework 缓存错误的主要内容,如果未能解决你的问题,请参考以下文章

在 Django Rest Framework 中缓存序列化程序响应

如何优化 Django REST Framework 的性能

Django rest 框架缓存策略

每个用户请求的 Django Rest Framework 指标

Django-rest-framework 和 django-rest-framework-jwt APIViews and validation Authorization headers

Django Rest Framework 和 django Rest Framework simplejwt 两因素身份验证