Django

⌘K
  1. Home
  2. Django
  3. Speed Optimization
  4. Part 3: Views Optimization

Part 3: Views Optimization

Views হলো যেখানে সবচেয়ে বেশি optimization করা যায়। এখানে database queries, caching, এবং response generation সব হয়।

🔴 Normal Views (Slow)

shop/views.py – Normal Version:

from django.shortcuts import render, get_object_or_404
from django.views.generic import ListView, DetailView
from .models import Product, Category, Brand, Review, Order

# ❌ Function-based view (No optimization)
def product_list(request):
    """সব products দেখান - Slow!"""
    products = Product.objects.all()  # ❌ No select_related

    # ❌ Template এ loop করলে N+1 problem
    # {% for product in products %}
    #   {{ product.category.name }}  <- Extra query!
    # {% endfor %}

    context = {'products': products}
    return render(request, 'shop/product_list.html', context)


def product_detail(request, pk):
    """একটি product এর details - Slow!"""
    product = get_object_or_404(Product, pk=pk)  # ❌ No select_related

    # ❌ প্রতিটি access এ extra query
    category_name = product.category.name  # Extra query!
    brand_name = product.brand.name        # Extra query!

    # ❌ Reviews fetch করতে extra query
    reviews = product.review_set.all()  # Extra query!

    # ❌ Average rating calculate করতে query
    avg_rating = reviews.aggregate(Avg('rating'))  # Extra query!

    context = {
        'product': product,
        'reviews': reviews,
        'avg_rating': avg_rating,
    }
    return render(request, 'shop/product_detail.html', context)


# ❌ Class-based view (No optimization)
class ProductListView(ListView):
    model = Product
    template_name = 'shop/product_list.html'
    context_object_name = 'products'
    paginate_by = 20

    # ❌ queryset optimize নেই
    # N+1 problem হবে template এ


class ProductDetailView(DetailView):
    model = Product
    template_name = 'shop/product_detail.html'
    context_object_name = 'product'

    # ❌ Related data fetch optimize নেই
    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        # ❌ Extra queries
        context['reviews'] = self.object.review_set.all()
        context['related_products'] = Product.objects.filter(
            category=self.object.category
        ).exclude(pk=self.object.pk)[:4]

        return context


def category_products(request, category_id):
    """একটি category এর সব products - Slow!"""
    category = get_object_or_404(Category, pk=category_id)
    products = Product.objects.filter(category=category)  # ❌ No optimization

    context = {
        'category': category,
        'products': products,
    }
    return render(request, 'shop/category_products.html', context)


def user_orders(request):
    """User এর সব orders - Slow!"""
    if not request.user.is_authenticated:
        return redirect('login')

    # ❌ No prefetch_related for items
    orders = Order.objects.filter(user=request.user)

    # ❌ Template এ loop করলে N+1 problem
    # {% for order in orders %}
    #   {% for item in order.items.all %}  <- Extra query per order!
    #   {% endfor %}
    # {% endfor %}

    context = {'orders': orders}
    return render(request, 'shop/user_orders.html', context)

❌ Normal Views এর সমস্যা:

  1. N+1 Query Problem – ForeignKey access এ extra queries
  2. No Caching – Same data বারবার fetch
  3. Inefficient Filtering – Index ব্যবহার না করা
  4. No Pagination – সব data একসাথে load
  5. Template এ Heavy Logic – Database queries template এ

✅ Optimized Views (Fast)

shop/views.py – Optimized Version:

from django.shortcuts import render, get_object_or_404, redirect
from django.views.generic import ListView, DetailView
from django.core.cache import cache
from django.db.models import Prefetch, Q, Count, Avg, F
from django.core.paginator import Paginator
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page
from .models import Product, Category, Brand, Review, Order, OrderItem

# ✅ Function-based view (Optimized)
def product_list_optimized(request):
    """সব products দেখান - Fast!"""

    # ✅ Cache check করুন
    cache_key = 'product_list_active'
    products = cache.get(cache_key)

    if products is None:
        # ✅ Optimized queryset
        products = Product.objects.filter(is_active=True)\
            .select_related('category', 'brand')\
            .only(
                'id', 'name', 'slug', 'price', 'stock',
                'average_rating', 'review_count',
                'category__name', 'brand__name'
            )\
            .order_by('-created_at')

        # ✅ Cache করুন (5 মিনিট)
        cache.set(cache_key, products, 300)

    # ✅ Pagination
    paginator = Paginator(products, 20)
    page_number = request.GET.get('page', 1)
    page_obj = paginator.get_page(page_number)

    context = {'page_obj': page_obj}
    return render(request, 'shop/product_list.html', context)


