انواع کش کردن در Django مبتنی بر API

1405/01/23 | 291 |
django

یکی از مهم ترین روش ها برای بهبود عملکرد API، کش کردن پاسخ ها است. در این مقاله، همه روش های کش کردن API در جنگو و Django REST Framework را بررسی می کنیم.

۱. پیکربندی کش برای API

ابتدا بک اند کش را پیکربندی کنید:

# settings.py
# استفاده از دیتابیس هایی مثل redis:
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': 'redis://127.0.0.1:6379/1',
        'OPTIONS': {
            'CLIENT_CLASS': 'django_redis.client.DefaultClient',
        }
    }
}

# یا برای توسعه محلی و داخل رم:
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
        'LOCATION': 'unique-snowflake',
    }
}

۲. کش کردن با استفاده از Decorator

۲.۱ کش ساده برای API View

# نمونه استفاده از object cache
from django.core.cache import cache
from rest_framework.views import APIView
from rest_framework.response import Response

class PostListAPIView(APIView):
    def get(self, request):
        cache_key = 'api_posts_list'
        posts = cache.get(cache_key)
        
        if not posts:
            posts = Post.objects.all()
            cache.set(cache_key, posts, 60 * 15)  # ۱۵ دقیقه
        
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

# نمونه استفاده از دکوراتور
from django.views.decorators.cache import cache_page, cache_control, never_cache
from django.views.decorators.vary import vary_on_cookie
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response

class PostListAPIView(APIView):
    @method_decorator(cache_control(max_age=60 * 15, public=True))
    @method_decorator(vary_on_cookie)
    def get(self, request):
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)


class UserProfileAPIView(APIView):
    @method_decorator(never_cache)  # هرگز کش نشود
    def get(self, request):
        return Response({
            'username': request.user.username,
            'email': request.user.email
        })

۲.۲ کش با پارامترهای درخواست

# کش با استفاده از object cache
import hashlib
from django.core.cache import cache
from rest_framework.views import APIView
from rest_framework.response import Response

class PostListAPIView(APIView):
    def get(self, request):
        # تولید کلید منحصر به فرد بر اساس پارامترها
        query_params = request.GET.urlencode()
        cache_key = f"api_posts_{hashlib.md5(query_params.encode()).hexdigest()}"
        
        posts = cache.get(cache_key)
        
        if not posts:
            # اعمال فیلترها
            posts = Post.objects.all()
            
            if category := request.GET.get('category'):
                posts = posts.filter(category=category)
            if tag := request.GET.get('tag'):
                posts = posts.filter(tags__slug=tag)
            if search := request.GET.get('q'):
                posts = posts.filter(title__icontains=search)
            
            cache.set(cache_key, list(posts.values()), 60 * 15)
        
        return Response(posts)

# نمونه استفاده از دکوراتور

from django.views.decorators.cache import cache_page
from django.utils.decorators import method_decorator
from rest_framework.views import APIView
from rest_framework.response import Response
import hashlib

class PostListAPIView(APIView):
    def get_cache_key(self, request):
        """کلید کش بر اساس پارامترها"""
        query = request.GET.urlencode()
        if query:
            hash_key = hashlib.md5(query.encode()).hexdigest()[:8]
            return f"api_posts_{hash_key}"
        return "api_posts_all"
    
    @method_decorator(cache_page(60 * 15))
    def get(self, request):
        cache_key = self.get_cache_key(request)
        
        # فیلترها
        posts = Post.objects.all()
        
        if category := request.GET.get('category'):
            posts = posts.filter(category__slug=category)
        if tag := request.GET.get('tag'):
            posts = posts.filter(tags__slug=tag)
        if search := request.GET.get('q'):
            posts = posts.filter(title__icontains=search)
        
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

۳. کش کردن با استفاده از Mixin

# utils/api_cache_mixin.py

import hashlib
from django.core.cache import cache

class CacheMixin:
    """Mixin برای کش کردن API Views"""
    
    cache_timeout = 60 * 15
    cache_key_prefix = 'api'
    cache_vary_on_user = False
    
    def _generate_cache_key(self):
        prefix = self.cache_key_prefix
        
        # تفکیک بر اساس کاربر
        if self.cache_vary_on_user:
            if self.request.user.is_authenticated:
                prefix = f"{prefix}_user_{self.request.user.id}"
            else:
                prefix = f"{prefix}_anon"
        
        # پارامترهای درخواست
        if self.request.GET:
            query_hash = hashlib.md5(
                self.request.GET.urlencode().encode()
            ).hexdigest()[:8]
            prefix = f"{prefix}_{query_hash}"
        
        # متد HTTP
        prefix = f"{prefix}_{self.request.method}"
        
        return prefix
    
    def get_cache_data(self):
        cache_key = self._generate_cache_key()
        return cache.get(cache_key)
    
    def set_cache_data(self, data):
        cache_key = self._generate_cache_key()
        cache.set(cache_key, data, self.cache_timeout)
    
    def get(self, request, *args, **kwargs):
        if hasattr(self, 'get_cache_data'):
            cached = self.get_cache_data()
            if cached is not None:
                return Response(cached)
        
        response = super().get(request, *args, **kwargs)
        
        if hasattr(self, 'set_cache_data') and response.status_code == 200:
            self.set_cache_data(response.data)
        
        return response

