09 β Ilg'or ORM va optimizatsiya¶
β¬ οΈ Oldingi: 08 β Admin panel Β· π README Β· Keyingi: 10 β Formalar va validatsiya β‘οΈ
Bu bobda: Django ORM'ning kuchli tomonlarini chuqur o'rganamiz.
aggregatebilan butun jadval bo'yicha yig'indi/o'rtacha/maksimum hisoblaymiz;annotatebilan har bir qatorga hisoblangan ustun qo'shamiz (GROUP BY);F()obyektlari yordamida bazada to'g'ridan-to'g'ri, atomik (race condition'siz) yangilash qilamiz va ustunlarni o'zaro taqqoslaymiz;Q()obyektlari bilanOR,AND,NOTmurakkab filtrlarni quramiz; eng muhimi β N+1 muammosiniselect_relatedvaprefetch_relatedbilan yechamiz va so'rovlar sonini aniq raqam bilan isbotlaymiz;transaction.atomicbilan "hammasi yoki hech narsa" mantig'ini ta'minlaymiz; oxirida o'z custom QuerySet va Manageringizni yozib, biznes mantig'ini chiroyli joyga ko'chiramiz. ORM aslida SQL generatori β har bo'limda u qanday SQL hosil qilishini ko'rsatamiz. Bu bob Python'ni bilishni nazarda tutadi (Python qo'llanma) va SQL asoslarini eslab turish foydali (SQL qo'llanma). Hamma kod Django 6.0.6 da haqiqatan ishga tushirib tekshirilgan.
Tayyorgarlik: misol modellar¶
Butun bob davomida bitta kichik internet-do'kon sxemasidan foydalanamiz. Quyidagi modellarni shop/models.py ga joylashtiramiz (custom Manager'ni keyinroq tushuntiramiz, hozircha shunchaki yozib qo'ying):
from django.db import models
class ProductQuerySet(models.QuerySet):
def mavjud(self):
return self.filter(faol=True, zaxira__gt=0)
def arzon(self, narx=100000):
return self.filter(narx__lt=narx)
class ProductManager(models.Manager):
def get_queryset(self):
return ProductQuerySet(self.model, using=self._db)
def mavjud(self):
return self.get_queryset().mavjud()
def arzon(self, narx=100000):
return self.get_queryset().arzon(narx)
class Category(models.Model):
nom = models.CharField(max_length=100)
def __str__(self):
return self.nom
class Product(models.Model):
nom = models.CharField(max_length=200)
category = models.ForeignKey(
Category, on_delete=models.CASCADE, related_name="mahsulotlar"
)
narx = models.DecimalField(max_digits=12, decimal_places=2)
zaxira = models.IntegerField(default=0)
faol = models.BooleanField(default=True)
korishlar = models.IntegerField(default=0)
objects = ProductManager()
def __str__(self):
return self.nom
class Customer(models.Model):
ism = models.CharField(max_length=100)
email = models.EmailField(unique=True)
balans = models.DecimalField(max_digits=12, decimal_places=2, default=0)
def __str__(self):
return self.ism
class Order(models.Model):
customer = models.ForeignKey(
Customer, on_delete=models.CASCADE, related_name="buyurtmalar"
)
sana = models.DateTimeField(auto_now_add=True)
tolangan = models.BooleanField(default=False)
class OrderItem(models.Model):
order = models.ForeignKey(
Order, on_delete=models.CASCADE, related_name="qatorlar"
)
product = models.ForeignKey(Product, on_delete=models.PROTECT)
soni = models.IntegerField(default=1)
narx = models.DecimalField(max_digits=12, decimal_places=2)
Tajriba qilish uchun eng qulay joy β Django shell:
Quyidagi misollar uchun bizda taxminan shunday ma'lumot bor: 2 ta kategoriya (Elektronika, Kitob), 4 ta mahsulot (Telefon 5 000 000 zaxira=10, Quloqchin 250 000 zaxira=0, Python kitobi 80 000 zaxira=50, Django kitobi 90 000 zaxira=5
faol=False), 3 ta mijoz (Ali, Vali, Hasan) va bir nechta buyurtma. Natijadagi raqamlar shu ma'lumotlarga mos.
1. aggregate β butun jadval bo'yicha bitta natija¶
aggregate() butun QuerySet'ni bitta lug'atga siqib qaytaradi. U SUM, AVG, MAX, MIN, COUNT kabi SQL agregat funksiyalarini ishlatadi va natija siz aylanib chiqadigan qatorlar emas, balki yakuniy raqamlar bo'ladi.
from django.db.models import Count, Sum, Avg, Max, Min
from shop.models import Product
# Avtomatik kalit nomlari: maydon__funksiya
Product.objects.aggregate(Sum("narx"), Avg("narx"), Max("narx"), Min("narx"), Count("id"))
Natija (haqiqiy chiqish):
{'narx__sum': Decimal('5420000'), 'narx__avg': Decimal('1355000'),
'narx__max': Decimal('5000000'), 'narx__min': Decimal('80000'), 'id__count': 4}
Kalit nomlarini o'zingiz belgilashingiz mumkin β bu tavsiya etiladi, chunki kod o'qishga qulay bo'ladi:
Product.objects.aggregate(jami=Sum("narx"), ortacha=Avg("narx"), soni=Count("id"))
# {'jami': Decimal('5420000'), 'ortacha': Decimal('1355000'), 'soni': 4}
aggregate ham odatdagi filter() bilan zanjirlanadi β avval filtrlaysiz, keyin yig'asiz:
# Faqat faol mahsulotlar zaxirasi yig'indisi
Product.objects.filter(faol=True).aggregate(jami=Sum("zaxira"))
# {'jami': 60}
Shartli agregatsiya (COUNT(... ) FILTER) β bitta so'rovda bir nechta shartli sanoq:
from django.db.models import Q
from shop.models import Order
Order.objects.aggregate(
tolangan_soni=Count("id", filter=Q(tolangan=True)),
tolanmagan_soni=Count("id", filter=Q(tolangan=False)),
)
# {'tolangan_soni': 2, 'tolanmagan_soni': 1}
β οΈ Tuzoq:
aggregate()aliasiga model maydoni bilan bir xil nom (tolangan=Count(...)) bersangiz, xato bermaydi β natija oddiy lug'at bo'lib qaytadi. Ammo aynan shu nomniannotate()da ishlatsangiz,ValueError: The annotation 'tolangan' conflicts with a field on the modelchiqadi, chunki annotatsiya har bir qatorga maydon kabi qo'shiladi. Chalkashlikni oldini olish uchun har ikki holatda hamtolangan_sonikabi alohida nom afzal.
2. annotate β har bir qatorga hisoblangan ustun¶
aggregate butun jadval uchun bitta raqam bersa, annotate har bir qatorga alohida hisoblangan qiymat qo'shadi. Ya'ni "har bir kategoriyada nechta mahsulot bor?" degan savolga aynan annotate javob beradi. SQL'da bu GROUP BY ga aylanadi.
from django.db.models import Count
from shop.models import Category
qs = Category.objects.annotate(mahsulot_soni=Count("mahsulotlar"))
for c in qs:
print(c.nom, "->", c.mahsulot_soni)
Natija:
Count("mahsulotlar") dagi mahsulotlar β bu Product.category da bergan related_name. Ya'ni biz teskari munosabat (kategoriyaga tegishli mahsulotlar) bo'ylab sanaymiz. Bu qaysi SQL'ga aylanishini ko'rsak:
SELECT "shop_category"."id", "shop_category"."nom", COUNT("shop_product"."id") AS "n"
FROM "shop_category"
LEFT OUTER JOIN "shop_product" ON ("shop_category"."id" = "shop_product"."category_id")
GROUP BY "shop_category"."id", "shop_category"."nom"
Bir nechta agregatni birga qo'shsa bo'ladi:
from django.db.models import Avg, Max
qs = Category.objects.annotate(
ortacha_narx=Avg("mahsulotlar__narx"),
eng_qimmat=Max("mahsulotlar__narx"),
)
for c in qs:
print(c.nom, "ortacha:", c.ortacha_narx, "max:", c.eng_qimmat)
# Elektronika ortacha: 2625000 max: 5000000
# Kitob ortacha: 85000 max: 90000
annotate dan keyin filter qo'ysangiz, u SQL HAVING ga aylanadi β ya'ni hisoblangan ustun bo'yicha filtrlash:
# 2 va undan ortiq mahsulotli kategoriyalar
Category.objects.annotate(n=Count("mahsulotlar")).filter(n__gte=2)
Mijozlarni buyurtma soni bo'yicha tartiblash:
from shop.models import Customer
Customer.objects.annotate(buyurtma_soni=Count("buyurtmalar")).order_by("-buyurtma_soni")
# Ali buyurtmalar: 2
# Vali buyurtmalar: 1
# Hasan buyurtmalar: 0
values().annotate() β guruhlash kaliti¶
Agar siz "obyekt qatorlari" emas, balki guruhlangan jadval (masalan, kategoriya bo'yicha jami narx) xohlasangiz, values() ni annotate() bilan birlashtiring. values() GROUP BY ning kalitini belgilaydi:
Product.objects.values("category__nom").annotate(
soni=Count("id"), jami=Sum("narx")
).order_by("category__nom")
# {'category__nom': 'Elektronika', 'soni': 2, 'jami': Decimal('5250000')}
# {'category__nom': 'Kitob', 'soni': 2, 'jami': Decimal('170000')}
Eslatma:
values()niannotate()dan oldin qo'ysangiz β u guruhlash kaliti bo'ladi.annotate()dan keyin qo'ysangiz β shunchaki tanlanadigan ustunlarni cheklaydi. Tartib muhim.
3. F() obyektlari β bazada to'g'ridan-to'g'ri ishlash¶
F() model maydoniga Python'ga olib kelmasdan murojaat qilish imkonini beradi. Bu ikki sababga ko'ra muhim: (1) atomiklik β race condition'ni oldini oladi; (2) tezlik β hisob baza ichida bajariladi.
Muammo: oddiy yangilash race condition'ga olib keladi¶
# β XAVFLI: ikki bosqich β o'qish va yozish orasida boshqa so'rov o'zgartirishi mumkin
p = Product.objects.get(nom="Python kitobi")
p.korishlar = p.korishlar + 1 # qiymat Python xotirasida hisoblanadi
p.save()
Agar bir vaqtda ikkita so'rov kelsa, ikkalasi ham eski qiymatni o'qiydi (masalan 10), ikkalasi ham 11 yozadi β bitta ko'rish yo'qoladi.
Yechim: F() bilan atomik yangilash¶
from django.db.models import F
from shop.models import Product
Product.objects.filter(nom="Python kitobi").update(korishlar=F("korishlar") + 1)
Bu bitta SQL so'rovga aylanadi va hisobni baza bajaradi:
Tekshirish:
p = Product.objects.get(nom="Python kitobi")
print(p.korishlar) # oldin: 0
Product.objects.filter(nom="Python kitobi").update(korishlar=F("korishlar") + 1)
p.refresh_from_db()
print(p.korishlar) # keyin: 1
update()dan keyin obyektning xotiradagi nusxasi eskiradi β yangi qiymatni ko'rish uchunp.refresh_from_db()chaqiring.
F() bilan ustunlararo taqqoslash va hisob¶
F() ni annotate ichida ham ishlatib, ikki ustunni ko'paytirish (masalan, ombor qiymati = narx Γ zaxira) mumkin:
qiymat = Product.objects.annotate(ombor_qiymati=F("narx") * F("zaxira"))
for x in qiymat:
print(x.nom, x.ombor_qiymati)
# Telefon 50000000
# Quloqchin 0
# Python kitobi 4000000
# Django kitobi 450000
F() ni filter ichida bir maydonni boshqa maydon bilan solishtirishga ham ishlatasiz. Masalan, "buyurtmadagi narx mahsulotning hozirgi narxidan farq qiladigan qatorlar":
from shop.models import OrderItem
OrderItem.objects.filter(narx__lt=F("product__narx"))
# (chegirma bilan sotilgan qatorlar)
4. Q() obyektlari β murakkab OR / AND / NOT filtrlar¶
Oddiy filter(a=1, b=2) har doim AND ni anglatadi. Lekin OR yoki NOT kerak bo'lganda Q() obyektlariga o'tasiz. Ular orasida operatorlar:
|βOR&βAND~βNOT
from django.db.models import Q
from shop.models import Product
# OR: arzon (narx < 100000) YOKI ko'p zaxira (> 20)
Product.objects.filter(Q(narx__lt=100000) | Q(zaxira__gt=20))
# ['Python kitobi', 'Django kitobi']
Bu quyidagi SQL'ga aylanadi:
SELECT ... FROM "shop_product"
WHERE ("shop_product"."narx" < 100000 OR "shop_product"."zaxira" > 20)
AND va NOT ni birga ishlatish:
# faol VA zaxirasi nol BO'LMAGAN
Product.objects.filter(Q(faol=True) & ~Q(zaxira=0))
# ['Telefon', 'Python kitobi']
Dinamik filtr qurish¶
Q() ning eng kuchli jihati β uni o'zgaruvchiga saqlab, bosqichma-bosqich qurish mumkin. Bu qidiruv formalaridagi ixtiyoriy filtrlar uchun ajoyib:
shart = Q(category__nom="Kitob")
shart |= Q(narx__gt=1000000) # endi: kitob YOKI 1 mln dan qimmat
Product.objects.filter(shart)
# ['Telefon', 'Python kitobi', 'Django kitobi']
Amaliy misol β qidiruv:
def mahsulot_qidir(soz=None, min_narx=None, faqat_mavjud=False):
shart = Q()
if soz:
shart &= Q(nom__icontains=soz)
if min_narx is not None:
shart &= Q(narx__gte=min_narx)
if faqat_mavjud:
shart &= Q(zaxira__gt=0)
return Product.objects.filter(shart)
Bo'sh Q() hech narsani cheklamaydi, shuning uchun hech qaysi parametr berilmasa hamma narsa qaytadi.
5. N+1 muammosi va select_related¶
Bu bobning eng muhim amaliy qismi. N+1 muammosi β bu sodir bo'ladigan eng keng tarqalgan ishlash xatosi. U shunday yuz beradi: siz N ta obyektni olasiz, keyin sikl ichida har biri uchun bog'liq obyektni olishga yana bittadan so'rov yuborasiz.
So'rovlar sonini aniq sanab ko'ramiz. Django'da connection.queries (faqat DEBUG=True bo'lganda to'ladi) va reset_queries() shu uchun bor:
from django.db import connection, reset_queries
# β N+1: har bir item uchun product alohida olinadi
reset_queries()
for item in OrderItem.objects.all():
_ = item.product.nom
print(len(connection.queries), "ta query") # 5 ta query (1 + 4 qator)
Endi select_related bilan β u SQL JOIN qilib, hamma narsani bitta so'rovda olib keladi:
# β
select_related: bitta JOIN'li so'rov
reset_queries()
for item in OrderItem.objects.select_related("product", "order__customer"):
_ = item.product.nom
_ = item.order.customer.ism
print(len(connection.queries), "ta query") # 1 ta query
Natija β 5 ta query'dan 1 ta query'ga. select_related ko'rsatadigan SQL (qisqartirilgan):
SELECT "shop_orderitem".*, "shop_product".*
FROM "shop_orderitem"
INNER JOIN "shop_product" ON ("shop_orderitem"."product_id" = "shop_product"."id")
Qoida:
select_relatedfaqat ForeignKey va OneToOne uchun ishlaydi (ya'ni "bitta" tomonga). UJOINishlatadi, shuning uchun bitta so'rov qoladi.order__customerkabi ikki bosqichli yo'lni ham qo'llab-quvvatlaydi.
6. prefetch_related β "ko'pchilik" tomon uchun¶
select_related JOIN ishlatgani uchun ko'plik munosabati (teskari ForeignKey yoki ManyToMany) bilan ishlamaydi β bunda har bir ota uchun bir nechta bola bo'ladi va JOIN qatorlarni takrorlaydi. Bunday hollarda prefetch_related ishlatasiz: u alohida so'rov yuborib, natijalarni Python'da birlashtiradi.
from shop.models import Category
from django.db import connection, reset_queries
# β Har bir kategoriya uchun mahsulotlar alohida olinadi
reset_queries()
for c in Category.objects.all():
_ = list(c.mahsulotlar.all())
print(len(connection.queries), "ta query") # 3 ta query (1 + 2 kategoriya)
# β
prefetch_related: doimiy 2 ta so'rov
reset_queries()
for c in Category.objects.prefetch_related("mahsulotlar"):
_ = list(c.mahsulotlar.all())
print(len(connection.queries), "ta query") # 2 ta query
prefetch_related har doim doimiy sonli so'rov beradi: bitta asosiy obyektlar uchun, yana bittasi bog'liq obyektlar uchun (WHERE category_id IN (...)). Kategoriyalar 2 ta bo'lsin yoki 2000 ta β baribir 2 ta so'rov.
Prefetch bilan moslashtirilgan so'rov¶
Ba'zan oldindan yuklanadigan bog'liq obyektlarni filtrlash yoki tartiblash kerak. Buning uchun Prefetch obyektidan foydalanasiz:
from django.db.models import Prefetch
from shop.models import Category, Product
# Faqat mavjud mahsulotlarni oldindan yuklab kelamiz
kat = Category.objects.prefetch_related(
Prefetch("mahsulotlar", queryset=Product.objects.mavjud())
).first()
list(kat.mahsulotlar.all()) # endi bu yerda faqat mavjud mahsulotlar
Qisqa qoida:
| Munosabat | Vosita | Qancha so'rov |
|---|---|---|
| ForeignKey / OneToOne ("bitta" tomon) | select_related |
1 (JOIN) |
| Teskari FK / ManyToMany ("ko'p" tomon) | prefetch_related |
2 (alohida + IN) |
Node.js'da Prisma yoki Sequelize'dagi
include/eager loading ham aynan shu muammoni yechadi β solishtirish uchun Node.js qo'llanmaga qarang.
7. transaction.atomic β "hammasi yoki hech narsa"¶
Ba'zi amallar bir nechta yozuvni o'zgartiradi va ular bo'linmas bo'lishi shart. Klassik misol β pul o'tkazmasi: bir hisobdan ayirib, ikkinchisiga qo'shasiz. Agar yarmida xato chiqsa, pul "havoda yo'qolib qolmasligi" kerak. transaction.atomic aynan shuni ta'minlaydi: blok ichidagi hamma narsa commit bo'ladi yoki birortasi xato bersa rollback (hammasi bekor) bo'ladi.
from decimal import Decimal
from django.db import transaction
from django.db.models import F
from shop.models import Customer
@transaction.atomic
def pul_otkaz(kimdan_id, kimga_id, summa):
# select_for_update: qatorni so'rov davomida qulflab turadi (race'ni oldini oladi)
kimdan = Customer.objects.select_for_update().get(pk=kimdan_id)
kimga = Customer.objects.select_for_update().get(pk=kimga_id)
kimdan.balans = F("balans") - summa
kimga.balans = F("balans") + summa
kimdan.save()
kimga.save()
Ishlatish:
ali = Customer.objects.get(ism="Ali") # balans: 1000000
vali = Customer.objects.get(ism="Vali") # balans: 500000
pul_otkaz(ali.pk, vali.pk, Decimal("200000"))
ali.refresh_from_db(); vali.refresh_from_db()
print(ali.balans, vali.balans) # 800000.00 700000.00
@transaction.atomic dekoratorni kontekst-menejer sifatida ham ishlatish mumkin. Endi rollbackni isbotlaymiz β blok ichida ataylab xato chiqaramiz:
try:
with transaction.atomic():
Customer.objects.filter(pk=ali.pk).update(balans=F("balans") - Decimal("100000"))
raise ValueError("xato!") # blok yarmida portladi
except ValueError:
pass
ali.refresh_from_db()
print(ali.balans) # 800000.00 β o'zgarmadi! Rollback ishladi.
UPDATE bajarilgan bo'lsa-da, exception tufayli butun bog'lam bekor qilindi va balans eski holatda qoldi.
Eslatma:
select_for_update()qator-darajasidagi qulf (SELECT ... FOR UPDATE) ishlatadi va transaction ichida bo'lishi shart. U PostgreSQL/MySQL'da to'liq ishlaydi; SQLite'da bu so'rov xatosiz qabul qilinadi, ammo SQLite butun bazani qulflagani uchun amalda haqiqiy parallel qulflashni boshqa baza beradi. Ishlab chiqarishda PostgreSQL tavsiya etiladi (SQL qo'llanma).
8. Custom QuerySet va Manager β biznes mantig'ini bir joyga¶
Vaqt o'tib, kod butun loyiha bo'ylab Product.objects.filter(faol=True, zaxira__gt=0) ni qayta-qayta yozayotganini ko'rasiz. Bu mo'rt va takrorlanuvchi. Yechim β bu mantiqni custom QuerySet ichidagi metodga ko'chirish, keyin uni Manager orqali ochish.
Bizning bob boshidagi modelda allaqachon shunday yozilgan edi. Uni qayta ko'rib chiqaylik:
from django.db import models
class ProductQuerySet(models.QuerySet):
def mavjud(self):
return self.filter(faol=True, zaxira__gt=0)
def arzon(self, narx=100000):
return self.filter(narx__lt=narx)
class ProductManager(models.Manager):
def get_queryset(self):
return ProductQuerySet(self.model, using=self._db)
def mavjud(self):
return self.get_queryset().mavjud()
def arzon(self, narx=100000):
return self.get_queryset().arzon(narx)
Endi mantiq bitta joyda yashaydi va o'qishga oson:
from shop.models import Product
Product.objects.mavjud() # ['Telefon', 'Python kitobi']
Product.objects.arzon() # ['Python kitobi', 'Django kitobi']
Product.objects.mavjud().arzon(100000) # zanjir: ['Python kitobi']
E'tibor bering β mavjud() QuerySet qaytargani uchun uni arzon() bilan zanjirlash mumkin. Bu QuerySet metodlarining kuchi.
Qisqaroq yo'l: QuerySet.as_manager()¶
Yuqorida mavjud/arzon metodlarini ikki marta (QuerySet'da va Manager'da) yozdik. Buni as_manager() bilan bir martaga qisqartirish mumkin β metodlar QuerySet'da bir marta yoziladi va Manager avtomatik yaratiladi:
class ProductQuerySet(models.QuerySet):
def mavjud(self):
return self.filter(faol=True, zaxira__gt=0)
def arzon(self, narx=100000):
return self.filter(narx__lt=narx)
class Product(models.Model):
# ... maydonlar ...
objects = ProductQuerySet.as_manager()
Endi Product.objects.mavjud().arzon() xuddi shunday ishlaydi, lekin kod ikki barobar qisqa. Boshlovchi sifatida as_manager() ni afzal ko'ring; faqat Manager'ga maxsus mantiq (masalan, get_queryset() ni butunlay o'zgartirish) kerak bo'lganda to'liq Manager yozasiz.
Coalesce β NULL ni standart qiymatga¶
Agregat hech qatorni topmasa NULL (Python'da None) qaytaradi. Buni 0 ga aylantirish uchun Coalesce ishlatamiz β masalan, har bir mijozning jami xaridi (xarid qilmagan mijoz uchun 0):
from decimal import Decimal
from django.db.models import Sum, DecimalField
from django.db.models.functions import Coalesce
from shop.models import Customer
Customer.objects.annotate(
jami_xarid=Coalesce(
Sum("buyurtmalar__qatorlar__narx"), Decimal("0"), output_field=DecimalField()
)
)
# Ali jami: 5160000
# Hasan jami: 0 <- buyurtmasiz mijoz uchun None emas, 0
# Vali jami: 80000
9. So'rovlarni testda hisoblash (N+1 ni qaytarib kelmaslik)¶
Eng yaxshi tomoni β so'rovlar sonini avtomatik test bilan qotirib qo'yish mumkin. TestCase.assertNumQueries(N) blok aynan N ta so'rov yuborganini tekshiradi. Agar kelajakda kimdir select_related ni olib tashlasa, test darhol qulaydi.
from decimal import Decimal
from django.test import TestCase
from django.db.models import Count, Sum, F, Prefetch
from shop.models import Category, Product, Customer, Order, OrderItem
class OrmTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.kat = Category.objects.create(nom="Kitob")
cls.p1 = Product.objects.create(nom="Python", category=cls.kat, narx=Decimal("80000"), zaxira=10)
cls.p2 = Product.objects.create(nom="Django", category=cls.kat, narx=Decimal("90000"), zaxira=0)
cls.mijoz = Customer.objects.create(ism="Ali", email="a@a.uz")
cls.order = Order.objects.create(customer=cls.mijoz)
OrderItem.objects.create(order=cls.order, product=cls.p1, soni=2, narx=cls.p1.narx)
def test_aggregate(self):
self.assertEqual(Product.objects.aggregate(jami=Sum("narx"))["jami"], Decimal("170000"))
def test_f_atomik(self):
Product.objects.filter(pk=self.p1.pk).update(korishlar=F("korishlar") + 5)
self.p1.refresh_from_db()
self.assertEqual(self.p1.korishlar, 5)
def test_custom_manager(self):
self.assertEqual(Product.objects.mavjud().count(), 1)
def test_select_related_n_plus_1(self):
# Faqat 1 ta query bo'lishini qotirib qo'yamiz
with self.assertNumQueries(1):
for item in OrderItem.objects.select_related("product"):
_ = item.product.nom
def test_prefetch_related(self):
with self.assertNumQueries(2):
for c in Category.objects.prefetch_related("mahsulotlar"):
_ = list(c.mahsulotlar.all())
Ishga tushirish:
Natija (haqiqiy chiqish, qisqartirilgan):
.....
----------------------------------------------------------------------
Ran 5 tests in 0.008s
OK
Testlar bo'yicha to'liqroq ma'lumot keyingi boblarda. CI'da bu testlarni avtomatik ishga tushirish haqida Git va GitHub qo'llanmada o'qing.
Mashqlar¶
Mashqlarni bob modellarida (
Category,Product,Customer,Order,OrderItem) bajaring. Har birinipython manage.py shellda yoki test sifatida sinab ko'ring.
Oson¶
- Barcha mahsulotlar narxining o'rtachasini bitta
aggregateso'rovi bilan toping va natija kalitiniortacha_narxdeb nomlang. - Har bir kategoriyada nechta mahsulot borligini
annotatebilan hisoblang va kategoriya nomi bilan birga chop eting. F()ishlatib,pk=1bo'lgan mahsulotningkorishlarmaydonini atomik ravishda 1 ga oshiring.Q()bilan narxi 100 000 dan arzon YOKI zaxirasi 20 dan ko'p mahsulotlarni filtrlang.- Faqat
faol=Truemahsulotlar soniniaggregate(Count(...))bilan toping. Product.objects.values("category__nom").annotate(...)ishlatib, har bir kategoriya bo'yicha jami narxni guruhlab chiqaring.
O'rta¶
aggregateichidaCount("id", filter=Q(...))bilan to'langan va to'lanmagan buyurtmalar sonini bitta so'rovda hisoblang. Alias nomlari model maydoni bilan to'qnashmasin.OrderItemlarni aylanib chiqib, har biriningproduct.nomini chop etadigan kodniselect_relatedbilan va siz yozing;connection.queriesorqali so'rovlar sonini ikkala holatda solishtiring.prefetch_relatedishlatib, har bir kategoriya uchun mahsulotlar ro'yxatini olib keling vareset_queries()bilan doimiy 2 ta so'rov bo'lishini isbotlang.annotatevaF()ni birlashtirib, har bir mahsulot uchunombor_qiymati = narx * zaxirahisoblang va natijani kamayish tartibida saralang.Coalesceishlatib, har bir mijozning jami xaridini hisoblang; xarid qilmagan mijoz uchun natija0bo'lsin (Noneemas).ProductuchunProductQuerySet.as_manager()yondashuvini qo'llab,mavjud()vaqimmat(narx=1000000)metodlarini yozing vaProduct.objects.mavjud().qimmat()zanjirini sinab ko'ring.
Qiyin¶
transaction.atomicichida ikkita mijoz o'rtasida pul o'tkazadigan funksiya yozing. Yetarli mablag' bo'lmasaValueErrorchiqarib, rollback ishlashini test bilan isbotlang (balans o'zgarmaganini tekshiring).F()vaselect_for_update()ishlating.assertNumQueriesishlatadiganTestCaseyozing: bitta testselect_related1 ta so'rovni, ikkinchisiprefetch_related2 ta so'rovni ta'minlashini qotirib qo'ysin.- Dinamik qidiruv funksiyasi yozing:
mahsulot_qidir(soz=None, min_narx=None, max_narx=None, faqat_mavjud=False).Q()ni bosqichma-bosqich quring; berilmagan parametrlar filtrga ta'sir qilmasin.
Yechimlar
Oson¶
1.
from django.db.models import Avg
from shop.models import Product
Product.objects.aggregate(ortacha_narx=Avg("narx"))
# {'ortacha_narx': Decimal('1355000')}
2.
from django.db.models import Count
from shop.models import Category
for c in Category.objects.annotate(n=Count("mahsulotlar")):
print(c.nom, c.n)
# Elektronika 2
# Kitob 2
3.
from django.db.models import F
from shop.models import Product
Product.objects.filter(pk=1).update(korishlar=F("korishlar") + 1)
4.
from django.db.models import Q
from shop.models import Product
Product.objects.filter(Q(narx__lt=100000) | Q(zaxira__gt=20))
5.
from django.db.models import Count
from shop.models import Product
Product.objects.filter(faol=True).aggregate(soni=Count("id"))
# {'soni': 3}
6.
from django.db.models import Sum
from shop.models import Product
Product.objects.values("category__nom").annotate(jami=Sum("narx")).order_by("category__nom")
# {'category__nom': 'Elektronika', 'jami': Decimal('5250000')}
# {'category__nom': 'Kitob', 'jami': Decimal('170000')}
O'rta¶
7.
from django.db.models import Count, Q
from shop.models import Order
Order.objects.aggregate(
tolangan_soni=Count("id", filter=Q(tolangan=True)),
tolanmagan_soni=Count("id", filter=Q(tolangan=False)),
)
# {'tolangan_soni': 2, 'tolanmagan_soni': 1}
8.
from django.db import connection, reset_queries
from shop.models import OrderItem
# select_related SIZ
reset_queries()
for item in OrderItem.objects.all():
_ = item.product.nom
print("siz:", len(connection.queries)) # 5
# select_related BILAN
reset_queries()
for item in OrderItem.objects.select_related("product"):
_ = item.product.nom
print("bilan:", len(connection.queries)) # 1
connection.queriesfaqatDEBUG=Trueda to'ladi.
9.
from django.db import connection, reset_queries
from shop.models import Category
reset_queries()
for c in Category.objects.prefetch_related("mahsulotlar"):
_ = list(c.mahsulotlar.all())
print(len(connection.queries)) # 2 (kategoriyalar soni qancha bo'lsa ham 2)
10.
from django.db.models import F
from shop.models import Product
qs = Product.objects.annotate(ombor_qiymati=F("narx") * F("zaxira")).order_by("-ombor_qiymati")
for x in qs:
print(x.nom, x.ombor_qiymati)
# Telefon 50000000
# Python kitobi 4000000
# Django kitobi 450000
# Quloqchin 0
11.
from decimal import Decimal
from django.db.models import Sum, DecimalField
from django.db.models.functions import Coalesce
from shop.models import Customer
qs = Customer.objects.annotate(
jami_xarid=Coalesce(Sum("buyurtmalar__qatorlar__narx"), Decimal("0"), output_field=DecimalField())
)
for m in qs:
print(m.ism, m.jami_xarid)
# Ali 5160000 / Hasan 0 / Vali 80000
12.
from django.db import models
class ProductQuerySet(models.QuerySet):
def mavjud(self):
return self.filter(faol=True, zaxira__gt=0)
def qimmat(self, narx=1000000):
return self.filter(narx__gt=narx)
class Product(models.Model):
# ... maydonlar ...
objects = ProductQuerySet.as_manager()
# Foydalanish
Product.objects.mavjud().qimmat() # zanjirlash ishlaydi
Qiyin¶
13.
from decimal import Decimal
from django.db import transaction
from django.db.models import F
from django.test import TestCase
from shop.models import Customer
class MablagYetarli(Exception):
pass
@transaction.atomic
def pul_otkaz(kimdan_id, kimga_id, summa):
kimdan = Customer.objects.select_for_update().get(pk=kimdan_id)
kimga = Customer.objects.select_for_update().get(pk=kimga_id)
if kimdan.balans < summa:
raise ValueError("Mablag' yetarli emas")
kimdan.balans = F("balans") - summa
kimga.balans = F("balans") + summa
kimdan.save()
kimga.save()
class TransferTest(TestCase):
def setUp(self):
self.a = Customer.objects.create(ism="A", email="a@a.uz", balans=Decimal("100"))
self.b = Customer.objects.create(ism="B", email="b@b.uz", balans=Decimal("0"))
def test_rollback_mablag_yetmasa(self):
with self.assertRaises(ValueError):
pul_otkaz(self.a.pk, self.b.pk, Decimal("999"))
self.a.refresh_from_db()
self.b.refresh_from_db()
# Balanslar o'zgarmadi β rollback ishladi
self.assertEqual(self.a.balans, Decimal("100"))
self.assertEqual(self.b.balans, Decimal("0"))
def test_muvaffaqiyatli(self):
pul_otkaz(self.a.pk, self.b.pk, Decimal("40"))
self.a.refresh_from_db()
self.b.refresh_from_db()
self.assertEqual(self.a.balans, Decimal("60"))
self.assertEqual(self.b.balans, Decimal("40"))
14.
from decimal import Decimal
from django.test import TestCase
from shop.models import Category, Product, Order, OrderItem, Customer
class QuerySonTest(TestCase):
@classmethod
def setUpTestData(cls):
kat = Category.objects.create(nom="Kitob")
p = Product.objects.create(nom="Py", category=kat, narx=Decimal("100"), zaxira=5)
m = Customer.objects.create(ism="Ali", email="a@a.uz")
o = Order.objects.create(customer=m)
OrderItem.objects.create(order=o, product=p, soni=1, narx=p.narx)
def test_select_related_bitta(self):
with self.assertNumQueries(1):
for it in OrderItem.objects.select_related("product"):
_ = it.product.nom
def test_prefetch_ikkita(self):
with self.assertNumQueries(2):
for c in Category.objects.prefetch_related("mahsulotlar"):
_ = list(c.mahsulotlar.all())
15.
from django.db.models import Q
from shop.models import Product
def mahsulot_qidir(soz=None, min_narx=None, max_narx=None, faqat_mavjud=False):
shart = Q()
if soz:
shart &= Q(nom__icontains=soz)
if min_narx is not None:
shart &= Q(narx__gte=min_narx)
if max_narx is not None:
shart &= Q(narx__lte=max_narx)
if faqat_mavjud:
shart &= Q(faol=True, zaxira__gt=0)
return Product.objects.filter(shart)
# Misollar:
mahsulot_qidir() # hammasi (bo'sh Q hech narsani cheklamaydi)
mahsulot_qidir(soz="kitob") # nomida "kitob" borlar
mahsulot_qidir(max_narx=100000, faqat_mavjud=True)
β¬ οΈ Oldingi: 08 β Admin panel Β· π README Β· Keyingi: 10 β Formalar va validatsiya β‘οΈ