def product_detail_optimized(request, slug):
    """একটি product এর details - Fast!"""

    # ✅ Cache check
    cache_key = f'product_detail_{slug}'
    cached_data = cache.get(cache_key)

    if cached_data:
        return render(request, 'shop/product_detail.html', cached_data)

    # ✅ Optimized query with all relations
    product = get_object_or_404(
        Product.objects.select_related('category', 'brand')
                      .prefetch_related(
                          Prefetch(
                              'reviews',
                              queryset=Review.objects.select_related('user')
                                                    .order_by('-created_at')[:10]
                          )
                      ),
        slug=slug,
        is_active=True
    )

    # ✅ Related products (optimized)
    related_products = Product.objects.filter(
        category=product.category,
        is_active=True
    ).exclude(pk=product.pk)\
     .select_related('category', 'brand')\
     .only('id', 'name', 'slug', 'price', 'average_rating')[:4]

    context = {
        'product': product,
        'related_products': related_products,
    }

    # ✅ Cache করুন (10 মিনিট)
    cache.set(cache_key, context, 600)

    return render(request, 'shop/product_detail.html', context)


# ✅ Class-based view (Fully Optimized)
class ProductListViewOptimized(ListView):
    model = Product
    template_name = 'shop/product_list.html'
    context_object_name = 'products'
    paginate_by = 20

    def get_queryset(self):
        """✅ Optimized queryset"""
        queryset = Product.objects.filter(is_active=True)

        # ✅ select_related for ForeignKeys
        queryset = queryset.select_related('category', 'brand')

        # ✅ only() - শুধু প্রয়োজনীয় fields
        queryset = queryset.only(
            'id', 'name', 'slug', 'price', 'stock',
            'is_featured', 'average_rating', 'review_count',
            'category__id', 'category__name', 'category__slug',
            'brand__id', 'brand__name', 'brand__slug'
        )

        # ✅ Filtering (indexed fields)
        category_slug = self.request.GET.get('category')
        if category_slug:
            queryset = queryset.filter(category__slug=category_slug)

        brand_slug = self.request.GET.get('brand')
        if brand_slug:
            queryset = queryset.filter(brand__slug=brand_slug)

        # ✅ Price range filter
        min_price = self.request.GET.get('min_price')
        if min_price:
            queryset = queryset.filter(price__gte=min_price)

        max_price = self.request.GET.get('max_price')
        if max_price:
            queryset = queryset.filter(price__lte=max_price)

        # ✅ Search (indexed fields)
        search = self.request.GET.get('q')
        if search:
            queryset = queryset.filter(
                Q(name__icontains=search) |
                Q(description__icontains=search)
            )

        # ✅ Sorting
        sort = self.request.GET.get('sort', '-created_at')
        allowed_sorts = [
            'price', '-price',
            'name', '-name',
            '-created_at', 'created_at',
            '-average_rating', 'average_rating'
        ]
        if sort in allowed_sorts:
            queryset = queryset.order_by(sort)

        return queryset

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        # ✅ Categories for filter (cached)
        cache_key = 'categories_with_count'
        categories = cache.get(cache_key)
        if categories is None:
            categories = Category.objects.annotate(
                product_count=Count('products', filter=Q(products__is_active=True))
            ).filter(product_count__gt=0)
            cache.set(cache_key, categories, 600)

        context['categories'] = categories

        # ✅ Brands for filter (cached)
        cache_key = 'brands_with_count'
        brands = cache.get(cache_key)
        if brands is None:
            brands = Brand.objects.filter(is_active=True).annotate(
                product_count=Count('products', filter=Q(products__is_active=True))
            ).filter(product_count__gt=0)
            cache.set(cache_key, brands, 600)

        context['brands'] = brands

        return context