نحوه استفاده:

from utils.api_cache_mixin import CacheMixin

class PostListAPIView(CacheMixin, APIView):
    cache_timeout = 60 * 15
    cache_key_prefix = 'posts_list'
    cache_vary_on_user = False
    
    def get(self, request):
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

۴. کش کردن با استفاده از ViewSet

from rest_framework import viewsets
from django.core.cache import cache

class PostViewSet(viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    
    def get_cache_key(self):
        """تولید کلید کش"""
        prefix = 'post_viewset'
        
        # بر اساس اکشن
        prefix = f"{prefix}_{self.action}"
        
        # برای detail بر اساس pk
        if self.action == 'retrieve':
            return f"{prefix}_{self.kwargs.get('pk')}"
        
        # برای list بر اساس پارامترها
        if self.request.GET:
            query_hash = hashlib.md5(
                self.request.GET.urlencode().encode()
            ).hexdigest()[:8]
            prefix = f"{prefix}_{query_hash}"
        
        return prefix
    
    def list(self, request, *args, **kwargs):
        cache_key = self.get_cache_key()
        cached_data = cache.get(cache_key)
        
        if cached_data is not None:
            return Response(cached_data)
        
        response = super().list(request, *args, **kwargs)
        cache.set(cache_key, response.data, 60 * 15)
        
        return response
    
    def retrieve(self, request, *args, **kwargs):
        cache_key = self.get_cache_key()
        cached_data = cache.get(cache_key)
        
        if cached_data is not None:
            return Response(cached_data)
        
        response = super().retrieve(request, *args, **kwargs)
        cache.set(cache_key, response.data, 60 * 30)
        
        return response
    
    def perform_create(self, serializer):
        serializer.save()
        cache.clear()  # پاک کردن کش لیست
    
    def perform_update(self, serializer):
        serializer.save()
        cache.clear()
    
    def perform_destroy(self, instance):
        instance.delete()
        cache.clear()

۵. کش کردن با استفاده از کتابخانه drf-extensions

این کتابخانه امکانات پیشرفته ای برای کش کردن فراهم می کند:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_CACHE_KEY_DEFAULT_PERMISSIONS': [],
}

# views.py
from rest_framework_extensions.cache.decorators import cache_response
from rest_framework_extensions.cache.mixins import CacheResponseMixin

class PostViewSet(CacheResponseMixin, viewsets.ModelViewSet):
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    
    @cache_response(timeout=60*15, key_func='get_cache_key')
    def list(self, request, *args, **kwargs):
        return super().list(request, *args, **kwargs)

۶. کش کردن با استفاده از CachingRouter

# urls.py
from rest_framework_extensions.routers import CachingRouter

router = CachingRouter()
router.register(r'posts', PostViewSet, basename='post')

urlpatterns = router.urls

۷. ETags و Conditional Responses

جنگو پشتیبانی داخلی از ETag دارد:

from django.views.decorators.http import etag
from django.views.decorators.cache import cache_control

class PostListAPIView(APIView):
    def get(self, request):
        posts = list(Post.objects.values('id', 'title', 'created_at'))
        
        # تولید ETag
        import hashlib
        etag_value = hashlib.md5(str(posts).encode()).hexdigest()
        
        # بررسی If-None-Match
        if_none_match = request.headers.get('If-None-Match')
        if if_none_match == etag_value:
            from rest_framework.response import Response
            return Response(status=304)
        
        response = Response(posts)
        response['ETag'] = etag_value
        return response

یا با استفاده از DRF:

from rest_framework.response import Response
from rest_framework.views import APIView
from django_etag import etag

class PostListAPIView(APIView):
    @etag(etag_func=lambda request, view: f"posts_list_{request.GET.urlencode()}")
    def get(self, request):
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        return Response(serializer.data)

۸. کش کردن با Vary Headers در API

from django.views.decorators.vary import vary_on_cookie
from django.views.decorators.http import require_GET

