Neden Django?
Flask ve FastAPI derslerinde küçük, esnek çatıları (framework) gördün: yönlendirme verirler, gerisini sen seçersin. Django ise "pili içinde" (batteries included) yaklaşımıyla gelir: ORM, migration sistemi, admin paneli, kullanıcı ve yetki sistemi, form doğrulama, şablon motoru, önbellek (cache), e-posta ve güvenlik korumaları tek pakette ve birbiriyle uyumlu. Anadolu Market'in iç yönetim paneli, Ilgaz Bank'ın kredi başvuru takip ekranı ya da belediyenin randevu sistemi gibi veritabanı ağırlıklı, formlu, yetkili kullanıcılı uygulamalarda Django ile birkaç günde çalışan bir ürün çıkarabilirsin.
pre kutularında gösterim içindir; kendi bilgisayarında denemen gerekir. Çalıştırılabilir kutular ve alıştırmalar ise Django'nun arkasındaki mantığı (QuerySet, URL eşleme, form doğrulama, migration SQL'i) saf Python ve sqlite3 ile taklit eder. Böylece Django'yu "sihir" olarak değil, anlaşılır bir makine olarak öğrenirsin.Kurulum: proje ve uygulama
- Proje klasörü aç ve sanal ortam (virtual environment) kur (Sanal ortam ve pip dersini hatırla).
- Django'yu kur, projeyi oluştur, bir uygulama (app) ekle.
- Veritabanı tablolarını oluştur ve geliştirme sunucusunu başlat; tarayıcıda
http://127.0.0.1:8000/adresini aç.
# Windows (PowerShell) python -m venv .venv .venv\Scripts\Activate.ps1 # macOS / Linux python3 -m venv .venv source .venv/bin/activate pip install django django-admin startproject anadolu_market . python manage.py startapp magaza python manage.py migrate python manage.py runserver
Komutların sonunda şu yapı oluşur:
anadolu_market/ # proje paketi: ayarlar ve kök URL'ler
settings.py
urls.py
wsgi.py / asgi.py # sunucunun giriş noktaları
magaza/ # uygulama: tek bir iş alanı
models.py views.py admin.py apps.py tests.py
migrations/
manage.py # yönetim komutlarının kapısı
| Kavram | Ne? | Anadolu Market örneği |
|---|---|---|
| Proje (project) | Ayarlar, kök URL'ler, uygulamaların toplamı | anadolu_market |
| Uygulama (app) | Tek bir iş alanının model, view ve şablonları; başka projeye taşınabilir | magaza, siparis, musteri |
INSTALLED_APPS | Projenin tanıdığı uygulamalar | "magaza.apps.MagazaConfig" eklenmezse modeller görünmez |
Yeni uygulamayı settings.py içindeki INSTALLED_APPS listesine eklemeyi unutma. Aynı dosyada DATABASES (varsayılan SQLite), DEBUG, ALLOWED_HOSTS, SECRET_KEY, TEMPLATES ve STATIC_URL ayarları bulunur.
MVT: Model, View, Template
Django kendi mimarisine MVT der. Klasik MVC ile eşleşmesi biraz kafa karıştırır: Django'nun "view"u MVC'deki controller'a, "template"i MVC'deki view'a karşılık gelir.
| Katman | Görevi | Dosya |
|---|---|---|
| Model | Veri yapısı ve iş kuralları; tabloya eşlenir | models.py |
| View | İsteği alır, modelden veri çeker, yanıt döndürür | views.py |
| Template | HTML üretimi; mantıktan arındırılmış sunum | templates/ |
| URLconf | Yolu doğru view'a yönlendirir | urls.py |
Bir isteğin yolculuğu: tarayıcı /urunler/42/ ister → ara katmanlar (middleware: oturum, CSRF, güvenlik başlıkları) çalışır → URLconf deseni eşler ve pk=42 yakalar → view modeli sorgular → şablon HTML üretir → yanıt yine ara katmanlardan geçip döner.
URL'ler ve view'lar
# magaza/urls.py
from django.urls import path
from . import views
app_name = "magaza"
urlpatterns = [
path("", views.urun_listesi, name="liste"),
path("urunler/yeni/", views.UrunOlustur.as_view(), name="yeni"),
path("urunler/<int:pk>/", views.UrunDetay.as_view(), name="detay"),
path("kategori/<slug:slug>/", views.kategori, name="kategori"),
]
# anadolu_market/urls.py içinde: path("magaza/", include("magaza.urls"))
Desenler yukarıdan aşağıya denenir ve ilk eşleşen kazanır. <int:pk> bir dönüştürücüdür (converter): yalnız rakamları eşler ve değeri int olarak view'a verir. Diğerleri: str (eğik çizgi hariç her şey), slug, uuid, path. Arka planda her desen bir düzenli ifadeye çevrilir; aşağıdaki kutu bu fikri taklit ediyor.
import re
DONUSTURUCU = {"int": (r"[0-9]+", int), "str": (r"[^/]+", str), "slug": (r"[-a-zA-Z0-9_]+", str)}
def derle(desen):
cevirici = {}
def degistir(m):
tur, ad = m.group(1), m.group(2)
cevirici[ad] = DONUSTURUCU[tur][1]
return f"(?P<{ad}>{DONUSTURUCU[tur][0]})"
regex = re.sub(r"<(\w+):(\w+)>", degistir, desen)
return re.compile("^" + regex + "$"), cevirici
regex, cevirici = derle("urunler/<int:pk>/")
for yol in ["urunler/42/", "urunler/abc/", "urunler/7"]:
m = regex.match(yol)
if m:
print(yol, "->", {k: cevirici[k](v) for k, v in m.groupdict().items()})
else:
print(yol, "-> 404")
View iki biçimde yazılır. Fonksiyon tabanlı view (FBV) açık ve basittir; sınıf tabanlı view (CBV) listeleme, detay, oluşturma gibi tekrarlayan kalıpları hazır verir.
# magaza/views.py
from django.shortcuts import render, get_object_or_404
from django.views.generic import DetailView, CreateView
from django.contrib.auth.decorators import login_required
from .models import Urun
def urun_listesi(request):
urunler = Urun.objects.filter(aktif=True).order_by("-eklenme")
return render(request, "magaza/liste.html", {"urunler": urunler})
class UrunDetay(DetailView):
model = Urun # pk'yı URL'den alır, yoksa 404
template_name = "magaza/detay.html"
class UrunOlustur(CreateView):
model = Urun
fields = ["ad", "kategori", "fiyat", "stok"]
success_url = "/magaza/"
Modeller ve ORM
Model, bir tabloyu temsil eden Python sınıfıdır. Her alan (field) bir sütundur; id birincil anahtarı otomatik eklenir.
# magaza/models.py
from django.db import models
class Kategori(models.Model):
ad = models.CharField(max_length=60, unique=True)
class Urun(models.Model):
ad = models.CharField(max_length=120)
kategori = models.ForeignKey(Kategori, on_delete=models.PROTECT, related_name="urunler")
fiyat = models.DecimalField(max_digits=10, decimal_places=2)
stok = models.PositiveIntegerField(default=0)
aktif = models.BooleanField(default=True)
eklenme = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.ad
Urun.objects bir yöneticidir (manager); ondan dönen QuerySet iki özelliğe sahiptir: tembeldir (lazy), yani üzerinde döngü kurana, len, list ya da dilim uygulayana kadar veritabanına gitmez; ve zincirlenebilirdir, her filter yeni bir QuerySet döndürür, eskisini değiştirmez. Alan adı ile işlem arasına çift alt çizgi koyarak sorgu yaparsın; buna lookup denir.
| ORM | SQL karşılığı (yaklaşık) |
|---|---|
filter(fiyat__gt=100) | WHERE fiyat > 100 |
filter(ad__icontains="çay") | WHERE ad LIKE '%çay%' (büyük/küçük harf duyarsız) |
filter(sehir__in=["İzmir", "Bursa"]) | WHERE sehir IN (...) |
exclude(stok=0) | WHERE NOT (stok = 0) |
filter(kategori__ad="İçecek") | JOIN kategori ... WHERE kategori.ad = ... |
aggregate(Sum("fiyat")) | SELECT SUM(fiyat) → tek sözlük |
values("kategori").annotate(n=Count("id")) | GROUP BY kategori → satır başına değer |
order_by("-fiyat")[:5] | ORDER BY fiyat DESC LIMIT 5 |
from django.db.models import Q, F, Count, Sum, Avg
pahali = Urun.objects.filter(fiyat__gte=500).exclude(stok=0)
print(pahali.query) # üretilen SQL'i görürsün
izmir_ya_da_buyuk = Siparis.objects.filter(Q(sehir="İzmir") | Q(tutar__gt=1000))
Urun.objects.filter(stok__lt=F("kritik_stok")) # iki sütunu karşılaştır
Urun.objects.aggregate(toplam=Sum("fiyat"), ort=Avg("fiyat"))
Kategori.objects.annotate(urun_sayisi=Count("urunler")).order_by("-urun_sayisi")
Urun.objects.select_related("kategori") # tek JOIN, N+1 yok
Kategori.objects.prefetch_related("urunler") # 2 sorgu, ters ilişki
Aşağıdaki kutu, zincirlenebilir ve lookup destekli bir QuerySet'in özünü 20 satırda taklit ediyor. Alıştırmalarda bunu adım adım sen kuracaksın.
ISLEM = {
"exact": lambda a, b: a == b,
"gt": lambda a, b: a > b,
"lte": lambda a, b: a <= b,
"icontains": lambda a, b: b.lower() in a.lower(),
}
class Sorgu:
def __init__(self, kayitlar, kosullar=()):
self.kayitlar, self.kosullar = kayitlar, list(kosullar)
def filter(self, **kw):
return Sorgu(self.kayitlar, self.kosullar + list(kw.items()))
def __iter__(self): # tembel: ancak gezilince çalışır
for k in self.kayitlar:
if all(ISLEM[(a.split("__") + ["exact"])[1]](k[a.split("__")[0]], v)
for a, v in self.kosullar):
yield k
urunler = [{"ad": "Rize Çayı", "fiyat": 120}, {"ad": "Türk Kahvesi", "fiyat": 350},
{"ad": "Yeşil Çay", "fiyat": 90}]
qs = Sorgu(urunler).filter(ad__icontains="çay")
ucuz = qs.filter(fiyat__lte=100)
print([u["ad"] for u in qs])
print([u["ad"] for u in ucuz])
ORM sonunda SQL üretir. Aynı sorgunun SQL karşılığını sqlite3 ile görmek, Django'da hangi sorgunun pahalı olduğunu sezmeni sağlar:
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE kategori (id INTEGER PRIMARY KEY, ad TEXT);
CREATE TABLE urun (id INTEGER PRIMARY KEY, ad TEXT, fiyat REAL, kategori_id INTEGER);
INSERT INTO kategori VALUES (1, 'İçecek'), (2, 'Mutfak');
INSERT INTO urun VALUES (1, 'Çay', 120, 1), (2, 'Kahve', 350, 1), (3, 'Bardak', 25, 2);
""")
# Kategori.objects.annotate(n=Count("urunler"), ort=Avg("urunler__fiyat")).order_by("-n")
sql = """SELECT k.ad, COUNT(u.id) AS n, AVG(u.fiyat) AS ort
FROM kategori k LEFT JOIN urun u ON u.kategori_id = k.id
GROUP BY k.id ORDER BY n DESC"""
for satir in db.execute(sql):
print(satir)
Migration: modelden tabloya
Modeli değiştirdiğinde veritabanı kendiliğinden değişmez. İki adım vardır: makemigrations modeldeki farkı bulup migrations/0002_...py dosyası yazar; migrate bu dosyaları veritabanına uygular. Migration dosyaları git'e girer; ekipteki herkes ve sunucu aynı şemaya böyle ulaşır.
python manage.py makemigrations magaza python manage.py sqlmigrate magaza 0001 # üretilecek SQL'i göster python manage.py showmigrations python manage.py migrate
Django'nun yaptığını küçük ölçekte taklit edelim: dataclass alanlarından CREATE TABLE üretmek.
from dataclasses import dataclass, fields
TIP = {int: "INTEGER", str: "VARCHAR(120)", float: "REAL", bool: "BOOL"}
@dataclass
class Musteri:
ad: str
sehir: str
puan: float
aktif: bool
def create_table(model):
sutunlar = ['"id" INTEGER PRIMARY KEY AUTOINCREMENT']
sutunlar += [f'"{f.name}" {TIP[f.type]} NOT NULL' for f in fields(model)]
return f'CREATE TABLE "magaza_{model.__name__.lower()}" (' + ", ".join(sutunlar) + ");"
print(create_table(Musteri))
Tablo adının magaza_musteri olmasına dikkat: Django varsayılan olarak uygulama_model adını kullanır.
Admin paneli
Django'nun en çok sevilen özelliği: birkaç satırla, yetkili personelin veriyi listeleyip arayıp düzenleyebileceği bir yönetim paneli.
# magaza/admin.py
from django.contrib import admin
from .models import Urun
@admin.register(Urun)
class UrunAdmin(admin.ModelAdmin):
list_display = ["ad", "kategori", "fiyat", "stok", "aktif"]
list_filter = ["kategori", "aktif"]
search_fields = ["ad"]
list_select_related = ["kategori"]
# terminalde: python manage.py createsuperuser → /admin/ adresine gir
Şablonlar, formlar ve kimlik doğrulama
<!-- magaza/templates/magaza/liste.html -->
{% extends "base.html" %}
{% block icerik %}
{% for u in urunler %}
<p>{{ u.ad }} — {{ u.fiyat|floatformat:2 }} TL</p>
{% empty %}
<p>Ürün yok.</p>
{% endfor %}
{% endblock %}
{{ u.ad }} çıktısı otomatik kaçırılır (autoescape): ürün adına <script> yazılsa bile metin olarak görünür. Formlarda ise ModelForm, model alanlarından form üretir; is_valid() her alanı dönüştürür, clean_<alan> metotlarındaki kuralları çalıştırır, hataları form.errors'a, temiz veriyi form.cleaned_data'ya koyar.
from django import forms
class UrunFormu(forms.ModelForm):
class Meta:
model = Urun
fields = ["ad", "kategori", "fiyat", "stok"]
def clean_fiyat(self):
fiyat = self.cleaned_data["fiyat"]
if fiyat <= 0:
raise forms.ValidationError("Fiyat pozitif olmalı.")
return fiyat
@login_required
def urun_ekle(request):
form = UrunFormu(request.POST or None)
if request.method == "POST" and form.is_valid():
form.save()
return redirect("magaza:liste")
return render(request, "magaza/form.html", {"form": form})
# şablonda: <form method="post">{% csrf_token %}{{ form.as_p }}</form>
Aynı doğrulama akışının saf Python taklidi:
class UrunFormu:
def __init__(self, veri):
self.veri, self.errors, self.cleaned_data = veri, {}, {}
def is_valid(self):
ad = self.veri.get("ad", "").strip()
if not ad:
self.errors["ad"] = "Bu alan zorunlu."
try:
fiyat = float(self.veri.get("fiyat", ""))
if fiyat <= 0:
self.errors["fiyat"] = "Fiyat pozitif olmalı."
except ValueError:
self.errors["fiyat"] = "Geçerli bir sayı gir."
if not self.errors:
self.cleaned_data = {"ad": ad, "fiyat": fiyat}
return not self.errors
f = UrunFormu({"ad": " Rize Çayı ", "fiyat": "120.5"})
print(f.is_valid(), f.cleaned_data)
g = UrunFormu({"ad": "", "fiyat": "bedava"})
print(g.is_valid(), g.errors)
Kimlik doğrulama hazırdır: django.contrib.auth kullanıcı modelini, parola özetlemeyi (PBKDF2), giriş/çıkış view'larını, gruplar ve izinleri (user.has_perm("magaza.change_urun")) sağlar. View'ı korumak için @login_required ya da CBV'de LoginRequiredMixin kullanırsın.
Django REST Framework (DRF)
Mobil uygulama ya da React ön yüz için JSON API gerekiyorsa DRF eklenir (pip install djangorestframework). Serializer, ModelForm'un API karşılığıdır.
from rest_framework import serializers, viewsets, routers, permissions
class UrunSerializer(serializers.ModelSerializer):
class Meta:
model = Urun
fields = ["id", "ad", "fiyat", "stok"]
class UrunViewSet(viewsets.ModelViewSet):
queryset = Urun.objects.select_related("kategori").filter(aktif=True)
serializer_class = UrunSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
router = routers.DefaultRouter()
router.register("urunler", UrunViewSet) # GET/POST /urunler/, GET/PUT/DELETE /urunler/5/
Güvenlik varsayılanları ve dağıtım
| Tehdit | Django'nun varsayılanı | Seni bozan şey |
|---|---|---|
| SQL enjeksiyonu | ORM her değeri parametre olarak gönderir | raw()/extra() içinde f-string |
| XSS | Şablonlarda otomatik kaçış | |safe, mark_safe ile kullanıcı verisi |
| CSRF | CsrfViewMiddleware + {% csrf_token %} | Gelişigüzel @csrf_exempt |
| Clickjacking | X-Frame-Options: DENY | Middleware'i kaldırmak |
| Parola | PBKDF2 ile tuzlu özet | Parolayı kendi alanında düz saklamak |
Yayına almadan önce: DEBUG = False, SECRET_KEY ortam değişkeninden, ALLOWED_HOSTS dolu, veritabanı PostgreSQL, statik dosyalar collectstatic ile toplanmış. Uygulamayı runserver ile değil, gunicorn (WSGI) ya da uvicorn (ASGI) ile çalıştırırsın; önüne nginx ya da bir platform (Render, Railway, Fly.io) konur. Docker ve dağıtım dersinde bu adımları konteynerle birleştireceksin.
python manage.py check --deploy # güvenlik ayarlarını denetler python manage.py collectstatic --noinput gunicorn anadolu_market.wsgi:application --bind 0.0.0.0:8000 --workers 3
{{ s.musteri.ad }} diye yazmak 1 + 50 sorgu üretir. Kod incelemesinde (code review) liste view'larında select_related/prefetch_related aranır; geliştirme sırasında django-debug-toolbar ile sayfa başına sorgu sayısı izlenir. İkinci klasik: modeli değiştirip migration dosyasını commit etmeyi unutmak; sunucuda "no such column" hatası alınır.Derinleş: Django'da HTMX, form, fetch ile JSON ve SPA kararı
Django'nun şablon sistemi, formları ve admin paneli sunucu tarafı oluşturma (SSR) için kurulmuştur. Bir noktada şu karar gelir: arayüzü ayrı bir SPA (React) ve DRF API'siyle mi kuracağız, yoksa Django şablonlarını koruyup yalnız gereken yerleri dinamik mi yapacağız? İkinci yolun en yaygın aracı HTMX'tir. Flask dersindeki ek bölümde HTMX'in temel özniteliklerini (hx-get, hx-target, hx-swap, hx-trigger) gördüysen, burada aynı fikrin Django'daki karşılığını, CSRF'yi ve JSON uç noktalarını öğreneceksin.
| Soru | Django şablonu + HTMX | DRF + SPA |
|---|---|---|
| Arama motoru (SEO) önemli mi? | Hazır HTML, doğal | Ek sunucu tarafı oluşturma gerekir |
| Mobil uygulama da aynı veriyi kullanacak mı? | Ayrı JSON uç noktaları gerekir | Aynı API |
| Ekip | Python ağırlıklı | Ayrı ön yüz uzmanlığı |
| Admin, form, yetki | Django'nun hazır araçları aynen | API ve istemcide yeniden kurulur |
Parça şablonu ve HX-Request
Django'da parça şablonu ayrı bir dosyadır ve tam sayfa şablonunda {% include %} ile kullanılır. Görünüm, request.headers ile HTMX isteğini tanır; django-htmx paketi bunu request.htmx olarak kısaltır. Sayfalamada Django'nun Paginator sınıfı kullanılır.
# urunler/views.py
from django.core.paginator import Paginator
from django.shortcuts import render
from .models import Urun
def urun_listesi(request):
q = request.GET.get("q", "")
qs = Urun.objects.filter(ad__icontains=q).order_by("id")
sayfa = Paginator(qs, 12).get_page(request.GET.get("sayfa"))
baglam = {"sayfa": sayfa, "q": q}
if request.headers.get("HX-Request") == "true": # ya da request.htmx
return render(request, "urunler/_satirlar.html", baglam)
return render(request, "urunler/liste.html", baglam){# urunler/_satirlar.html #}
{% for u in sayfa %}
<tr><td>{{ u.ad }}</td><td>{{ u.fiyat }} TL</td></tr>
{% endfor %}
{% if sayfa.has_next %}
<tr hx-get="?q={{ q|urlencode }}&sayfa={{ sayfa.next_page_number }}"
hx-trigger="revealed" hx-swap="outerHTML"><td>Yükleniyor...</td></tr>
{% endif %}CSRF: Django'nun kapısı
Django, POST/PUT/PATCH/DELETE isteklerinde CSRF belirteci (token) ister. Klasik formda {% csrf_token %} gizli bir csrfmiddlewaretoken alanı ekler. HTMX ve fetch isteklerinde belirteç başlıkla gönderilir: X-CSRFToken. Belirtecin değeri csrftoken çerezinde de durur.
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'> {# tüm HTMX istekleri başlığı taşır #}
<script>
function cerez(ad) {
return document.cookie.split("; ").find(c => c.startsWith(ad + "="))?.split("=")[1];
}
const r = await fetch("/api/sepet/", {
method: "POST",
headers: {"Content-Type": "application/json", "X-CSRFToken": cerez("csrftoken")},
body: JSON.stringify({urun_id: 19, adet: 2}),
});
</script>Çerezden belirteç okumak tarayıcıda çalışan kodun işidir; mantığı Python'la görelim. Gerçek Anadolu Market ürünlerini veri modülünden okuyup JSON'a çeviriyoruz:
import json
from decimal import Decimal
from http.cookies import SimpleCookie
import veri
c = SimpleCookie()
c.load("sessionid=abc123; csrftoken=Xy9QeT2k")
print("çerezdeki belirteç:", c["csrftoken"].value)
urunler = veri.kayitlar("anadolu_market", "products")
kitaplar = [{"id": u["product_id"], "ad": u["name"], "fiyat": Decimal(str(u["price"]))}
for u in urunler if u["category"] == "Kitap"][:3]
def ondalik(o): # DjangoJSONEncoder'ın yaptığı gibi
if isinstance(o, Decimal):
return str(o)
raise TypeError(f"{type(o).__name__} JSON'a çevrilemez")
print(json.dumps(kitaplar, default=ondalik, ensure_ascii=False))JsonResponse ve fetch
JSON uç noktası için DRF şart değildir; küçük işlerde JsonResponse yeter. İki ayrıntı: varsayılan safe=True yalnız sözlük kabul eder (liste için safe=False), ve DecimalField değerleri Django'nun kodlayıcısında metne çevrilir. Türkçe karakterlerin kaçırılmaması için json_dumps_params={"ensure_ascii": False} verilebilir.
import json
from django.http import JsonResponse
from django.views.decorators.http import require_POST
@require_POST
def sepet_api(request):
try:
govde = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"hata": "geçersiz JSON"}, status=400)
form = SepetFormu(govde) # Django formu JSON ile de çalışır
if not form.is_valid():
return JsonResponse({"hatalar": form.errors}, status=422)
return JsonResponse({"tutar": str(form.tutar())})
def kategoriler(request):
return JsonResponse(list(Kategori.objects.values("id", "ad")), safe=False,
json_dumps_params={"ensure_ascii": False})Form hataları HTMX ile
HTMX 2, varsayılan olarak 4xx/5xx yanıtları sayfaya yerleştirmez. Form hatalarını göstermek için iki yol var: hatalı formu 200 ile parça olarak döndürmek, ya da 422 dönüp HX-Retarget ve HX-Reswap yanıt başlıklarıyla hataların nereye konacağını söylemek (ve istemcide 422'yi yerleştirmeye izin veren yapılandırmayı açmak). Hangi yolu seçersen seç, kural aynıdır: doğrulama her zaman sunucuda yapılır.
Sorgu sayısını ölç, sonra optimize et
from django.db import connection ile len(connection.queries) (DEBUG açıkken) ya da testlerde self.assertNumQueries(2) kullan. only("ad", "fiyat") ve defer("aciklama") gereksiz sütunları çekmez; values_list("ad", flat=True) model nesnesi bile kurmaz. Büyük tabloda iterator() belleği korur; toplu yazmada bulk_create ve update() tek sorguyla çalışır.
İşlem bütünlüğü ve yarış durumları
Stok düşme gibi işlemleri transaction.atomic() içinde yap; iki kullanıcı aynı anda son ürünü alırken select_for_update() satırı kilitler. Urun.objects.filter(pk=5).update(stok=F("stok") - 1) ise okuma-yazma arasında kayıp güncellemeyi önler, çünkü hesap veritabanında yapılır.
Mimari alışkanlıklar
İş kuralını view'a gömme; model metotlarına ya da ayrı bir services.py katmanına koy. Sinyalleri (signals) ölçülü kullan; gizli yan etkiler hata ayıklamayı zorlaştırır. Ayarları django-environ ya da ortam değişkenleriyle yönet, pytest-django ile test yaz. Uzun süren işleri (e-posta, rapor) Celery gibi bir kuyruğa ver; bunun mantığını Orkestrasyon dersinde göreceksin.