class ProductDetailViewOptimized(DetailView):
    model = Product
    template_name = 'shop/product_detail.html'
    context_object_name = 'product'
    slug_field = 'slug'
    slug_url_kwarg = 'slug'

    def get_queryset(self):
        """✅ Optimized queryset with all relations"""
        return Product.objects.filter(is_active=True)\
            .select_related('category', 'brand')\
            .prefetch_related(
                Prefetch(
                    'reviews',
                    queryset=Review.objects.select_related('user')
                                          .order_by('-created_at')
                ),
                'reviews__user'
            )

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        product = self.object

        # ✅ Related products (cached per category)
        cache_key = f'related_products_{product.category_id}_{product.id}'
        related_products = cache.get(cache_key)

        if related_products is None:
            related_products = Product.objects.filter(
                category=product.category,
                is_active=True
            ).exclude(pk=product.pk)\
             .select_related('category', 'brand')\
             .only('id', 'name', 'slug', 'price', 'average_rating', 'stock')[:4]

            cache.set(cache_key, list(related_products), 600)

        context['related_products'] = related_products

        # ✅ Reviews already prefetched, no extra query
        context['reviews'] = product.reviews.all()[:10]

        return context


def category_products_optimized(request, category_slug):
    """একটি category এর সব products - Fast!"""

    # ✅ Cache check
    cache_key = f'category_products_{category_slug}'
    cached_data = cache.get(cache_key)

    if cached_data:
        return render(request, 'shop/category_products.html', cached_data)

    # ✅ Optimized category fetch
    category = get_object_or_404(
        Category.objects.annotate(
            product_count=Count('products', filter=Q(products__is_active=True))
        ),
        slug=category_slug
    )

    # ✅ Optimized products fetch
    products = Product.objects.filter(
        category=category,
        is_active=True
    ).select_related('brand')\
     .only('id', 'name', 'slug', 'price', 'stock', 'average_rating', 'brand__name')\
     .order_by('-created_at')

    # ✅ Pagination
    paginator = Paginator(products, 20)
    page_number = request.GET.get('page', 1)
    page_obj = paginator.get_page(page_number)

    context = {
        'category': category,
        'page_obj': page_obj,
    }

    # ✅ Cache করুন (5 মিনিট)
    cache.set(cache_key, context, 300)

    return render(request, 'shop/category_products.html', context)


def user_orders_optimized(request):
    """User এর সব orders - Fast!"""
    if not request.user.is_authenticated:
        return redirect('login')

    # ✅ Optimized orders fetch
    orders = Order.objects.filter(user=request.user)\
        .prefetch_related(
            Prefetch(
                'items',
                queryset=OrderItem.objects.select_related('product')
                                         .only('id', 'quantity', 'price', 'product__name')
            )
        )\
        .annotate(
            item_count=Count('items')
        )\
        .order_by('-created_at')

    # ✅ Pagination
    paginator = Paginator(orders, 10)
    page_number = request.GET.get('page', 1)
    page_obj = paginator.get_page(page_number)

    context = {'page_obj': page_obj}
    return render(request, 'shop/user_orders.html', context)


# ✅ Cache decorator দিয়ে পুরো view cache করুন
@cache_page(60 * 5)  # 5 মিনিট cache
def featured_products(request):
    """Featured products - Cached view"""
    products = Product.objects.filter(
        is_active=True,
        is_featured=True
    ).select_related('category', 'brand')\
     .only('id', 'name', 'slug', 'price', 'average_rating')[:10]

    context = {'products': products}
    return render(request, 'shop/featured_products.html', context)


# ✅ Advanced: Multiple prefetch with filtering
def brand_detail_optimized(request, brand_slug):
    """Brand details with products - Fully optimized"""

    # ✅ Brand with annotated data
    brand = get_object_or_404(
        Brand.objects.annotate(
            product_count=Count('products', filter=Q(products__is_active=True)),
            avg_product_price=Avg('products__price', filter=Q(products__is_active=True))
        ),
        slug=brand_slug,
        is_active=True
    )

    # ✅ Products with custom prefetch
    products = Product.objects.filter(
        brand=brand,
        is_active=True
    ).select_related('category')\
     .prefetch_related(
         Prefetch(
             'reviews',
             queryset=Review.objects.filter(rating__gte=4)
                                   .select_related('user')[:3]
         )
     )\
     .annotate(
         order_count=Count('order_items')
     )\
     .order_by('-order_count', '-average_rating')

    # ✅ Pagination
    paginator = Paginator(products, 20)
    page_number = request.GET.get('page', 1)
    page_obj = paginator.get_page(page_number)

    context = {
        'brand': brand,
        'page_obj': page_obj,
    }

    return render(request, 'shop/brand_detail.html', context)


