# คำถามสัมภาษณ์งาน Django: ORM, Middleware และ DRF เจาะลึก > คำถามสัมภาษณ์งาน Django ครอบคลุมการปรับแต่ง ORM ด้วย select_related และ prefetch_related, สถาปัตยกรรม middleware, ประสิทธิภาพ serializer ของ Django REST Framework, permissions และรูปแบบ pagination - Published: 2026-04-03 - Updated: 2026-04-03 - Author: SharpSkill - Tags: django, python, interview, orm, middleware, drf, rest-api - Reading time: 9 min --- คำถามสัมภาษณ์งาน Django ในปี 2026 มุ่งเน้นไปที่สามเสาหลักที่แยกผู้สมัครระดับ senior ออกจากคนอื่น: ความชำนาญด้าน ORM, สถาปัตยกรรม middleware และ design pattern ของ Django REST Framework (DRF) บทความนี้วิเคราะห์คำถามที่ผู้สัมภาษณ์มักถามบ่อย พร้อมตัวอย่างโค้ดที่พร้อมใช้งานจริงโดยใช้ Django 5.2 LTS และ DRF 3.17 > **สิ่งที่ผู้สัมภาษณ์ทดสอบจริง** > > การสัมภาษณ์งาน Django ไม่ค่อยถามเรื่อง CRUD พื้นฐาน จุดเน้นได้เปลี่ยนไปที่การปรับแต่ง QuerySet (select_related vs prefetch_related) การออกแบบ middleware แบบกำหนดเอง และประสิทธิภาพ serializer ของ DRF ผู้สมัครที่สามารถอธิบายปัญหา N+1 query และเขียน viewset ที่มีประสิทธิภาพได้อย่างสม่ำเสมอ จะโดดเด่นกว่าผู้ที่รู้จักเพียง generic views ## คำถาม ORM Django: การปรับแต่ง QuerySet คำถาม ORM Django ที่พบบ่อยที่สุดมุ่งเน้นไปที่ปัญหา N+1 เมื่อกำหนด model ที่มีความสัมพันธ์แบบ foreign key ผู้สมัครต้องแสดงให้เห็นว่าควรใช้ `select_related` หรือ `prefetch_related` เมื่อใด ```python # models.py from django.db import models class Company(models.Model): name = models.CharField(max_length=200) founded_year = models.IntegerField() class Developer(models.Model): name = models.CharField(max_length=200) company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="developers") skills = models.ManyToManyField("Skill", related_name="developers") class Skill(models.Model): name = models.CharField(max_length=100) category = models.CharField(max_length=50) ``` ความแตกต่างระหว่างสองวิธีนี้ขึ้นอยู่กับประเภทของความสัมพันธ์ที่กำลังเข้าถึง ```python # queries.py — Correct approach for ForeignKey (single object) developers = Developer.objects.select_related("company").all() # Generates ONE SQL query with JOIN # SELECT developer.*, company.* FROM developer INNER JOIN company ... # Correct approach for ManyToMany (multiple objects) developers = Developer.objects.prefetch_related("skills").all() # Generates TWO SQL queries: # 1. SELECT * FROM developer # 2. SELECT * FROM skill INNER JOIN developer_skills WHERE developer_id IN (...) ``` `select_related` ทำ SQL JOIN และใช้งานได้กับ ForeignKey และ OneToOneField ส่วน `prefetch_related` รัน query แยกต่างหากและใช้งานได้กับ ManyToManyField และความสัมพันธ์ reverse ForeignKey การใช้ผิดวิธีจะทำให้เกิด JOIN ที่ไม่จำเป็นบนชุดข้อมูลขนาดใหญ่ หรือเกิดปัญหา N+1 ที่ร้ายแรง ## ORM ขั้นสูง: Custom Manager และ QuerySet Chaining ผู้สัมภาษณ์มักขอให้ผู้สมัครเขียน custom manager ที่ห่อหุ้ม business logic เป้าหมายคือการตรวจสอบว่าผู้สมัครเข้าใจ manager pattern ของ Django และสามารถเขียน query interface ที่นำกลับมาใช้ได้ ```python # managers.py from django.db import models from django.utils import timezone class ActiveDeveloperQuerySet(models.QuerySet): def active(self): """Filter developers who logged in within the last 30 days.""" cutoff = timezone.now() - timezone.timedelta(days=30) return self.filter(last_login__gte=cutoff) def senior(self): """Filter developers with 5+ years of experience.""" return self.filter(years_experience__gte=5) def by_skill(self, skill_name): """Filter developers by a specific skill.""" return self.filter(skills__name=skill_name) class ActiveDeveloperManager(models.Manager): def get_queryset(self): return ActiveDeveloperQuerySet(self.model, using=self._db) def active(self): return self.get_queryset().active() ``` คำถามต่อเนื่องที่สำคัญ: "ทำไมใช้ custom QuerySet แทนที่จะใช้แค่ Manager?" คำตอบคือ chaining method ของ custom QuerySet สามารถ chain ต่อกันได้ แต่ method ของ Manager ไม่สามารถ chain หลังจากการเรียกครั้งแรก ```python # usage.py — QuerySet chaining in action # This works because each method returns a QuerySet senior_python_devs = ( Developer.active_objects # custom manager .active() # ActiveDeveloperQuerySet method .senior() # chains another QuerySet method .by_skill("Python") # chains a third method .select_related("company") # standard QuerySet method still works ) ``` > **Django 5.2 LTS: Composite Primary Keys** > > Django 5.2 เปิดตัว CompositePrimaryKey ซึ่งเป็นฟีเจอร์ที่รอคอยมานานสำหรับ model ที่ต้องการ primary key หลายคอลัมน์ คำถามสัมภาษณ์เกี่ยวกับฟีเจอร์นี้เพิ่มมากขึ้นเรื่อย โดยเฉพาะสำหรับผู้สมัครที่ทำงานกับฐานข้อมูล legacy หรือ schema ของ data warehouse ## คำถาม Middleware Django: Pipeline Request-Response คำถามเกี่ยวกับ middleware ทดสอบความเข้าใจของผู้สมัครเกี่ยวกับวงจรชีวิต request-response ของ Django คำถามมาตรฐานคือ: "อธิบายลำดับการทำงานของ middleware เมื่อประมวลผล request และ response" คำตอบเป็นไปตามรูปแบบที่แน่นอน ในขั้นตอน request คลาส middleware จะทำงานจากบนลงล่างตามลำดับที่กำหนดใน `MIDDLEWARE` ในขั้นตอน response จะทำงานจากล่างขึ้นบน สถาปัตยกรรมแบบชั้นหัวหอมนี้หมายความว่า middleware ตัวแรกในรายการจะห่อหุ้มทุกอย่าง ```python # middleware.py import time import logging from django.http import JsonResponse logger = logging.getLogger(__name__) class RequestTimingMiddleware: """Logs the time taken to process each request.""" def __init__(self, get_response): self.get_response = get_response # next middleware or view def __call__(self, request): start_time = time.monotonic() response = self.get_response(request) # passes to next layer duration_ms = (time.monotonic() - start_time) * 1000 logger.info( "method=%s path=%s status=%d duration=%.2fms", request.method, request.path, response.status_code, duration_ms, ) return response ``` คำถามต่อเนื่องที่พบบ่อย: "ทำอย่างไรให้ตัดสายการทำงานของ middleware chain?" การคืนค่า HttpResponse จาก `__call__` ก่อนเรียก `self.get_response(request)` จะหยุดทั้งสายทันที middleware ที่เหลือและ view จะไม่ถูกเรียกใช้งาน ```python # middleware.py — Rate limiting with short-circuit from django.core.cache import cache class RateLimitMiddleware: """Blocks requests exceeding 100 per minute per IP.""" def __init__(self, get_response): self.get_response = get_response def __call__(self, request): ip = request.META.get("REMOTE_ADDR") cache_key = f"rate_limit:{ip}" request_count = cache.get(cache_key, 0) if request_count >= 100: return JsonResponse( # short-circuits — view never executes {"error": "Rate limit exceeded. Try again in 60 seconds."}, status=429, ) cache.set(cache_key, request_count + 1, timeout=60) return self.get_response(request) ``` ## Hook ของ Middleware: process_view, process_exception และ process_template_response นอกจาก `__call__` แล้ว middleware ของ Django ยังรองรับ hook method เสริมอีกสามตัว ผู้สัมภาษณ์ใช้คำถามเหล่านี้เพื่อวัดความลึกของความรู้ ```python # middleware.py — Full middleware with all hooks class AuditMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): return self.get_response(request) def process_view(self, request, view_func, view_args, view_kwargs): """Called after URL resolution, before the view executes.""" logger.info("Calling view: %s", view_func.__name__) return None # returning None continues normal processing def process_exception(self, request, exception): """Called only if the view raises an exception.""" logger.error("View exception: %s", exception, exc_info=True) return None # returning None lets Django's default handling proceed def process_template_response(self, request, response): """Called if the response has a render() method (TemplateResponse).""" response.context_data["audit_timestamp"] = time.time() return response # must return a response with render() ``` `process_view` ถูกเรียกหลังจากการแก้ URL แต่ก่อนที่ view จะทำงาน การคืนค่า `None` จะทำงานต่อไป การคืนค่า `HttpResponse` จะตัดสายการทำงาน `process_exception` จะถูกเรียกเฉพาะเมื่อเกิด exception ที่ยังไม่ได้จัดการ `process_template_response` จะถูกเรียกเฉพาะกับออบเจกต์ `TemplateResponse` ไม่ใช่ `HttpResponse` ทั่วไป ## คำถาม DRF: ประสิทธิภาพ Serializer คำถาม serializer ของ [Django REST Framework](https://www.django-rest-framework.org/) มุ่งเน้นไปที่ nested serialization และผลกระทบด้านประสิทธิภาพของแต่ละวิธี คำถามที่พบบ่อยที่สุด: "จะจัดการความสัมพันธ์แบบซ้อนอย่างไรโดยไม่ทำให้เกิด N+1 query?" ```python # serializers.py from rest_framework import serializers from .models import Developer, Company, Skill class SkillSerializer(serializers.ModelSerializer): class Meta: model = Skill fields = ["name", "category"] class CompanySerializer(serializers.ModelSerializer): class Meta: model = Company fields = ["name", "founded_year"] class DeveloperSerializer(serializers.ModelSerializer): company = CompanySerializer(read_only=True) # nested FK skills = SkillSerializer(many=True, read_only=True) # nested M2M class Meta: model = Developer fields = ["id", "name", "company", "skills"] ``` Serializer เพียงอย่างเดียวไม่สามารถแก้ปัญหาประสิทธิภาพได้ ViewSet ต้องปรับแต่ง queryset ด้วย ```python # views.py from rest_framework import viewsets from .models import Developer from .serializers import DeveloperSerializer class DeveloperViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = DeveloperSerializer def get_queryset(self): return ( Developer.objects .select_related("company") # JOIN for FK .prefetch_related("skills") # separate query for M2M .order_by("-id") ) ``` หากไม่มี `select_related` และ `prefetch_related` ใน ViewSet developer แต่ละคนที่ถูก serialize จะสร้าง query แยกต่างหากสำหรับ company และ skills สำหรับรายการ 50 developer นั่นหมายถึง 1 + 50 + 50 = 101 query แทนที่จะเป็น 3 ## Custom Permissions และ Authentication ใน DRF คำถาม DRF ที่พบบ่อย: "เขียน custom permission ที่จำกัดการเข้าถึงตาม role ของผู้ใช้และเจ้าของออบเจกต์" ```python # permissions.py from rest_framework.permissions import BasePermission class IsOwnerOrAdmin(BasePermission): """ Object-level permission: - Admin users can access any object - Regular users can only access objects they own """ message = "Access restricted to the object owner or admin users." def has_object_permission(self, request, view, obj): if request.user.is_staff: return True # Assumes the model has an 'owner' field pointing to User return obj.owner == request.user ``` ```python # views.py — Applying custom permissions from rest_framework import viewsets, permissions from .permissions import IsOwnerOrAdmin class ProjectViewSet(viewsets.ModelViewSet): permission_classes = [permissions.IsAuthenticated, IsOwnerOrAdmin] def get_queryset(self): # Non-admin users only see their own projects if self.request.user.is_staff: return Project.objects.all() return Project.objects.filter(owner=self.request.user) def perform_create(self, serializer): serializer.save(owner=self.request.user) # auto-assign owner ``` จุดสำคัญที่ผู้สัมภาษณ์มองหา: `has_permission` ทำงานทุก request (ระดับรายการ) ในขณะที่ `has_object_permission` ทำงานเฉพาะเมื่อ `get_object()` ถูกเรียก (ระดับรายละเอียด) การลืม override `get_queryset` สำหรับ list view จะสร้างช่องโหว่ด้านความปลอดภัยที่ผู้ใช้สามารถเห็นออบเจกต์ที่ไม่ใช่ของตนเอง > **ข้อผิดพลาดด้านความปลอดภัยที่พบบ่อยใน DRF** > > การพึ่งพา has_object_permission เพียงอย่างเดียวโดยไม่กรอง queryset จะทำให้ endpoint รายการไม่ได้รับการป้องกัน ควรใช้ permission ระดับออบเจกต์ร่วมกับ get_queryset ที่กรองแล้วเสมอเพื่อควบคุมการเข้าถึงทั้งใน view รายการและรายละเอียด ## รูปแบบ Throttling และ Pagination ใน DRF Throttling และ pagination เป็นคำถามต่อเนื่องที่มาตรฐาน ผู้สัมภาษณ์ต้องการเห็นว่าผู้สมัครเข้าใจการตั้งค่า API สำหรับงาน production ```python # settings.py — Production DRF configuration REST_FRAMEWORK = { "DEFAULT_THROTTLE_CLASSES": [ "rest_framework.throttling.AnonRateThrottle", "rest_framework.throttling.UserRateThrottle", ], "DEFAULT_THROTTLE_RATES": { "anon": "20/minute", # unauthenticated users "user": "200/minute", # authenticated users }, "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.CursorPagination", "PAGE_SIZE": 25, } ``` ```python # pagination.py — Custom cursor pagination for consistent ordering from rest_framework.pagination import CursorPagination class CreatedAtCursorPagination(CursorPagination): page_size = 25 ordering = "-created_at" # must be a unique, sequential field cursor_query_param = "cursor" # ?cursor=abc123 ``` Cursor-based pagination มีประสิทธิภาพดีกว่า offset pagination บนชุดข้อมูลขนาดใหญ่เพราะไม่ต้องนับจำนวนแถวทั้งหมด ข้อแลกเปลี่ยนคือ client ไม่สามารถกระโดดไปยังหน้าใดก็ได้ นี่คือคำตอบที่คาดหวังเมื่อผู้สัมภาษณ์ถามว่า "ทำไมถึงเลือก cursor pagination แทน page number pagination?" ## สรุป ประเด็นสำคัญสำหรับการเตรียมสัมภาษณ์งาน Django: - **การปรับแต่ง ORM**: เลือกวิธี query ให้ตรงกับประเภทความสัมพันธ์เสมอ `select_related` สำหรับ ForeignKey/OneToOne และ `prefetch_related` สำหรับ ManyToMany และ reverse FK Custom QuerySet ช่วยให้สร้าง business logic ที่ chain และนำกลับมาใช้ได้ - **สถาปัตยกรรม middleware**: Request ไหลจากบนลงล่าง response จากล่างขึ้นบน การตัดสายโดยการคืนค่า response ก่อน `get_response()` เป็น pattern พื้นฐานสำหรับ rate limiting การตรวจสอบสิทธิ์ และการ validate request - **ประสิทธิภาพ serializer DRF**: Nested serializer ต้องการการปรับแต่ง queryset อย่างชัดเจนใน ViewSet หากไม่มี `select_related`/`prefetch_related` การ serialize จะทำให้เกิด N+1 query ในระดับที่ใหญ่ขึ้น - **Permissions DRF**: ใช้ `has_object_permission` ร่วมกับ `get_queryset` ที่กรองแล้วเพื่อควบคุมการเข้าถึงทั้ง endpoint รายการและรายละเอียด Permission ระดับออบเจกต์เพียงอย่างเดียวไม่ป้องกัน list view - **Throttling และ pagination**: Cursor pagination รองรับการขยายตัวได้ดีกว่า offset บนตารางขนาดใหญ่ ตั้งค่า throttle rate แยกต่างหากสำหรับผู้ใช้ที่ไม่ได้ยืนยันตัวและผู้ใช้ที่ยืนยันแล้ว ฝึกฝน pattern เหล่านี้กับ [คำถามสัมภาษณ์ Django ORM](/technologies/django/interview-questions/django-orm-advanced) และ [คำถาม middleware Django](/technologies/django/interview-questions/django-middleware) บน SharpSkill เพื่อสร้างความมั่นใจก่อนเข้าสัมภาษณ์จริง --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/django/django-interview-orm-middleware-drf