Part 2: Admin Panel Optimization
Admin panel এ সবচেয়ে বেশি performance issue হয় কারণ:
- List page এ অনেক data দেখায়
- ForeignKey fields এ N+1 query problem
- ManyToMany relations slow
- Filtering ও searching optimize না থাকলে
🔴 Normal Admin (Slow)
shop/admin.py – Normal Version:
from django.contrib import admin
from .models import Category, Brand, Product, Review, Order, OrderItem
# ❌ সবচেয়ে সাধারণ (এবং ধীর) admin
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
pass # কোন optimization নেই
@admin.register(Brand)
class BrandAdmin(admin.ModelAdmin):
list_display = ['name', 'country']
# ❌ সমস্যা: search optimize নেই
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ['name', 'get_category', 'get_brand', 'price', 'stock']
list_filter = ['is_active', 'category', 'brand']
search_fields = ['name', 'description']
# ❌ সমস্যা: প্রতিটি product এর জন্য আলাদা query
def get_category(self, obj):
return obj.category.name # Extra query!
get_category.short_description = 'Category'
def get_brand(self, obj):
return obj.brand.name # Extra query!
get_brand.short_description = 'Brand'
# ❌ সমস্যা: queryset optimize নেই
# 100 products = 1 (main) + 100 (category) + 100 (brand) = 201 queries!
@admin.register(Review)
class ReviewAdmin(admin.ModelAdmin):
list_display = ['get_product', 'get_user', 'rating', 'created_at']
def get_product(self, obj):
return obj.product.name # Extra query!
def get_user(self, obj):
return obj.user.username # Extra query!
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'get_user', 'total_amount', 'status', 'created_at']
def get_user(self, obj):
return obj.user.username # Extra query!
@admin.register(OrderItem)
class OrderItemAdmin(admin.ModelAdmin):
list_display = ['get_order', 'get_product', 'quantity', 'price']
def get_order(self, obj):
return f"Order #{obj.order.id}" # Extra query!
def get_product(self, obj):
return obj.product.name # Extra query!❌ Normal Admin এর সমস্যা:
Performance Test করুন:
# Django shell খুলুন
python manage.py shellfrom django.contrib.admin.sites import site
from shop.models import Product
from django.test.utils import override_settings
from django.db import connection
from django.db import reset_queries
# Query count check
from django.conf import settings
settings.DEBUG = True
# Admin changelist query
from shop.admin import ProductAdmin
admin_instance = ProductAdmin(Product, site)
# Simulate admin list view
reset_queries()
queryset = admin_instance.get_queryset(request=None)
list(queryset[:100]) # First 100 products
print(f"Total Queries: {len(connection.queries)}")
# Output: 201 queries! (1 main + 100 category + 100 brand)✅ Optimized Admin (Fast)
shop/admin.py – Optimized Version:
from django.contrib import admin
from django.db.models import Count, Avg, Sum, Q
from django.utils.html import format_html
from django.urls import reverse
from django.utils.safestring import mark_safe
from .models import Category, Brand, Product, Review, Order, OrderItem
# ✅ Category Admin - Optimized
@admin.register(Category)
class CategoryAdminOptimized(admin.ModelAdmin):
list_display = [
'name',
'slug',
'product_count', # ✅ Annotated field
'created_at'
]
list_filter = ['created_at']
search_fields = ['name', 'slug'] # ✅ Indexed fields এ search
prepopulated_fields = {'slug': ('name',)} # ✅ Auto slug
date_hierarchy = 'created_at' # ✅ Date navigation
# ✅ Queryset optimization
def get_queryset(self, request):
queryset = super().get_queryset(request)
# Annotate করে product count add করুন
queryset = queryset.annotate(
_product_count=Count('products', filter=Q(products__is_active=True))
)
return queryset
# ✅ Annotated field display
def product_count(self, obj):
return obj._product_count
product_count.short_description = 'Active Products'
product_count.admin_order_field = '_product_count' # ✅ Sortable
# ✅ Brand Admin - Optimized
@admin.register(Brand)
class BrandAdminOptimized(admin.ModelAdmin):
list_display = [
'name',
'slug',
'country',
'is_active',
'product_count',
'logo_preview' # ✅ Image preview
]
list_filter = ['is_active', 'country', 'created_at']
search_fields = ['name', 'slug', 'country']
prepopulated_fields = {'slug': ('name',)}
list_editable = ['is_active'] # ✅ Quick edit
def get_queryset(self, request):
queryset = super().get_queryset(request)
queryset = queryset.annotate(
_product_count=Count('products')
)
return queryset
def product_count(self, obj):
return obj._product_count
product_count.short_description = 'Products'
product_count.admin_order_field = '_product_count'
# ✅ Image preview
def logo_preview(self, obj):
if obj.logo:
return format_html(
'<img src="{}" width="50" height="50" style="object-fit:cover;border-radius:5px;" />',
obj.logo.url
)
return '-'
logo_preview.short_description = 'Logo'
# ✅ Product Admin - Fully Optimized
@admin.register(Product)
class ProductAdminOptimized(admin.ModelAdmin):
list_display = [
'name',
'category_link', # ✅ Clickable link
'brand_link', # ✅ Clickable link
'price_display', # ✅ Formatted price
'stock_status', # ✅ Visual stock indicator
'rating_display', # ✅ Star rating
'review_count',
'is_active',
'is_featured',
]
list_filter = [
'is_active',
'is_featured',
'category',
'brand',
'created_at',
('price', admin.NumericRangeFilter), # ✅ Price range filter
]
search_fields = [
'name',
'slug',
'description',
'category__name', # ✅ Related field search
'brand__name',
]
list_editable = ['is_active', 'is_featured'] # ✅ Quick edit
prepopulated_fields = {'slug': ('name',)}
date_hierarchy = 'created_at'
# ✅ Fieldsets for better organization
fieldsets = (
('Basic Information', {
'fields': ('name', 'slug', 'description')
}),
('Pricing & Stock', {
'fields': ('price', 'stock'),
'classes': ('collapse',) # ✅ Collapsible
}),
('Relations', {
'fields': ('category', 'brand')
}),
('Status', {
'fields': ('is_active', 'is_featured')
}),
('Statistics (Read-only)', {
'fields': ('review_count', 'average_rating'),
'classes': ('collapse',),
}),
)
readonly_fields = ['review_count', 'average_rating', 'created_at', 'updated_at']
# ✅✅✅ সবচেয়ে গুরুত্বপূর্ণ: Queryset Optimization
def get_queryset(self, request):
queryset = super().get_queryset(request)
# ✅ select_related: ForeignKey relations (1-to-1, Many-to-1)
queryset = queryset.select_related(
'category', # Product -> Category
'brand', # Product -> Brand
)
# ✅ prefetch_related: Reverse ForeignKey & ManyToMany
# (এখানে দরকার নেই কারণ list view এ reviews দেখাচ্ছি না)
# ✅ only(): শুধু প্রয়োজনীয় fields load করুন
# queryset = queryset.only(
# 'id', 'name', 'price', 'stock', 'is_active',
# 'category__name', 'brand__name'
# )
return queryset
# ✅ Custom display methods (no extra queries!)
def category_link(self, obj):
"""Category name with admin link"""
url = reverse('admin:shop_category_change', args=[obj.category.id])
return format_html('<a href="{}">{}</a>', url, obj.category.name)
category_link.short_description = 'Category'
category_link.admin_order_field = 'category__name' # ✅ Sortable
def brand_link(self, obj):
"""Brand name with admin link"""
url = reverse('admin:shop_brand_change', args=[obj.brand.id])
return format_html('<a href="{}">{}</a>', url, obj.brand.name)
brand_link.short_description = 'Brand'
brand_link.admin_order_field = 'brand__name'
def price_display(self, obj):
"""Formatted price with currency"""
return format_html('<strong>৳{:,.2f}</strong>', obj.price)
price_display.short_description = 'Price'
price_display.admin_order_field = 'price'
def stock_status(self, obj):
"""Visual stock indicator"""
if obj.stock > 50:
color = 'green'
status = f'✓ {obj.stock}'
elif obj.stock > 0:
color = 'orange'
status = f'⚠ {obj.stock}'
else:
color = 'red'
status = '✗ Out of Stock'
return format_html(
'<span style="color:{}; font-weight:bold;">{}</span>',
color, status
)
stock_status.short_description = 'Stock'
stock_status.admin_order_field = 'stock'
def rating_display(self, obj):
"""Star rating display"""
if obj.average_rating > 0:
stars = '⭐' * int(obj.average_rating)
return format_html(
'{} <small>({:.1f})</small>',
stars, obj.average_rating
)
return '-'
rating_display.short_description = 'Rating'
rating_display.admin_order_field = 'average_rating'
# ✅ Custom actions
actions = ['make_active', 'make_inactive', 'make_featured', 'update_ratings']
def make_active(self, request, queryset):
updated = queryset.update(is_active=True)
self.message_user(request, f'{updated} products marked as active.')
make_active.short_description = 'Mark selected as Active'
def make_inactive(self, request, queryset):
updated = queryset.update(is_active=False)
self.message_user(request, f'{updated} products marked as inactive.')
make_inactive.short_description = 'Mark selected as Inactive'
def make_featured(self, request, queryset):
updated = queryset.update(is_featured=True)
self.message_user(request, f'{updated} products marked as featured.')
make_featured.short_description = 'Mark selected as Featured'
def update_ratings(self, request, queryset):
"""Bulk update ratings for selected products"""
count = 0
for product in queryset:
product.update_rating_cache()
count += 1
self.message_user(request, f'Updated ratings for {count} products.')
update_ratings.short_description = 'Update ratings cache'
# ✅ Review Admin - Optimized
@admin.register(Review)
class ReviewAdminOptimized(admin.ModelAdmin):
list_display = [
'id',
'product_link',
'user_link',
'rating_stars',
'is_verified',
'created_at',
]
list_filter = [
'rating',
'is_verified',
'created_at',
]
search_fields = [
'product__name',
'user__username',
'user__email',
'comment',
]
list_editable = ['is_verified']
date_hierarchy = 'created_at'
# ✅ Queryset optimization
def get_queryset(self, request):
queryset = super().get_queryset(request)
# ✅ select_related for ForeignKeys
queryset = queryset.select_related('product', 'user')
return queryset
def product_link(self, obj):
url = reverse('admin:shop_product_change', args=[obj.product.id])
return format_html('<a href="{}">{}</a>', url, obj.product.name)
product_link.short_description = 'Product'
product_link.admin_order_field = 'product__name'
def user_link(self, obj):
url = reverse('admin:auth_user_change', args=[obj.user.id])
return format_html('<a href="{}">{}</a>', url, obj.user.username)
user_link.short_description = 'User'
user_link.admin_order_field = 'user__username'
def rating_stars(self, obj):
return '⭐' * obj.rating
rating_stars.short_description = 'Rating'
rating_stars.admin_order_field = 'rating'
# ✅ OrderItem Inline - Optimized
class OrderItemInlineOptimized(admin.TabularInline):
model = OrderItem
extra = 0 # ✅ No extra empty forms
# ✅ Optimize ForeignKey queries
autocomplete_fields = ['product'] # ✅ Ajax search instead of dropdown
readonly_fields = ['subtotal_display']
def subtotal_display(self, obj):
if obj.id:
return format_html('<strong>৳{:,.2f}</strong>', obj.subtotal)
return '-'
subtotal_display.short_description = 'Subtotal'
def get_queryset(self, request):
queryset = super().get_queryset(request)
# ✅ Prefetch product details
queryset = queryset.select_related('product')
return queryset
# ✅ Order Admin - Optimized
@admin.register(Order)
class OrderAdminOptimized(admin.ModelAdmin):
list_display = [
'id',
'user_link',
'total_amount_display',
'status_badge',
'item_count',
'created_at',
]
list_filter = [
'status',
'created_at',
('total_amount', admin.NumericRangeFilter),
]
search_fields = [
'id',
'user__username',
'user__email',
]
list_editable = ['status']
date_hierarchy = 'created_at'
# ✅ Inline for order items
inlines = [OrderItemInlineOptimized]
# ✅ Queryset optimization
def get_queryset(self, request):
queryset = super().get_queryset(request)
# ✅ select_related for user
queryset = queryset.select_related('user')
# ✅ Annotate item count
queryset = queryset.annotate(
_item_count=Count('items')
)
# ✅ prefetch_related for items (for detail view)
queryset = queryset.prefetch_related(
'items',
'items__product'
)
return queryset
def user_link(self, obj):
url = reverse('admin:auth_user_change', args=[obj.user.id])
return format_html('<a href="{}">{}</a>', url, obj.user.username)
user_link.short_description = 'User'
user_link.admin_order_field = 'user__username'
def total_amount_display(self, obj):
return format_html('<strong>৳{:,.2f}</strong>', obj.total_amount)
total_amount_display.short_description = 'Total'
total_amount_display.admin_order_field = 'total_amount'
def status_badge(self, obj):
colors = {
'pending': '#ffc107',
'processing': '#17a2b8',
'shipped': '#007bff',
'delivered': '#28a745',
'cancelled': '#dc3545',
}
color = colors.get(obj.status, '#6c757d')
return format_html(
'<span style="background:{}; color:white; padding:3px 10px; border-radius:3px; font-weight:bold;">{}</span>',
color, obj.get_status_display()
)
status_badge.short_description = 'Status'
status_badge.admin_order_field = 'status'
def item_count(self, obj):
return obj._item_count
item_count.short_description = 'Items'
item_count.admin_order_field = '_item_count'
# ✅ Custom actions
actions = ['mark_as_processing', 'mark_as_shipped', 'mark_as_delivered']
def mark_as_processing(self, request, queryset):
updated = queryset.update(status='processing')
self.message_user(request, f'{updated} orders marked as processing.')
mark_as_processing.short_description = 'Mark as Processing'
def mark_as_shipped(self, request, queryset):
updated = queryset.update(status='shipped')
self.message_user(request, f'{updated} orders marked as shipped.')
mark_as_shipped.short_description = 'Mark as Shipped'
def mark_as_delivered(self, request, queryset):
updated = queryset.update(status='delivered')
self.message_user(request, f'{updated} orders marked as delivered.')
mark_as_delivered.short_description = 'Mark as Delivered'
# ✅ OrderItem Admin - Optimized (if needed separately)
@admin.register(OrderItem)
class OrderItemAdminOptimized(admin.ModelAdmin):
list_display = [
'id',
'order_link',
'product_link',
'quantity',
'price_display',
'subtotal_display',
]
list_filter = ['order__status', 'order__created_at']
search_fields = [
'order__id',
'product__name',
'order__user__username',
]
# ✅ Ajax autocomplete for ForeignKeys
autocomplete_fields = ['order', 'product']
def get_queryset(self, request):
queryset = super().get_queryset(request)
# ✅ Optimize all ForeignKey relations
queryset = queryset.select_related('order', 'order__user', 'product')
return queryset
def order_link(self, obj):
url = reverse('admin:shop_order_change', args=[obj.order.id])
return format_html('<a href="{}">Order #{}</a>', url, obj.order.id)
order_link.short_description = 'Order'
order_link.admin_order_field = 'order__id'
def product_link(self, obj):
url = reverse('admin:shop_product_change', args=[obj.product.id])
return format_html('<a href="{}">{}</a>', url, obj.product.name)
product_link.short_description = 'Product'
product_link.admin_order_field = 'product__name'
def price_display(self, obj):
return format_html('৳{:,.2f}', obj.price)
price_display.short_description = 'Price'
price_display.admin_order_field = 'price'
def subtotal_display(self, obj):
return format_html('<strong>৳{:,.2f}</strong>', obj.subtotal)
subtotal_display.short_description = 'Subtotal'
# ✅ Enable autocomplete for Product (required for autocomplete_fields)
ProductAdminOptimized.search_fields = ['name', 'slug']📊 Admin 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 per product!
# 100 products = 1 + 100 = 101 queries
# ✅ With select_related
products = Product.objects.select_related('category')
for product in products:
print(product.category.name) # No extra query!
# 100 products = 1 query (JOIN করে)কখন ব্যবহার করবেন:
- ForeignKey (Many-to-One)
- OneToOneField
2️⃣ prefetch_related() – Reverse ForeignKey & ManyToMany
# ❌ Without prefetch_related
orders = Order.objects.all()
for order in orders:
print(order.items.count()) # Extra query per order!
# 100 orders = 1 + 100 = 101 queries
# ✅ With prefetch_related
orders = Order.objects.prefetch_related('items')
for order in orders:
print(order.items.count()) # No extra query!
# 100 orders = 2 queries (1 main + 1 prefetch)কখন ব্যবহার করবেন:
- Reverse ForeignKey (One-to-Many)
- ManyToManyField
3️⃣ annotate() – Aggregate Data
# ❌ Without annotate
categories = Category.objects.all()
for cat in categories:
count = cat.products.count() # Extra query!
# ✅ With annotate
categories = Category.objects.annotate(
product_count=Count('products')
)
for cat in categories:
count = cat.product_count # No extra query!4️⃣ only() vs defer()
# ✅ only(): শুধু নির্দিষ্ট fields load করুন
products = Product.objects.only('id', 'name', 'price')
# SELECT id, name, price FROM product
# ✅ defer(): নির্দিষ্ট fields বাদ দিন
products = Product.objects.defer('description')
# SELECT id, name, price, ... (description ছাড়া সব)5️⃣ autocomplete_fields – Ajax Search
# ❌ Without autocomplete (Dropdown with all products)
# 10,000 products = page load slow!
# ✅ With autocomplete (Ajax search)
autocomplete_fields = ['product']
# শুধু search করলে load হয়, fast!🧪 Admin Performance Test
test_admin_performance.py তৈরি করুন:
import os
import django
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 django.contrib.admin.sites import site
from shop.models import Product
from shop.admin import ProductAdminOptimized
from django.test import RequestFactory
# Enable query logging
@override_settings(DEBUG=True)
def test_admin_queries():
# Create fake request
factory = RequestFactory()
request = factory.get('/admin/shop/product/')
request.user = None
# Test Normal Admin (without optimization)
print("=" * 60)
print("🔴 Testing NORMAL Admin (No Optimization)")
print("=" * 60)
reset_queries()
# Simulate normal queryset
products = Product.objects.all()[:100]
for product in products:
_ = product.category.name # Trigger query
_ = product.brand.name # Trigger query
print(f"Total Queries: {len(connection.queries)}")
print(f"Time: {sum(float(q['time']) for q in connection.queries):.3f}s")
# Test Optimized Admin
print("\n" + "=" * 60)
print("✅ Testing OPTIMIZED Admin")
print("=" * 60)
reset_queries()
# Simulate optimized queryset
admin_instance = ProductAdminOptimized(Product, site)
products = admin_instance.get_queryset(request)[:100]
for product in products:
_ = product.category.name # No extra query!
_ = product.brand.name # No extra query!
print(f"Total Queries: {len(connection.queries)}")
print(f"Time: {sum(float(q['time']) for q in connection.queries):.3f}s")
print("\n" + "=" * 60)
print("📊 RESULT")
print("=" * 60)
if __name__ == '__main__':
test_admin_queries()চালান:
python test_admin_performance.pyExpected Output:
============================================================
🔴 Testing NORMAL Admin (No Optimization)
============================================================
Total Queries: 201
Time: 0.850s
============================================================
✅ Testing OPTIMIZED Admin
============================================================
Total Queries: 1
Time: 0.008s
============================================================
📊 RESULT
============================================================
Optimized admin is 106x faster! 🚀📝 Admin Optimization Checklist
✅ করণীয়:
- select_related() – সব ForeignKey relations এ
- prefetch_related() – Reverse relations এ
- annotate() – Count/Avg/Sum calculations এ
- list_select_related – Admin list view এ
- autocomplete_fields – Large ForeignKey dropdowns এ
- search_fields – Indexed fields এ search
- list_filter – Indexed fields এ filter
- readonly_fields – Calculated fields এ
- list_editable – Quick edit এর জন্য
- actions – Bulk operations এর জন্য
❌ এড়িয়ে চলুন:
- List display তে complex calculations
- Non-indexed fields এ search/filter
- Too many list_display fields
- Large text fields list display তে
- Nested loops in display methods