# ✅ Search view with optimization
def search_products(request):
    """Product search - Optimized"""
    query = request.GET.get('q', '').strip()

    if not query:
        return render(request, 'shop/search.html', {'products': []})

    # ✅ Cache search results
    cache_key = f'search_{query}'
    products = cache.get(cache_key)

    if products is None:
        # ✅ Optimized search query
        products = Product.objects.filter(
            Q(name__icontains=query) |
            Q(description__icontains=query) |
            Q(category__name__icontains=query) |
            Q(brand__name__icontains=query),
            is_active=True
        ).select_related('category', 'brand')\
         .only('id', 'name', 'slug', 'price', 'average_rating')\
         .distinct()[:50]  # Limit results

        # ✅ Cache করুন (2 মিনিট)
        cache.set(cache_key, list(products), 120)

    # ✅ Pagination
    paginator = Paginator(products, 20)
    page_number = request.GET.get('page', 1)
    page_obj = paginator.get_page(page_number)

    context = {
        'query': query,
        'page_obj': page_obj,
    }

    return render(request, 'shop/search.html', context)

📊 Views Optimization Techniques বিস্তারিত

1️⃣ select_related() – ForeignKey Optimization

# ❌ Without select_related (N+1 Problem)
products = Product.objects.all()
for product in products:
    print(product.category.name)  # Extra query!
    print(product.brand.name)     # Extra query!
# 100 products = 1 + 100 + 100 = 201 queries

# ✅ With select_related (JOIN)
products = Product.objects.select_related('category', 'brand')
for product in products:
    print(product.category.name)  # No extra query!
    print(product.brand.name)     # No extra query!
# 100 products = 1 query (with JOINs)

# SQL Query:
# SELECT product.*, category.*, brand.*
# FROM product
# INNER JOIN category ON product.category_id = category.id
# INNER JOIN brand ON product.brand_id = brand.id

কখন ব্যবহার করবেন:

  • ✅ ForeignKey (Many-to-One)
  • ✅ OneToOneField
  • ✅ যখন relation সবসময় access করা হয়

কখন ব্যবহার করবেন না:

  • ❌ ManyToManyField (prefetch_related ব্যবহার করুন)
  • ❌ Reverse ForeignKey (prefetch_related ব্যবহার করুন)

2️⃣ prefetch_related() – Reverse Relations & ManyToMany

# ❌ Without prefetch_related
orders = Order.objects.all()
for order in orders:
    for item in order.items.all():  # Extra query per order!
        print(item.product.name)
# 100 orders with 5 items each = 1 + 100 = 101 queries

# ✅ With prefetch_related
orders = Order.objects.prefetch_related('items')
for order in orders:
    for item in order.items.all():  # No extra query!
        print(item.product.name)
# 100 orders = 2 queries (1 main + 1 prefetch)

# SQL Queries:
# Query 1: SELECT * FROM order
# Query 2: SELECT * FROM orderitem WHERE order_id IN (1,2,3,...,100)

কখন ব্যবহার করবেন:

  • ✅ Reverse ForeignKey (One-to-Many)
  • ✅ ManyToManyField
  • ✅ GenericForeignKey

3️⃣ Prefetch() – Custom Prefetch with Filtering

# ✅ Advanced: Prefetch with custom queryset
products = Product.objects.prefetch_related(
    Prefetch(
        'reviews',
        queryset=Review.objects.filter(rating__gte=4)
                              .select_related('user')
                              .order_by('-created_at')[:5]
    )
)

# এখন product.reviews.all() শুধু 4+ rating এর 5টি review দেবে
# এবং user relation already loaded থাকবে!

4️⃣ only() – Load Specific Fields Only

# ❌ Without only() - সব fields load হয়
products = Product.objects.all()
# SELECT * FROM product (সব columns)

# ✅ With only() - শুধু প্রয়োজনীয় fields
products = Product.objects.only('id', 'name', 'price')
# SELECT id, name, price FROM product

# ⚠️ সতর্কতা: অন্য field access করলে extra query হবে
for product in products:
    print(product.name)  # OK, no query
    print(product.description)  # Extra query!

5️⃣ defer() – Exclude Specific Fields