@require_GET
@vary_on_cookie
def api_view(request):
    # کش جدا برای هر کاربر
    ...

در DRF:

from rest_framework.views import APIView
from rest_framework.response import Response

class UserProfileAPIView(APIView):
    def get(self, request):
        # تنظیم Vary
        response = Response(request.user.profile)
        response['Vary'] = 'Cookie, Authorization'
        return response

۹. کش کردن با Redis و Json

برای عملکرد بهتر، داده ها را به صورت JSON ذخیره کنید:

import json
from django.core.cache import cache
from rest_framework.response import Response

class PostListAPIView(APIView):
    def get(self, request):
        cache_key = 'api_posts_json'
        cached = cache.get(cache_key)
        
        if cached:
            # تبدیل JSON به داده
            data = json.loads(cached)
            return Response(data)
        
        posts = Post.objects.all()
        serializer = PostSerializer(posts, many=True)
        data = serializer.data
        
        # ذخیره به صورت JSON
        cache.set(cache_key, json.dumps(data), 60 * 15)
        
        return Response(data)

۱۰. Invalid کردن کش API

۱۰.۱ با استفاده از سیگنال ها

# signals.py
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.core.cache import cache

@receiver(post_save, sender=Post)
def invalidate_post_cache(sender, instance, **kwargs):
    # پاک کردن کش‌های مربوط به پست
    cache.delete('api_posts_list')
    cache.delete(f'api_post_detail_{instance.pk}')
    cache.delete(f'api_post_detail_{instance.slug}')
    
    # پاک کردن کش‌های فیلتر شده
    if instance.category:
        cache.delete(f'api_posts_category_{instance.category.slug}')

@receiver(post_delete, sender=Post)
def invalidate_post_cache_on_delete(sender, instance, **kwargs):
    invalidate_post_cache(sender, instance, **kwargs)

۱۰.۲ با استفاده از Pattern

from django_redis import get_redis_connection

def invalidate_api_cache(pattern):
    conn = get_redis_connection("default")
    keys = conn.keys(f"*{pattern}*")
    if keys:
        conn.delete(*keys)

# استفاده
invalidate_api_cache('api_posts')

۱۰.۳ در ViewSet

class PostViewSet(viewsets.ModelViewSet):
    def perform_create(self, serializer):
        serializer.save()
        self._clear_cache()
    
    def perform_update(self, serializer):
        serializer.save()
        self._clear_cache()
    
    def perform_destroy(self, instance):
        instance.delete()
        self._clear_cache()
    
    def _clear_cache(self):
        from django.core.cache import cache
        # پاک کردن کش‌های مرتبط
        cache.delete('api_posts_list')
        # یا استفاده از pattern
        conn = get_redis_connection("default")
        conn.delete(*conn.keys('*api_posts*'))

۱۱. کش کردن با Rate Limiting ترکیبی

# settings.py

REST_FRAMEWORK = {
    'DEFAULT_THROTTLE_CLASSES': [
        'rest_framework.throttling.AnonRateThrottle',
        'rest_framework.throttling.UserRateThrottle',
    ],
    'DEFAULT_THROTTLE_RATES': {
        'anon': '100/minute',
        'user': '1000/minute',
    },
    'DEFAULT_CACHE_KEY_THROTTLE': 'throttle_%(scope)s_%(ident)s',
}

۱۲. کش کردن با استفاده از Middleware

# utils/api_cache_middleware.py

from django.core.cache import cache
from django.utils.deprecation import MiddlewareMixin

class APICacheMiddleware(MiddlewareMixin):
    """Middleware برای کش کردن خودکار API"""
    
    CACHE_TIMEOUT = 60 * 15
    
    def process_response(self, request, response):
        # فقط GET requests
        if request.method != 'GET':
            return response
        
        # فقط پاسخ‌های موفق
        if response.status_code != 200:
            return response
        
        # فقط API URLs
        if not request.path.startswith('/api/'):
            return response
        
        # تولید کلید کش
        cache_key = f"api_cache_{request.path}_{request.GET.urlencode()}"
        
        # ذخیره در کش
        cache.set(cache_key, response.content, self.CACHE_TIMEOUT)
        
        return response
    
    def process_request(self, request):
        # بررسی کش قبل از پردازش
        if request.method != 'GET':
            return None
        
        if not request.path.startswith('/api/'):
            return None
        
        cache_key = f"api_cache_{request.path}_{request.GET.urlencode()}"
        cached_content = cache.get(cache_key)
        
        if cached_content:
            from django.http import HttpResponse
            return HttpResponse(cached_content, content_type='application/json')
        
        return None
# settings.py

