07 β Model munosabatlari¶
β¬ οΈ Oldingi: 06 β ORM va QuerySet so'rovlar Β· π README Β· Keyingi: 08 β Admin panel β‘οΈ
Bu bobda: real ilovalarda ma'lumotlar hech qachon yolg'iz bo'lmaydi β muallifning postlari, postning teglari, foydalanuvchining profili bir-biriga bog'langan. Shu bog'lanishlarni Django'da qanday ifodalashni o'rganamiz.
ForeignKeyorqali "bittadan ko'pga" (one-to-many) munosabatni,ManyToManyFieldorqali "ko'pdan ko'pga" (many-to-many) munosabatni,OneToOneFieldorqali "bitta-bittaga" (one-to-one) munosabatni quramiz.on_deleteargumentining barcha muhim variantlarini (CASCADE,PROTECT,SET_NULL) amalda ko'rib, ota obyekt o'chganda bola obyektga nima bo'lishini boshqaramiz.related_namebilan teskari munosabatga chiroyli nom beramiz, related obyektlarga to'g'ri va teskari yo'nalishda kirishni, M2M ga qo'shimcha ma'lumot saqlash uchunthroughmodelni ishlatishni o'rganamiz. Oxirida har bir Django dasturchisi duch keladigan eng mashhur ishlash muammosi β N+1 so'rov muammosiga kirishib,select_relatedvaprefetch_relatedbilan uni qanday hal qilishni ko'ramiz. Modellarni bilmasangiz, avval 06-bobga qayting; baza jadvallari vaJOINtushunchasi uchun SQL qo'llanmasi yordam beradi. Hamma kod Django 6.0.6 va Python 3.14 da haqiqatan ishga tushirib tekshirilgan.
Nega munosabatlar kerak?¶
Tasavvur qiling, blog yozyapmiz. Har bir postning muallifi bor. Har bir muallif bir nechta post yozishi mumkin. Har bir post bir nechta teg (tag) bilan belgilanadi, va har bir teg ko'p postlarda uchraydi. Har bir foydalanuvchining bitta profili bor.
Bularning hammasini bitta jadvalga tiqishtirish mumkin emas β ma'lumot takrorlanib ketadi, yangilash kabusga aylanadi. Yechim: ma'lumotni alohida jadvallarga bo'lib, ular orasiga bog'lanish (munosabat) o'rnatish. Bu β relyatsion bazaning asosiy g'oyasi.
Django'da munosabatlarning uch turi bor:
| Tur | Maydon | Misol | Ma'no |
|---|---|---|---|
| One-to-many | ForeignKey |
Post -> Author | Bitta muallifga ko'p post |
| Many-to-many | ManyToManyField |
Post <-> Tag | Ko'p post, ko'p teg |
| One-to-one | OneToOneField |
User -> Profile | Bitta foydalanuvchi, bitta profil |
Quyidagi misollar uchun blog ilovasida bitta models.py faylida ishlaymiz. To'liq fayl shunday ko'rinadi (qismlarini keyin bo'lim-bo'lim tushuntiramiz):
# blog/models.py
from django.conf import settings
from django.db import models
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="profile",
)
bio = models.TextField(blank=True)
class Category(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Tag(models.Model):
name = models.CharField(max_length=50)
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="posts",
)
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="posts",
)
tags = models.ManyToManyField(Tag, related_name="posts", blank=True)
body = models.TextField(blank=True)
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.title
Eslatma: foydalanuvchiga ishora qilganda doim
settings.AUTH_USER_MODELishlating, to'g'ridan-to'g'rifrom django.contrib.auth.models import Userimport qilibUseryozmang. Bu β Django'ning rasmiy tavsiyasi: kelajakda maxsus (custom) User modelga o'tsangiz, modellaringizni umuman o'zgartirmaysiz.
ForeignKey β one-to-many munosabat¶
ForeignKey "bittadan ko'pga" munosabatni ifodalaydi. Bizning misolda: bitta muallif (User) ko'p post yozadi. ForeignKey maydoni har doim "ko'p" tomonda, ya'ni Post modelida turadi:
class Post(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(
settings.AUTH_USER_MODEL, # qaysi modelga bog'lanamiz
on_delete=models.CASCADE, # author o'chsa, postlari ham o'chadi
related_name="posts", # teskari munosabat nomi (keyin tushuntiramiz)
)
Bazada bu Post jadvaliga author_id ustunini qo'shadi β bu ustun auth_user jadvalining id ustuniga ishora qiluvchi chet kalit (foreign key). Buni Django avtomatik yaratadi. Generatsiya qilingan SQL'da quyidagini ko'rasiz:
CREATE TABLE "blog_post" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"title" varchar(200) NOT NULL,
...
"author_id" integer NOT NULL REFERENCES "auth_user" ("id"),
"category_id" bigint NULL REFERENCES "blog_category" ("id")
);
author_id ustuni nomidagi _id qo'shimchasiga e'tibor bering: Python kodida post.author deb yozasiz (bu User obyektini qaytaradi), bazada esa author_id (faqat son) saqlanadi.
To'g'ri munosabatga kirish: bola -> ota¶
Postdan uning muallifiga o'tish β eng oddiy yo'nalish. Maydon nomini ishlatasiz:
post = Post.objects.create(title="Birinchi post", author=user)
print(post.author.username) # bola -> ota: User obyekti
print(post.author_id) # faqat ID kerak bo'lsa, qo'shimcha so'rovsiz
post.author baza so'rovi yuboradi (User obyektini olib keladi). Agar sizga faqat ID kerak bo'lsa, post.author_id ishlating β u allaqachon yuklangan, qo'shimcha so'rov yubormaydi. Bu kichik narsa, lekin tezlik uchun muhim.
null=True qachon kerak?¶
Yuqoridagi author maydoni majburiy β har post muallifsiz bo'lolmaydi. Lekin category ixtiyoriy bo'lsin desak:
category = models.ForeignKey(
Category,
on_delete=models.SET_NULL,
null=True, # bazada NULL bo'lishi mumkin
blank=True, # formada bo'sh qoldirish mumkin
)
null=True β baza darajasi (ustun NULL qabul qiladi). blank=True β forma/validatsiya darajasi (maydonni bo'sh qoldirsa bo'ladi). Ko'pincha ikkalasi birga keladi, lekin ular har xil narsa.
related_name va teskari munosabat: ota -> bola¶
ForeignKey ikki yo'nalishli ko'prik yaratadi. Postdan muallifga o'tdik (post.author). Endi teskari: muallifdan uning barcha postlariga qanday o'tamiz?
Django har bir ForeignKey uchun avtomatik teskari menejer (reverse manager) yaratadi. related_name="posts" bergan bo'lsak, shunday yozamiz:
user = User.objects.get(username="ali")
print(user.posts.count()) # ali ning postlari soni
print(user.posts.all()) # hamma postlari (QuerySet)
print(user.posts.filter(title__icontains="django")) # filtrlash ham mumkin
user.posts β oddiy menejer, ya'ni unda all(), filter(), count(), create() va boshqalar bor. Bu β to'liq QuerySet, demak 06-bobdagi hamma usullar ishlaydi.
related_name bermasangiz nima bo'ladi?¶
Agar related_name ko'rsatmasangiz, Django standart nom yasaydi: <model_nomi_kichik_harfda>_set. Ya'ni:
post_set ishlaydi, lekin xunuk. related_name="posts" ancha tabiiy. Shuning uchun deyarli har doim related_name beriladi.
Teskari menejerdan obyekt yaratish¶
Teskari menejer orqali to'g'ridan-to'g'ri yangi obyekt yaratish ham mumkin β bunda bog'lanish avtomatik o'rnatiladi:
user = User.objects.get(username="ali")
# author=user ni alohida yozish shart emas, avtomatik bog'lanadi:
new_post = user.posts.create(title="Teskari menejerdan yaratildi")
print(new_post.author == user) # True
Diqqat β to'g'rilik: quyidagi ikki nom bir-biriga teskari, ularni adashtirmang.
on_delete β ota o'chganda bolaga nima bo'ladi?¶
ForeignKey va OneToOneField uchun on_delete majburiy argument. U bitta muhim savolga javob beradi: ota obyekt (masalan Category) o'chirilsa, unga bog'langan bola obyektlar (Post'lar) bilan nima qilamiz? Javobni noto'g'ri tanlasangiz, ma'lumotingiz "yetim" qoladi yoki tasodifan butun bir bo'lim o'chib ketadi.
Eng ko'p ishlatiladigan uchta variant:
CASCADE β birga o'chadi¶
Ota o'chsa, hamma bolalari ham o'chadi. "Bu post bu foydalanuvchiga tegishli, foydalanuvchi yo'q bo'lsa postlari ham keraksiz" mantiqi uchun.
Amalda:
user = User.objects.create_user("dilshod")
post = Post.objects.create(title="Cascade test", author=user)
pid = post.id
user.delete() # foydalanuvchini o'chiramiz
print(Post.objects.filter(id=pid).exists()) # False β post ham o'chdi
CASCADE β eng keng tarqalgan tanlov, lekin ehtiyot bo'ling: u jimgina butun zanjirni o'chiradi. Foydalanuvchini o'chirsangiz, uning postlari, postlarning izohlari... hammasi yo'qoladi.
PROTECT β o'chirishni taqiqlaydi¶
Agar bog'langan bola obyektlar bo'lsa, ota obyektni o'chirishga umuman yo'l qo'ymaydi va ProtectedError ko'taradi. "Avval bog'langan yozuvlarni hal qilmasdan turib bu narsani o'chira olmaysan" mantiqi uchun. Misol: izohi bor postni tasodifan o'chirib qo'ymaslik.
class Comment(models.Model):
post = models.ForeignKey(Post, on_delete=models.PROTECT, related_name="comments")
text = models.CharField(max_length=300)
Amalda:
from django.db.models.deletion import ProtectedError
post = Post.objects.create(title="Muhim post", author=user)
Comment.objects.create(post=post, text="Zo'r maqola!")
try:
post.delete() # β izohi bor, o'chmaydi
except ProtectedError:
print("Postni o'chirib bo'lmaydi: unga bog'langan izohlar bor")
Kutilmagan, lekin foydali xatti-harakat:
PROTECTzanjir bo'ylab "tepaga" ham ta'sir qiladi. AgarPost.authorCASCADE, lekinComment.postPROTECTbo'lsa, izohi bor postga ega foydalanuvchini o'chirishga ham yo'l qo'yilmaydi. Sababi: foydalanuvchini o'chirish postlarini o'chirishga uriniladi (CASCADE), lekin postda himoyalangan izoh bor (PROTECT), shuning uchun butun amal bekor qilinadi. Bu xato emas β Django ma'lumotingizni shunday himoya qiladi.
SET_NULL β bog'lanish uziladi, bola yashaydi¶
Ota o'chsa, bolaning chet kalit maydoni NULL ga o'rnatiladi. Bola obyekt o'chmaydi, faqat "bog'lanishi yo'qoladi". Buning uchun maydon null=True bo'lishi shart.
category = models.ForeignKey(
Category, on_delete=models.SET_NULL, null=True, blank=True, related_name="posts"
)
Amalda:
cat = Category.objects.create(name="Texnologiya")
post = Post.objects.create(title="AI haqida", author=user, category=cat)
cat.delete() # kategoriyani o'chiramiz
post.refresh_from_db() # postni bazadan qayta yuklaymiz
print(post.category) # None β post qoldi, kategoriyasi yo'q
print(post.category_id) # None
refresh_from_db() β muhim: cat.delete() faqat bazani o'zgartirdi, lekin Python xotirasidagi post obyekti hali eski qiymatni tutib turibdi. Yangi holatni ko'rish uchun obyektni bazadan qayta yuklaymiz.
To'liq ro'yxat (qisqacha)¶
| Variant | Ota o'chsa bola... | null=True kerakmi? |
|---|---|---|
CASCADE |
birga o'chadi | yo'q |
PROTECT |
o'chirilmaydi (ProtectedError) |
yo'q |
SET_NULL |
qoladi, FK = NULL | ha |
SET_DEFAULT |
qoladi, FK = default qiymat | yo'q (default kerak) |
SET(...) |
qoladi, FK = berilgan qiymat | holatga qarab |
DO_NOTHING |
hech narsa (baza o'zi hal qiladi β xavfli) | yo'q |
RESTRICT |
o'chirilmaydi (RestrictedError), lekin... |
yo'q |
RESTRICT ning PROTECT dan aniq farqi: u ham o'chirishni taqiqlaydi (RestrictedError ko'taradi), ammo agar o'sha bolaning o'zi shu amal davomida boshqa CASCADE yo'li bilan o'chirilayotgan bo'lsa, unda ruxsat beradi. Ya'ni: bola allaqachon o'chib ketayotgan bo'lsa, RESTRICT to'sqinlik qilmaydi; PROTECT esa bunday vaziyatda ham qattiq taqiqlaydi. Bu nozik farq murakkab kaskad zanjirlarda kerak bo'ladi.
Amalda 90% holatlarda CASCADE, PROTECT yoki SET_NULL yetadi.
ManyToManyField β many-to-many munosabat¶
Endi murakkabroq holat: bitta post ko'p tegga ega, bitta teg ko'p postda uchraydi. Bu "ko'pdan ko'pga" munosabat. Uni bitta chet kalit bilan ifodalab bo'lmaydi (author_id kabi bitta ustun yetmaydi). Yechim: alohida bog'lovchi jadval (join table). Django buni ManyToManyField orqali avtomatik yaratadi:
class Post(models.Model):
title = models.CharField(max_length=200)
tags = models.ManyToManyField(Tag, related_name="posts", blank=True)
makemigrations qilganingizda Django yashirin blog_post_tags jadvalini yaratadi:
CREATE TABLE "blog_post_tags" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"post_id" bigint NOT NULL REFERENCES "blog_post" ("id"),
"tag_id" bigint NOT NULL REFERENCES "blog_tag" ("id")
);
Bu jadvalning har bir qatori bitta "post-teg" juftligini ifodalaydi. Siz uni hech qachon to'g'ridan-to'g'ri ko'rmaysiz β ORM hammasini siz uchun boshqaradi.
M2M bilan ishlash: add, remove, set, clear¶
ForeignKeydan farqli o'laroq, M2M maydonini = bilan o'rnatib bo'lmaydi. post.tags = [...] yozsangiz, Django uni ataylab taqiqlaydi va TypeError ko'taradi:
# β XATO: forward M2M ga to'g'ridan-to'g'ri biriktirish taqiqlangan
post.tags = [t1, t2]
# TypeError: Direct assignment to the forward side of a many-to-many
# set is prohibited. Use tags.set() instead.
Sababi: M2M bog'lanish alohida join jadvalda saqlanadi, oddiy ustun emas β shuning uchun uni atribut sifatida bevosita o'zlashtirib bo'lmaydi. Django sizni xatoning oldini olish uchun maxsus metodlarni ishlatishga majbur qiladi:
post = Post.objects.create(title="Django darslari", author=user)
t1 = Tag.objects.create(name="django")
t2 = Tag.objects.create(name="python")
post.tags.add(t1, t2) # teg
print(post.tags.count()) # 2
post.tags.remove(t1) # bittasini olib tashlash
print(post.tags.count()) # 1
post.tags.set([t1, t2]) # to'liq qayta o'rnatish (eskisini almashtiradi)
print(post.tags.count()) # 2
post.tags.clear() # hammasini uzish
print(post.tags.count()) # 0
| Metod | Vazifa |
|---|---|
.add(obj, ...) |
bog'lanish qo'shadi |
.remove(obj, ...) |
bog'lanishni olib tashlaydi |
.set([obj, ...]) |
hammasini berilgan ro'yxatga almashtiradi |
.clear() |
hamma bog'lanishni uzadi |
Diqqat: .add(), .remove(), .clear() faqat bog'lanishni o'zgartiradi, Tag obyektlarining o'zini o'chirmaydi.
Teskari yo'nalish M2M'da¶
M2M ikki tomonlama simmetrik. related_name="posts" berdik, demak tegdan postlarga ham o'tamiz:
django_tag = Tag.objects.get(name="django")
print(django_tag.posts.all()) # bu teg bilan belgilangan hamma postlar
print(django_tag.posts.count())
M2M bo'ylab filtrlash va distinct() tuzog'i¶
Munosabat bo'ylab filtrlashda ikki tomonli pastki chiziq (__) ishlatiladi:
Ammo bu yerda klassik tuzoq bor. Agar bir postda ikkala teg ham bo'lsa, JOIN natijasida u ikki marta qaytadi:
qs = Post.objects.filter(tags__name__in=["django", "python"])
print(qs.count()) # 2 β bir post ikki marta sanaldi! β
print(qs.distinct().count()) # 1 β to'g'ri natija β
M2M bo'ylab filtr ishlatganda dublikatdan qutulish uchun ko'pincha .distinct() qo'shish kerak. Buni eslab qolish muhim β debug paytida ko'p vaqt yeydi.
OneToOneField β one-to-one munosabat¶
OneToOneField β aslida unique=True qo'yilgan ForeignKey. U "har bir A uchun aynan bitta B" munosabatini ifodalaydi. Eng mashhur ishlatilishi: standart User modelini buzmasdan unga qo'shimcha maydonlar qo'shish (profil naqshi).
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="profile",
)
bio = models.TextField(blank=True)
Bazada bu user_id ustunini UNIQUE cheklov bilan yaratadi β ya'ni bitta foydalanuvchiga ikkita profil bog'lab bo'lmaydi:
CREATE TABLE "blog_profile" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"bio" text NOT NULL,
"user_id" integer NOT NULL UNIQUE REFERENCES "auth_user" ("id")
);
Ikkala yo'nalishda kirish β sodda, hech qanday _set yoki ro'yxat yo'q, chunki har tomondan aynan bitta obyekt bor:
user = User.objects.create_user("ali")
profile = Profile.objects.create(user=user, bio="Backend dasturchi")
print(profile.user.username) # to'g'ri: profile -> user
print(user.profile.bio) # teskari: user -> profile (related_name)
Yo'q profil tuzog'i¶
Agar foydalanuvchining profili hali yaratilmagan bo'lsa, user.profile ga murojaat qilish xato (DoesNotExist) ko'taradi:
yangi = User.objects.create_user("yangi_user")
# bu user uchun Profile yaratilmagan
try:
yangi.profile # β Profile.DoesNotExist
except Profile.DoesNotExist:
print("Bu foydalanuvchining profili yo'q")
# Xavfsiz tekshiruv:
if hasattr(yangi, "profile"):
print(yangi.profile.bio)
else:
print("Profil yo'q") # hasattr False qaytaradi
hasattr(user, "profile") β yo'q profil holatini xavfsiz tekshirishning oson yo'li.
through β M2M ga qo'shimcha ma'lumot qo'shish¶
Oddiy ManyToManyField faqat "bog'langan/bog'lanmagan" faktini saqlaydi. Lekin ko'pincha bog'lanishning o'ziga qo'shimcha ma'lumot qo'shish kerak bo'ladi. Masalan: foydalanuvchi loyihaga a'zo β lekin u qaysi rolda (admin, muharrir) va qachon qo'shilgan?
Bu ma'lumotni na User, na Projectga qo'yib bo'lmaydi β u aynan bog'lanishga tegishli. Yechim: o'z qo'limiz bilan oraliq model (through model) yozish:
class Project(models.Model):
name = models.CharField(max_length=100)
members = models.ManyToManyField(
settings.AUTH_USER_MODEL,
through="Membership", # oraliq modelni ko'rsatamiz
related_name="projects",
)
def __str__(self):
return self.name
class Membership(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
role = models.CharField(max_length=50) # qo'shimcha maydon!
joined = models.DateField(auto_now_add=True) # qo'shimcha maydon!
Membership β Django avtomatik yasaydigan yashirin join jadvalining o'rniga bizniki. Unda ikkita ForeignKey (har tomonga) va istalgancha qo'shimcha maydon bor.
through model bilan ishlash¶
through ishlatilganda muhim cheklov bor: oddiy .add(user) ishlamaydi, chunki Django role maydoniga qanday qiymat qo'yishni bilmaydi. Ikki yo'l bor:
user = User.objects.create_user("ali")
project = Project.objects.create(name="E-commerce sayt")
# 1-yo'l: Membership obyektini to'g'ridan-to'g'ri yaratish
Membership.objects.create(user=user, project=project, role="admin")
# 2-yo'l: through_defaults bilan .add() (Django 2.2+)
user2 = User.objects.create_user("vali")
project.members.add(user2, through_defaults={"role": "muharrir"})
O'qishda esa hammasi odatdagidek ishlaydi:
print(project.members.count()) # 2 β a'zolar soni
print(project.members.all()) # User obyektlari
print(user.projects.all()) # teskari: ali ning loyihalari
# qo'shimcha maydonga kirish uchun Membership orqali:
m = Membership.objects.get(user=user, project=project)
print(m.role) # "admin"
project.members sizga User obyektlarini beradi (rol ko'rinmaydi). Rolni ko'rish uchun Membership jadvaliga murojaat qilasiz. Bu β through modelning to'lovi: ko'proq nazorat, lekin biroz ko'proq kod.
N+1 muammosiga kirish¶
Endi har bir Django dasturchisi (va umuman ORM ishlatadigan har kim) duch keladigan eng mashhur ishlash muammosiga keldik. U shu qadar keng tarqalganki, o'z nomi bor: N+1 so'rov muammosi.
Tasavvur qiling, hamma postlarni muallifi bilan birga chiqarmoqchimiz:
# β N+1 MUAMMO
for post in Post.objects.all(): # 1 ta so'rov: hamma postlar
print(post.title, post.author.username) # HAR post uchun +1 so'rov!
Bu kod toza ishlaydi, lekin yashirin muammosi bor. Post.objects.all() bitta so'rov yuboradi. Lekin har bir post.author murojaati alohida baza so'rovi yuboradi, chunki muallif hali yuklanmagan. Agar 5 ta post bo'lsa: 1 (postlar) + 5 (har bir author) = 6 so'rov. 1000 post bo'lsa β 1001 so'rov! Baza tiqilib qoladi.
So'rovlar sonini o'lchab, buni o'z ko'zimiz bilan ko'ramiz. Django'da CaptureQueriesContext aynan shu uchun:
from django.db import connection
from django.test.utils import CaptureQueriesContext
# N+1: select_related YO'Q
with CaptureQueriesContext(connection) as ctx:
for post in Post.objects.all():
_ = post.author.username
print("So'rovlar:", len(ctx.captured_queries)) # 6 (5 post uchun)
Yechim 1: select_related (FK va O2O uchun)¶
select_related Django'ga "bu munosabatni darrov, SQL JOIN orqali olib kel" deydi. Natijada hammasi bitta so'rovda keladi. U ForeignKey va OneToOneField uchun ishlaydi (chunki bu yerda har bola uchun aynan bitta ota bor, JOIN qulay):
# β
select_related bilan
with CaptureQueriesContext(connection) as ctx:
for post in Post.objects.select_related("author"):
_ = post.author.username
print("So'rovlar:", len(ctx.captured_queries)) # 1 ta!
5 ta postdan 6 so'rov o'rniga endi 1 ta so'rov. 1000 post bo'lsa ham β baribir 1 ta.
Yechim 2: prefetch_related (M2M va teskari munosabat uchun)¶
ManyToManyField yoki teskari ForeignKey (masalan user.posts) uchun JOIN ishlamaydi (har tomonda ko'p obyekt bor). Bu holatda prefetch_related ishlatamiz. U ikkita alohida so'rov yuboradi va natijani Python tomonda birlashtiradi:
# β
prefetch_related bilan (M2M tags uchun)
with CaptureQueriesContext(connection) as ctx:
for post in Post.objects.prefetch_related("tags"):
_ = list(post.tags.all())
print("So'rovlar:", len(ctx.captured_queries)) # 2 ta (doimiy)
Postlar soni qancha bo'lishidan qat'i nazar, doimo 2 ta so'rov: biri postlar uchun, ikkinchisi shu postlarga tegishli hamma teglar uchun.
Qaysi birini ishlataman?¶
| Munosabat turi | Optimallashtirish |
|---|---|
ForeignKey (bola -> ota) |
select_related |
OneToOneField |
select_related |
ManyToManyField |
prefetch_related |
Teskari ForeignKey (user.posts) |
prefetch_related |
Ikkalasini birga ham ishlatish mumkin: Post.objects.select_related("author").prefetch_related("tags").
Bu β kirish, davomi keyin. N+1 muammosi va uning chuqurroq yechimlari (masalan
Prefetchobyekti,annotatebilan birlashtirish,only/defer) bo'yicha to'liqroq ma'lumot ORM optimallashtirish bo'limida bo'ladi. Hozircha asosiy qoidani eslab qoling: siklda munosabatga murojaat qilsangiz, ehtimolselect_relatedyokiprefetch_relatedkerak.
Munosabatlarni test bilan mustahkamlash¶
Munosabatlar to'g'ri ishlashini pytest-django bilan tekshiramiz. Bu β har bir jiddiy loyihada bo'lishi kerak bo'lgan odat. (pytest-django o'rnatish va sozlash uchun keyingi test boblariga qarang; bu yerda faqat munosabatlarga oid testlarni ko'rsatamiz.)
# blog/tests_rel.py
import pytest
from django.contrib.auth.models import User
from django.db.models import Count
from django.db.models.deletion import ProtectedError
from blog.models import Profile, Category, Tag, Post, Comment, Project, Membership
@pytest.mark.django_db
def test_foreignkey_reverse():
u = User.objects.create_user("ali")
Post.objects.create(title="A", author=u)
Post.objects.create(title="B", author=u)
assert u.posts.count() == 2 # teskari menejer ishlayapti
assert Post.objects.first().author == u
@pytest.mark.django_db
def test_protect_blocks_delete():
u = User.objects.create_user("ali")
p = Post.objects.create(title="A", author=u)
Comment.objects.create(post=p, text="izoh")
with pytest.raises(ProtectedError): # izohli post o'chmasligi kerak
p.delete()
@pytest.mark.django_db
def test_set_null():
u = User.objects.create_user("ali")
c = Category.objects.create(name="K")
p = Post.objects.create(title="A", author=u, category=c)
c.delete()
p.refresh_from_db()
assert p.category_id is None # post qoldi, kategoriyasi NULL
@pytest.mark.django_db
def test_annotate_count():
u = User.objects.create_user("ali")
Post.objects.create(title="A", author=u)
Post.objects.create(title="B", author=u)
qs = User.objects.annotate(n=Count("posts")) # munosabat bo'ylab sanash
assert qs.get(username="ali").n == 2
@pytest.mark.django_db
def test_m2m_distinct():
u = User.objects.create_user("ali")
p = Post.objects.create(title="A", author=u)
t1 = Tag.objects.create(name="django")
t2 = Tag.objects.create(name="python")
p.tags.add(t1, t2)
qs = Post.objects.filter(tags__name__in=["django", "python"])
assert qs.count() == 2 # dublikat: bir post ikki marta sanaldi
assert qs.distinct().count() == 1 # distinct() tuzatadi
@pytest.mark.django_db
def test_onetoone_missing_profile():
u = User.objects.create_user("ali")
assert not hasattr(u, "profile") # profil hali yaratilmagan
with pytest.raises(Profile.DoesNotExist):
_ = u.profile
Profile.objects.create(user=u, bio="Backend")
u.refresh_from_db()
assert u.profile.bio == "Backend" # endi bor
@pytest.mark.django_db
def test_through_membership():
u = User.objects.create_user("ali")
proj = Project.objects.create(name="E-commerce")
proj.members.add(u, through_defaults={"role": "admin"})
assert proj.members.count() == 1
assert u.projects.count() == 1 # teskari yo'nalish ishlayapti
m = Membership.objects.get(user=u, project=proj)
assert m.role == "admin" # through modeldagi qo'shimcha maydon
Ishga tushirish:
Natija: 7 passed. Bu yetti test bobning asosiy g'oyalarini qamrab oladi: FK teskari menejer, PROTECT, SET_NULL, annotate(Count(...)), M2M distinct() tuzog'i, OneToOneField DoesNotExist, va through model. test_annotate_count dagi annotate(Count("posts")) β juda foydali naqsh: har bir foydalanuvchining nechta posti borligini bitta so'rovda hisoblaydi (yana N+1'dan qochish).
Boshqa tillar bilan solishtirish¶
Agar Node.js ekotizimidan kelgan bo'lsangiz, Django ORM'ining munosabatlari Prisma yoki Sequelize'dagiga juda o'xshaydi: ForeignKey ~ @relation/belongsTo, ManyToManyField ~ implicit/explicit many-to-many, select_related/prefetch_related ~ Prisma'dagi include. N+1 muammosi esa tildan qat'i nazar β barcha ORM'larda bir xil. SQL darajasida nima sodir bo'layotganini chuqurroq tushunish uchun SQL qo'llanmasidagi JOIN boblariga murojaat qiling β select_related aslida shunchaki INNER JOIN (yoki LEFT JOIN) ekanini ko'rasiz.
Mashqlar¶
Oson¶
Authormodeli (name) vaBookmodeli (title,author->ForeignKey) yarating. Bitta muallifga bir nechta kitob bog'lash one-to-many ekanini tushuntiring.ForeignKeyqaysi modelda turishi kerak?Book.authormaydonigarelated_name="books"bering. Bitta muallifning hamma kitoblarini olib keladigan kod yozing.Book.authorgarelated_namebermasangiz, muallifning kitoblariga qaysi nom bilan murojaat qilasiz?Post.authormaydonidaon_delete=models.CASCADEturibdi. Foydalanuvchi o'chsa postlariga nima bo'ladi? Bir gap bilan yozing.categorymaydoniSET_NULLbo'lishi uchun unga yana qaysi argument majburiy kerak? Nega?post.authorvapost.author_idorasidagi farqni ayting: qaysi biri qo'shimcha baza so'rovi yuborishi mumkin?
O'rta¶
StudentvaCoursemodellari orasidaManyToManyFieldo'rnating (talaba ko'p kursga yoziladi, kursda ko'p talaba). Bitta talabani uchta kursga yozing va keyin bittasidan chiqaring (add,remove).- 7-mashqdagi M2M ga
throughmodel qo'shing:Enrollment(grademaydoni bilan). Talabani kursgathrough_defaultsorqali baho bilan yozing. Profile(OneToOneField->User) modeli yarating. Profili yo'q foydalanuvchidauser.profilega murojaat qilsangiz qanday xato chiqadi? Unihasattrbilan xavfsiz tekshiradigan kod yozing.Post.objects.filter(tags__name__in=["a", "b"])so'rovi nega ba'zan dublikat qaytaradi va buni qanday tuzatasiz?- Hamma postlarni muallifi bilan chiqaradigan siklni
select_relatedsiz va bilan yozing.CaptureQueriesContextorqali ikkala holatda so'rovlar sonini o'lchang. Comment.postmaydonidaon_delete=models.PROTECTturibdi. Izohi bor postni o'chirishga uringanda nima bo'ladi? Bunitry/exceptbilan ushlaydigan kod yozing.
Qiyin¶
- Bitta
forsiklida har bir postning ham muallifini (FK), ham teglarini (M2M) ishlatadigan kod yozing. Uni eng kam so'rov soniga optimallashtiring (maslahat:select_relatedvaprefetch_relatedni zanjir qiling) vaCaptureQueriesContextbilan isbotlang. User.objects.annotate(...)ishlatib, har bir foydalanuvchini posti soni bilan birga, bitta so'rovda chiqaring. Postga ega bo'lmagan foydalanuvchilar uchun son0bo'lishini tekshiring.- Quyidagi senariy uchun
on_deleteni loyihalang va sababini yozing:Order(buyurtma)Customer(mijoz)ga bog'langan;OrderItem(buyurtma qatori)Orderga bog'langan;OrderItemProductga ham bog'langan. Mijoz o'chsa buyurtmalari ham o'chsin; buyurtma o'chsa qatorlar ham o'chsin; lekin sotuvda ishlatilgan mahsulotni o'chirishga yo'l qo'yilmasin. Har bir FK uchun moson_deleteni tanlang.
Yechimlar
1.
class Author(models.Model):
name = models.CharField(max_length=100)
class Book(models.Model):
title = models.CharField(max_length=200)
# ForeignKey HAR DOIM "ko'p" tomonda turadi, ya'ni Book'da.
# Bitta Author -> ko'p Book = one-to-many.
author = models.ForeignKey(Author, on_delete=models.CASCADE)
2.
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
# Foydalanish:
author = Author.objects.get(name="Oqil")
author.books.all() # shu muallifning hamma kitoblari
author.books.count()
3. related_name bermasangiz, standart nom <model>_set bo'ladi:
4. Foydalanuvchi o'chirilsa, unga bog'langan hamma postlar ham avtomatik o'chadi (CASCADE β birga o'chish).
5. null=True majburiy. Sababi: SET_NULL ota o'chganda FK maydonini NULL ga o'rnatadi, demak bazaning ustuni NULL qabul qilishi shart. null=True bo'lmasa Django makemigrations paytida xato beradi.
6. post.author β User obyektini olib kelish uchun qo'shimcha baza so'rovi yuborishi mumkin (agar select_related bilan oldindan yuklanmagan bo'lsa). post.author_id esa allaqachon Post qatorida mavjud, qo'shimcha so'rov yubormaydi β faqat ID kerak bo'lganda shuni ishlating.
7.
class Student(models.Model):
name = models.CharField(max_length=100)
class Course(models.Model):
title = models.CharField(max_length=200)
students = models.ManyToManyField(Student, related_name="courses", blank=True)
# Foydalanish:
s = Student.objects.create(name="Ali")
c1 = Course.objects.create(title="Python")
c2 = Course.objects.create(title="Django")
c3 = Course.objects.create(title="SQL")
s.courses.add(c1, c2, c3) # 3 kursga yozildi
print(s.courses.count()) # 3
s.courses.remove(c2) # Django'dan chiqdi
print(s.courses.count()) # 2
8.
class Course(models.Model):
title = models.CharField(max_length=200)
students = models.ManyToManyField(
Student, through="Enrollment", related_name="courses"
)
class Enrollment(models.Model):
student = models.ForeignKey(Student, on_delete=models.CASCADE)
course = models.ForeignKey(Course, on_delete=models.CASCADE)
grade = models.CharField(max_length=2, blank=True)
# Foydalanish:
c.students.add(s, through_defaults={"grade": "A"})
# yoki:
Enrollment.objects.create(student=s, course=c, grade="A")
print(Enrollment.objects.get(student=s, course=c).grade) # "A"
9.
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
bio = models.TextField(blank=True)
# Profili yo'q user'da:
u = User.objects.create_user("yangi")
# u.profile -> Profile.DoesNotExist xatosi ko'tariladi
# Xavfsiz tekshiruv:
if hasattr(u, "profile"):
print(u.profile.bio)
else:
print("Profil hali yaratilmagan")
Profile.DoesNotExist (RelatedObjectDoesNotExist).
10. M2M bo'ylab filtr bazada JOIN qiladi. Agar bir post ikkala tegga ham ega bo'lsa, JOIN natijasida u har bir mos teg uchun bir martadan, ya'ni ikki marta qaytadi. Tuzatish β .distinct():
11.
from django.db import connection
from django.test.utils import CaptureQueriesContext
# N+1 (yomon):
with CaptureQueriesContext(connection) as ctx:
for post in Post.objects.all():
_ = post.author.username
print("select_related siz:", len(ctx.captured_queries)) # masalan 6
# Optimallashtirilgan:
with CaptureQueriesContext(connection) as ctx:
for post in Post.objects.select_related("author"):
_ = post.author.username
print("select_related bilan:", len(ctx.captured_queries)) # 1
12.
from django.db.models.deletion import ProtectedError
post = Post.objects.create(title="Muhim", author=user)
Comment.objects.create(post=post, text="izoh")
try:
post.delete()
except ProtectedError:
print("Postni o'chirib bo'lmaydi: bog'langan izohlar bor")
# Post o'chmaydi, ProtectedError ko'tariladi.
13.
from django.db import connection
from django.test.utils import CaptureQueriesContext
with CaptureQueriesContext(connection) as ctx:
qs = Post.objects.select_related("author").prefetch_related("tags")
for post in qs:
_ = post.author.username # FK -> select_related (JOIN, qo'shimcha so'rovsiz)
_ = list(post.tags.all()) # M2M -> prefetch_related (1 qo'shimcha so'rov)
print("Jami so'rovlar:", len(ctx.captured_queries))
# Natija: 2 ta (1 ta postlar+author JOIN, 1 ta hamma teglar) β post soniga bog'liq emas.
14.
from django.db.models import Count
qs = User.objects.annotate(post_soni=Count("posts"))
for u in qs:
print(u.username, u.post_soni)
# Count() LEFT JOIN ishlatadi, shuning uchun postsiz user uchun ham
# qator chiqadi va post_soni = 0 bo'ladi (NULL emas). Bitta so'rov.
15.
class Customer(models.Model):
name = models.CharField(max_length=100)
class Product(models.Model):
name = models.CharField(max_length=100)
class Order(models.Model):
# Mijoz o'chsa buyurtmalari ham o'chsin -> CASCADE
customer = models.ForeignKey(Customer, on_delete=models.CASCADE, related_name="orders")
class OrderItem(models.Model):
# Buyurtma o'chsa qatorlari ham o'chsin -> CASCADE
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items")
# Sotuvda ishlatilgan mahsulotni o'chirishga yo'l qo'yilmasin -> PROTECT
product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name="order_items")
quantity = models.PositiveIntegerField(default=1)
Order.customer = CASCADE: buyurtma mijozsiz mantiqsiz, shuning uchun birga o'chadi.
- OrderItem.order = CASCADE: qator buyurtmaning bir qismi, buyurtma o'chsa keraksiz.
- OrderItem.product = PROTECT: mahsulot tarix uchun muhim; sotilgan mahsulotni o'chirsak buyurtma qatori "yetim" qoladi va hisobotlar buziladi. PROTECT bunga yo'l qo'ymaydi.
β¬ οΈ Oldingi: 06 β ORM va QuerySet so'rovlar Β· π README Β· Keyingi: 08 β Admin panel β‘οΈ