# ✅ defer() - নির্দিষ্ট fields বাদ দিন
products = Product.objects.defer('description')
# SELECT id, name, price, ... (description ছাড়া)

# Large text fields বাদ দিতে useful
products = Product.objects.defer('description', 'long_text_field')

6️⃣ annotate() – Add Calculated Fields

# ❌ Without annotate - প্রতিবার query
for product in products:
    review_count = product.reviews.count()  # Extra query!

# ✅ With annotate - একবারে calculate
products = Product.objects.annotate(
    review_count=Count('reviews'),
    avg_rating=Avg('reviews__rating'),
    total_orders=Count('order_items')
)

for product in products:
    print(product.review_count)  # No extra query!
    print(product.avg_rating)    # No extra query!

7️⃣ F() – Database-level Operations

# ❌ Python-level operation (slow)
product = Product.objects.get(pk=1)
product.stock = product.stock - 1
product.save()
# 2 queries: SELECT + UPDATE

# ✅ Database-level operation (fast)
Product.objects.filter(pk=1).update(stock=F('stock') - 1)
# 1 query: UPDATE product SET stock = stock - 1

# ✅ Atomic operation, race condition safe!

8️⃣ Q() – Complex Queries

# ✅ OR conditions
products = Product.objects.filter(
    Q(name__icontains='phone') | Q(description__icontains='phone')
)

# ✅ Complex conditions
products = Product.objects.filter(
    Q(price__gte=100) & Q(price__lte=500) |
    Q(is_featured=True)
)

# ✅ NOT condition
products = Product.objects.filter(
    ~Q(category__name='Electronics')
)

9️⃣ Caching Strategies

# ✅ Strategy 1: Cache queryset results
cache_key = 'featured_products'
products = cache.get(cache_key)
if products is None:
    products = list(Product.objects.filter(is_featured=True)[:10])
    cache.set(cache_key, products, 300)  # 5 minutes

# ✅ Strategy 2: Cache view with decorator
@cache_page(60 * 5)  # 5 minutes
def my_view(request):
    ...

# ✅ Strategy 3: Cache template fragment
{% load cache %}
{% cache 300 sidebar %}
    ... expensive template code ...
{% endcache %}

# ✅ Strategy 4: Low-level cache
from django.core.cache import cache

def get_product_stats(product_id):
    cache_key = f'product_stats_{product_id}'
    stats = cache.get(cache_key)
    if stats is None:
        stats = calculate_stats(product_id)
        cache.set(cache_key, stats, 600)
    return stats

🔟 Pagination

# ✅ Always paginate large querysets
from django.core.paginator import Paginator

products = Product.objects.all()
paginator = Paginator(products, 20)  # 20 per page
page_obj = paginator.get_page(page_number)

# Template এ:
# {% for product in page_obj %}
#   ...
# {% endfor %}

🧪 Views Performance Test

test_views_performance.py:

import os
import django
import time

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'optimization_project.settings')
django.setup()

from django.test.utils import override_settings
from django.db import connection, reset_queries
from shop.models import Product

@override_settings(DEBUG=True)
def test_views_queries():
    print("=" * 60)
    print("🔴 Testing NORMAL View Queries")
    print("=" * 60)

    reset_queries()
    start = time.time()

    # Normal query (N+1 problem)
    products = Product.objects.all()[:100]
    for product in products:
        _ = product.category.name
        _ = product.brand.name

    end = time.time()

    print(f"Queries: {len(connection.queries)}")
    print(f"Time: {end - start:.3f}s")

    print("\n" + "=" * 60)
    print("✅ Testing OPTIMIZED View Queries")
    print("=" * 60)

    reset_queries()
    start = time.time()

    # Optimized query
    products = Product.objects.select_related('category', 'brand')[:100]
    for product in products:
        _ = product.category.name
        _ = product.brand.name

    end = time.time()

    print(f"Queries: {len(connection.queries)}")
    print(f"Time: {end - start:.3f}s")

if __name__ == '__main__':
    test_views_queries()

এটি Part 3 শেষ। পরবর্তী Part এ API Optimization ও Django Bolt দেখব।

পরবর্তী Part এ থাকবে:

  • Normal API vs Optimized API
  • Django Bolt integration
  • API caching strategies
  • Serializer optimization

চালিয়ে যাব?

How can we help?