MIDDLEWARE = [
    # ...
    'utils.api_cache_middleware.APICacheMiddleware',
]

۱۳. کش کردن با پارامترهای احراز هویت

class PostListAPIView(APIView):
    def get(self, request):
        # کلید کش بر اساس نوع کاربر
        if request.user.is_authenticated:
            cache_key = f"api_posts_user_{request.user.id}"
        else:
            cache_key = "api_posts_anon"
        
        # اضافه کردن پارامترها
        if request.GET:
            cache_key = f"{cache_key}_{request.GET.urlencode()}"
        
        cached = cache.get(cache_key)
        
        if cached is not None:
            return Response(cached)
        
        posts = Post.objects.all()
        
        # پست‌های عمومی برای همه
        # پست‌های خصوصی فقط برای کاربر
        if not request.user.is_authenticated:
            posts = posts.filter(is_public=True)
        
        serializer = PostSerializer(posts, many=True)
        cache.set(cache_key, serializer.data, 60 * 15)
        
        return Response(serializer.data)

مثال کامل با ViewSet

# views.py
import hashlib
from rest_framework import viewsets
from rest_framework.response import Response
from django.core.cache import cache

class PostViewSet(viewsets.ModelViewSet):
    """ViewSet کامل با کش"""
    
    queryset = Post.objects.all()
    serializer_class = PostSerializer
    
    def get_cache_key(self):
        """تولید کلید کش هوشمند"""
        parts = ['api', 'posts', self.action]
        
        # تفکیک بر اساس کاربر
        if self.request.user.is_authenticated:
            parts.append(f"user_{self.request.user.id}")
        
        # اضافه کردن پارامترها
        if self.request.GET:
            query_hash = hashlib.md5(
                self.request.GET.urlencode().encode()
            ).hexdigest()[:8]
            parts.append(query_hash)
        
        # اضافه کردن pk برای detail
        if self.action == 'retrieve' and 'pk' in self.kwargs:
            parts.append(f"pk_{self.kwargs['pk']}")
        
        return '_'.join(parts)
    
    def get_cache_timeout(self):
        """زمان کش بر اساس اکشن"""
        timeouts = {
            'list': 60 * 15,      # ۱۵ دقیقه
            'retrieve': 60 * 30, # ۳۰ دقیقه
            'create': 0,          # کش نشود
            'update': 0,
            'destroy': 0,
        }
        return timeouts.get(self.action, 60 * 15)
    
    def list(self, request, *args, **kwargs):
        cache_key = self.get_cache_key()
        timeout = self.get_cache_timeout()
        
        if timeout > 0:
            cached = cache.get(cache_key)
            if cached is not None:
                return Response(cached)
        
        response = super().list(request, *args, **kwargs)
        
        if timeout > 0:
            cache.set(cache_key, response.data, timeout)
        
        return response
    
    def retrieve(self, request, *args, **kwargs):
        cache_key = self.get_cache_key()
        timeout = self.get_cache_timeout()
        
        if timeout > 0:
            cached = cache.get(cache_key)
            if cached is not None:
                return Response(cached)
        
        response = super().retrieve(request, *args, **kwargs)
        
        if timeout > 0:
            cache.set(cache_key, response.data, timeout)
        
        return response
    
    def perform_create(self, serializer):
        serializer.save()
        self._invalidate_cache()
    
    def perform_update(self, serializer):
        serializer.save()
        self._invalidate_cache()
    
    def perform_destroy(self, instance):
        instance.delete()
        self._invalidate_cache()
    
    def _invalidate_cache(self):
        """پاک کردن کش مرتبط"""
        # پاک کردن کش لیست
        cache.delete('api_posts_list')
        
        # پاک کردن با pattern
        try:
            from django_redis import get_redis_connection
            conn = get_redis_connection("default")
            keys = conn.keys('api_posts_*')
            if keys:
                conn.delete(*keys)
        except:
            pass

نتیجه گیری

برای کش کردن API در جنگو، چندین روش دارید:

  • API ساده: از Decorator ساده استفاده کنید
  • چندین View مشابه: از Mixin استفاده کنید
  • API پیچیده با CRUD: از ViewSet استفاده کنید
  • کش خودکار: از Middleware استفاده کنید
  • کاهش ترافیک: از ETags استفاده کنید

فراموش نکنید که کش را در زمان مناسب invalid کنید تا کاربران داده های قدیمی دریافت نکنند!


اشتراک گذاری:

ثبت دیدگاه


نکته: آدرس ایمیل شما منتشر نخواهد شد
0/600

دیدگاه کاربران (0)


هیچ دیدگاهی ثبت نشده است. می‌توانید اولین نفر باشید.