19 β Signallar va custom mantiq¶
β¬ οΈ Oldingi: 18 β DRF filtrlash, paginatsiya, nested Β· π README Β· Keyingi: 20 β Keshlash va performance β‘οΈ
Bu bobda: Django'ning eng "sehrli" ko'ringan, lekin aslida juda oddiy mexanizmi β signallar bilan tanishamiz. Signal β bu "biror narsa sodir bo'lganda menga xabar ber" degan obuna tizimi.
pre_save,post_save,pre_deletevam2m_changedsignallarini batafsil o'rganamiz;@receiverdekoratori bilan qabul qiluvchi (receiver) funksiya yozamiz;created,update_fields,pk_setkabi argumentlardan to'g'ri foydalanamiz; cheksiz tsikl (infinite loop) tuzog'idan qochamiz. Eng muhimi β qachon signal ishlatish kerak va qachon kerak emas degan savolga aniq javob beramiz (ko'pchilik signalni keragidan ko'p ishlatadi). Keyin signalga muqobil β toza, ochiq mantiq: custom model metodlari va@property, customManagervaQuerySet(so'rov mantig'ini bitta joyga yig'ish,as_manager()vafrom_queryset()qisqa yo'llari). Va nihoyat β signallarni qayerda ulash kerak:AppConfig.ready(). O'zingizning custom signalingizni ham yasab,Signal()vasend()bilan ishlatamiz. Bu bob Python'ni bilishni nazarda tutadi (Python qo'llanma). Hamma kod Django 6.0.6, Python 3.14 da haqiqatan ishga tushirib tekshirilgan.
Signal nima va nega kerak?¶
Tasavvur qiling: yangi foydalanuvchi ro'yxatdan o'tganda unga avtomatik profil yaratmoqchisiz. Eng sodda yo'l β User yaratadigan har bir joyda qo'lda Profil.objects.create(...) yozish. Lekin foydalanuvchi ko'p joyda yaratiladi: ro'yxatdan o'tish formasi, admin panel, createsuperuser buyrug'i, test fixture'lari... Har biriga profil yaratish kodini qo'shib chiqasizmi? Bittasini unutsangiz β xato.
Signal aynan shu muammoni yechadi: "User saqlanganda, qayerda saqlanishidan qat'i nazar, menga xabar ber β men profil yarataman". Bu β publish/subscribe (nashr/obuna) naqshi. Django ichidagi modellar (jo'natuvchi) signal jo'natadi, sizning funksiyangiz (qabul qiluvchi) unga obuna bo'ladi.
Eng muhim tushuncha: jo'natuvchi qabul qiluvchini bilmaydi. User modeli "kimdir mening saqlanishimni kuzatyaptimi?" deb so'ramaydi β u shunchaki signal chiqaradi. Bu bo'shashtirilgan aloqa (loose coupling): bitta app'ni o'zgartirmasdan boshqa app unga reaksiya qo'shishi mumkin. Aynan shu kuch ham, xavf ham β chunki mantiq "ko'rinmas" joyda yashiringan bo'ladi.
Django'da tayyor (built-in) model signallari:
| Signal | Qachon | Asosiy argumentlar |
|---|---|---|
pre_save |
save() da, bazaga yozishdan oldin |
instance |
post_save |
bazaga yozilgandan keyin | instance, created, update_fields |
pre_delete |
delete() da, o'chirishdan oldin |
instance |
post_delete |
o'chirilgandan keyin | instance |
m2m_changed |
ManyToMany maydon o'zgarganda | action, pk_set, reverse |
Eslatma:
pre_save/post_savefaqatModel.save()chaqirilganda ishlaydi.Post.objects.filter(...).update(...)vabulk_create()ularni chaqirmaydi β bu bo'limning oxirida muhim ogohlantirish bor.
Birinchi signal: post_save bilan profil yaratish¶
Avval ishlaydigan modellarni yozamiz. Bu bob davomida kichik blog sxemasidan foydalanamiz: Post, Teg, har bir User uchun Profil, va o'zgarishlarni yozib boruvchi AuditLog.
shop/models.py:
from django.conf import settings
from django.db import models
from django.utils import timezone
class Post(models.Model):
HOLATLAR = [("qoralama", "Qoralama"), ("chop", "Chop etilgan")]
muallif = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="postlar"
)
sarlavha = models.CharField(max_length=200)
matn = models.TextField(blank=True)
holat = models.CharField(max_length=10, choices=HOLATLAR, default="qoralama")
chop_sana = models.DateTimeField(null=True, blank=True)
soz_soni = models.PositiveIntegerField(default=0, editable=False)
teglar = models.ManyToManyField("Teg", blank=True, related_name="postlar")
yaratilgan = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.sarlavha
class Teg(models.Model):
nom = models.CharField(max_length=50, unique=True)
def __str__(self):
return self.nom
class Profil(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profil"
)
bio = models.TextField(blank=True)
def __str__(self):
return f"{self.user.username} profili"
class AuditLog(models.Model):
xabar = models.CharField(max_length=255)
vaqt = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.xabar
settings.AUTH_USER_MODELdan foydalanish β yaxshi odat. To'g'ridan-to'g'rifrom django.contrib.auth.models import Useryozish ham ishlaydi, lekin loyihada custom user model bo'lsasettings.AUTH_USER_MODELmoslashuvchan qoladi.
Endi signallarni alohida faylga β shop/signals.py ga yozamiz. Nima uchun alohida fayl? Chunki signallarni models.py ga yozsangiz, fayl shishadi va import tartibida muammo chiqishi mumkin. Konvensiya: signals.py.
shop/signals.py:
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Profil
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def profil_yarat(sender, instance, created, **kwargs):
if created:
Profil.objects.create(user=instance)
Bu funksiyani diqqat bilan o'qiymiz:
@receiver(post_save, sender=...)β "post_savesignalini, faqatUserjo'natganda eshit" degani.sender=bo'lmasa β har qanday model saqlanganda ishlaydi (deyarli har doim xato).senderβ signalni kim jo'natdi (bu yerdaUserklassi).instanceβ saqlangan aniq obyekt (haqiqiyUserqatori).createdβTruebo'lsa, yangi qator yaratildi (INSERT);Falsebo'lsa, mavjud qator yangilandi (UPDATE). Profilni faqat yangi user uchun yaratamiz, shuning uchunif created:shart.**kwargsβ Django signalga boshqa argumentlar ham yuboradi (using,update_fields,raw,signal). Hammasini yozish shart emas, lekin**kwargsni har doim qo'shing β Django yangi argument qo'shsa, funksiyangiz buzilmaydi.
Bu kod ishlashi uchun signal import qilinishi kerak (pastda ready() bo'limida ko'ramiz). Test natijasi:
# shell yoki testda
from django.contrib.auth.models import User
u = User.objects.create_user("demo", password="x")
print(Profil.objects.filter(user=u).exists()) # True -- avtomatik yaratildi!
Biz buni pytest bilan tekshirdik va True chiqdi β profil hech qanday qo'shimcha kodsiz paydo bo'ldi.
Mustahkamroq variant:
Profil.objects.create()o'rnigaProfil.objects.get_or_create(user=instance)ishlatish ko'pincha xavfsizroq β agar boshqa joyda profil allaqachon yaratilgan bo'lsa,IntegrityErrorchiqmaydi.
pre_save: bazaga yozishdan oldin aralashish¶
pre_save instance bazaga yozilmasdan oldin ishlaydi β demak, maydonlarini o'zgartirsangiz, o'zgargan qiymat saqlanadi. Klassik foydalanish: avtomatik slug, normallashtirilgan qiymat, yoki sana belgilash.
Postning holati "chop" ga o'tganda, agar chop_sana hali belgilanmagan bo'lsa, joriy vaqtni qo'yamiz:
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils import timezone
from .models import Post
@receiver(pre_save, sender=Post)
def chop_sanani_belgila(sender, instance, **kwargs):
if instance.holat == "chop" and instance.chop_sana is None:
instance.chop_sana = timezone.now()
pre_save da instance.save() chaqirmaymiz β shunchaki maydonni o'zgartiramiz, Django o'zi davom etib bazaga yozadi. Tekshirildi:
u = User.objects.create_user("vali", password="x")
p = Post.objects.create(muallif=u, sarlavha="Salom", holat="chop")
print(p.chop_sana) # None emas -- pre_save uni belgilab qo'ydi
Diagrammada ko'rinib turibdi: save() β pre_save β SQL (INSERT/UPDATE) β post_save. pre_save da instance hali bazada yo'q (pk bo'lmasligi mumkin), post_save da esa allaqachon yozilgan (pk aniq mavjud).
Diqqat β bu signal mantig'i ko'pincha model metodi bo'lishi kerak edi. Bu
chop_sanamantig'ini keyinroqPost.save()ni override qilish yokichop_et()metodi bilan qilish toza bo'ladimi degan savolni ko'rib chiqamiz. Hozircha signal sintaksisini o'rganyapmiz.
post_save + created: log yozish¶
post_save da instance allaqachon bazaga yozilgan β pk aniq mavjud, bog'liq obyektlar yaratish xavfsiz. Har yangi post yaratilganda audit log yozamiz:
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Post, AuditLog
@receiver(post_save, sender=Post)
def post_log(sender, instance, created, **kwargs):
if created:
AuditLog.objects.create(xabar=f"Yangi post: {instance.sarlavha}")
created bilan farqlash juda muhim: bu signal har bir save() da ishlaydi β ham INSERT, ham UPDATE'da. if created: bo'lmasa, har tahrirlashda yana log yozilib ketadi.
update_fields β qaysi maydon o'zgardi?¶
save(update_fields=[...]) bilan saqlasangiz, post_save qabul qiluvchisiga update_fields argumenti frozenset shaklida keladi (aks holda None). Bu β "faqat ko'rishlar soni o'zgarsa kesh tozala" kabi mantiq uchun zo'r:
@receiver(post_save, sender=Post)
def faqat_holat_ozgarsa(sender, instance, update_fields, **kwargs):
if update_fields and "holat" in update_fields:
# masalan: bildirishnoma yuborish
...
Biz buni testda tekshirdik: p.save(update_fields=["soz_soni"]) chaqirilganda qabul qiluvchiga frozenset({"soz_soni"}) keldi.
pre_delete va post_delete¶
O'chirishdan oldin ishlasa β instance hali bazada bor, ma'lumotini o'qish mumkin. O'chirilgandan keyin ishlasa β instance Python obyekti sifatida hali mavjud, lekin bazada yo'q.
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from .models import Post, AuditLog
@receiver(pre_delete, sender=Post)
def post_ochirish_log(sender, instance, **kwargs):
AuditLog.objects.create(xabar=f"Post ochirildi: {instance.sarlavha}")
Tekshirildi:
p = Post.objects.create(muallif=u, sarlavha="Ochiriladi")
p.delete()
print(AuditLog.objects.filter(xabar__contains="ochirildi").exists()) # True
Muhim cheklov:
pre_delete/post_deletefaqatinstance.delete()yoki QuerySetdelete()ichida obyekt-obyekt o'chirilganda chaqiriladi. LekinCASCADEo'chirishlarda Django bog'liq obyektlar uchun ham signal yuboradi. Bir narsa esda: katta hajmdadelete()qilsangiz, har bir qator uchun signal ishlaydi β bu sekin bo'lishi mumkin.
m2m_changed: ManyToMany maydon o'zgarganda¶
ManyToMany maydon β alohida holat. post.teglar.add(t1, t2) chaqirganda post.save() ishlamaydi β bu boshqa (oraliq) jadvalni o'zgartiradi. Shuning uchun maxsus signal: m2m_changed.
Qabul qiluvchining sender'i β model emas, balki oraliq (through) jadval: Post.teglar.through.
from django.db.models.signals import m2m_changed
from django.dispatch import receiver
from .models import Post, AuditLog
@receiver(m2m_changed, sender=Post.teglar.through)
def teg_ozgardi(sender, instance, action, pk_set, **kwargs):
if action == "post_add":
AuditLog.objects.create(
xabar=f"'{instance.sarlavha}' ga {len(pk_set)} ta teg qoshildi"
)
Asosiy argumentlar:
actionβ nima sodir bo'ldi.m2m_changedhar o'zgarishda ikki marta ishlaydi:"pre_add"(oldin) va"post_add"(keyin). Boshqalari:"pre_remove"/"post_remove","pre_clear"/"post_clear". Odatdapost_*ni tekshiramiz.pk_setβ qaysi obyektlar qo'shildi/o'chirildi (ularningpklari to'plami).clear()daNonebo'ladi.instanceβ qaysi obyektning m2m maydoni o'zgardi (bu yerdaPost).reverseβFalsebo'lsa,post.teglar.add()(oldinga);Truebo'lsa,teg.postlar.add()(teskari).
Tekshirildi:
p = Post.objects.create(muallif=u, sarlavha="Teglik")
t1 = Teg.objects.create(nom="django")
t2 = Teg.objects.create(nom="python")
p.teglar.add(t1, t2)
print(AuditLog.objects.filter(xabar__contains="2 ta teg").exists()) # True
if action == "post_add": bo'lmasa, "pre_add" da pk_set hali yangi qiymatlarni o'z ichiga olmaydi yoki ikki marta log yozilib ketadi.
AppConfig.ready(): signallarni qayerda ulash kerak¶
Signal fayli (signals.py) shunchaki yozib qo'yilsa hech narsa qilmaydi β uni biror joy import qilishi kerak, shunda @receiver dekoratorlari ishga tushadi va signallarga ulanadi. To'g'ri joy β app'ning AppConfig.ready() metodi. Django ilovani yuklaganda buni avtomatik chaqiradi.
shop/apps.py:
from django.apps import AppConfig
class ShopConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "shop"
def ready(self):
from . import signals # noqa: F401
E'tibor bering:
- Import
ready()ichida β yuqorida, fayl boshidaimportqilsangiz, app'lar hali to'liq yuklanmaganda modellarga murojaat qilibAppRegistryNotReadyxatosi chiqishi mumkin.ready()esa hamma app yuklanib bo'lgach chaqiriladi β xavfsiz joy. # noqa: F401β linter "bu import ishlatilmayapti" deb shikoyat qilmasin (aslida import yon-ta'sir uchun: dekoratorlarni ishga tushirish).- Django 6.0 da
startappapps.pyni avtomatik yaratadi vaINSTALLED_APPSda to'liq yo'l ("shop.apps.ShopConfig") yoki shunchaki"shop"yozilsa, Django shuAppConfigni topadi.
Biz python manage.py check ni ishga tushirdik β System check identified no issues chiqdi, signallar muvaffaqiyatli ulandi.
Tez-tez uchraydigan xato: Signalni
models.pyoxirida yozib,ready()da hech narsa qilmaslik. Ba'zan ishlaydi (chunkimodels.pyhar doim import qilinadi), lekin signal alohidasignals.pyda bo'lsa,ready()siz u hech qachon import qilinmaydi va signal jim turaveradi. Bu β eng ko'p "nega signalim ishlamayapti?" sababidir.
Qachon signal ishlatish kerak β va qachon KERAK EMAS¶
Bu β bobning eng muhim bo'limi. Signal qudratli, lekin ko'pchilik uni keragidan ortiq ishlatadi. Sababi: signal mantiqni ko'rinmas qiladi. Kodni o'qiyotgan dasturchi post.save() deb yozadi, ammo orqada uch xil signal ishga tushib, log yozib, kesh tozalab, email yuborayotganini bilmaydi.
Signal MOS keladi:
- Boshqa app'ga reaksiya. O'z modelingizni o'zgartira olmaysiz (masalan,
django.contrib.auth.User), lekin unga reaksiya qo'shmoqchisiz β profil yaratish bunga klassik misol. - Haqiqatan yon-ta'sir (side effect), asosiy oqimga taalluqli emas: audit log, qidiruv indeksini yangilash, keshni tozalash.
- Bir nechta turli model bir xil hodisaga reaksiya qilishi kerak bo'lganda (umumiy "tozalash" mantig'i).
Signal KERAK EMAS (aksincha β zarar):
- O'z modelingizning asosiy mantig'i. "Post chop etilganda chop_sana qo'y" β bu
Postning o'z ishi.pre_savesignalidan ko'rasave()metodi yokichop_et()metodi ravshanroq. - Tartibga (order) bog'liq mantiq. Bir nechta
post_saveqabul qiluvchisi bo'lsa, ularning ishga tushish tartibi kafolatlanmaydi. - Transaksiya muhim bo'lganda. Signal
save()bilan bir transaksiyada ishlaydi, lekin agar signalda email yuborsangiz va transaksiya keyin bekor (rollback) bo'lsa β email allaqachon ketgan bo'ladi. To'g'ri yechim:transaction.on_commit(...). - Test va debug. Signal mantig'ini test qilish va kuzatish qiyinroq β chunki u "yashirin".
Oltin qoida: Asosiy biznes oqimini aniq chaqir (model metodi, servis funksiyasi). Faqat haqiqiy, qo'shimcha yon-ta'sirni signalga qo'y. Quyidagi ikki bo'limda aynan shu "aniq" muqobillarni ko'ramiz.
Custom model metodlari va @property¶
Signaldan oldin har doim oddiy savol bering: "bu mantiq aslida shu modelning o'z ishi emasmi?". Agar shunday bo'lsa β model metodi yoki @property ravshanroq, oson test qilinadigan va ko'rinadigan yechim.
class Post(models.Model):
# ... maydonlar ...
@property
def qisqa_matn(self):
return (self.matn[:50] + "...") if len(self.matn) > 50 else self.matn
def chop_etilganmi(self):
return self.holat == "chop" and self.chop_sana is not None
Farqi:
@propertyβ argumentsiz, hisoblangan "maydon" kabi:post.qisqa_matn(qavssiz). Templatda ham{{ post.qisqa_matn }}deb chaqiriladi β template metodni qavssiz chaqiradi.- Oddiy metod β
post.chop_etilganmi()(qavs bilan). Mantiq biroz "amal" tusida bo'lsa, metod tabiiyroq.
Tekshirildi:
p = Post.objects.create(muallif=u, sarlavha="X", matn="a"*100, holat="chop")
print(p.qisqa_matn) # "aaaa...aaa..." (50 belgi + ...)
print(p.chop_etilganmi()) # True
Asosiy oqimni save() orqali β signalga muqobil¶
Yodingizdami, chop_sanani pre_save signalida belgilagandik? Endi uni ochiq qilamiz. Ikki toza muqobil:
# β Yashirin: pre_save signalida -- o'qiyotgan dasturchi ko'rmaydi
# (yuqorida ko'rsatilgan -- mantiq "ko'rinmas")
# β
Variant A: save() ni override qilish (model o'z ishini biladi)
class Post(models.Model):
# ...
def save(self, *args, **kwargs):
if self.holat == "chop" and self.chop_sana is None:
from django.utils import timezone
self.chop_sana = timezone.now()
super().save(*args, **kwargs)
# β
Variant B: aniq "amal" metodi -- eng ravshani
class Post(models.Model):
# ...
def chop_et(self):
from django.utils import timezone
self.holat = "chop"
self.chop_sana = timezone.now()
self.save(update_fields=["holat", "chop_sana"])
Variant B (post.chop_et()) ko'pincha eng yaxshisi: chaqiruvchi nima bo'layotganini ko'radi. "Post chop etish" β bu asosiy biznes amali, signalda yashirinmasin.
save()override qilganda diqqat:super().save(*args, **kwargs)ni albatta chaqiring va*args, **kwargsni o'tkazing β aks holdaupdate_fields,usingkabi parametrlar yo'qoladi.
Custom Manager va QuerySet¶
Post.objects β bu Manager. Post.objects.filter(...) β QuerySet qaytaradi. So'rov mantig'i takrorlanaversa (filter(holat="chop", chop_sana__lte=timezone.now()) ni o'nta joyda yozish), uni bitta nomli joyga yig'ish kerak. Bu β custom QuerySet/Managerning maqsadi.
To'liq, ochiq usul¶
from django.db import models
from django.utils import timezone
class PostQuerySet(models.QuerySet):
def chop_etilgan(self):
return self.filter(holat="chop", chop_sana__lte=timezone.now())
def muallif(self, user):
return self.filter(muallif=user)
class PostManager(models.Manager):
def get_queryset(self):
return PostQuerySet(self.model, using=self._db)
def chop_etilgan(self):
return self.get_queryset().chop_etilgan()
Modelga ulash:
Endi:
Post.objects.chop_etilgan() # faqat chop etilganlar
Post.objects.get_queryset().chop_etilgan().muallif(u) # zanjir!
QuerySet metodlari self ni qaytarganligi (aniqrog'i, yangi QuerySet) tufayli ularni zanjirlash mumkin: .chop_etilgan().muallif(u). Bu β Django ORM'ining go'zalligi. Biz buni testda tekshirdik: ikkala chaqiruv ham to'g'ri sonni qaytardi.
Qisqa yo'l: as_manager() va from_queryset()¶
Yuqorida PostManager ichida har bir metodni qo'lda takrorlash zerikarli. Django ikki qisqa yo'l beradi:
# 1-usul: QuerySet'dan to'g'ridan-to'g'ri Manager yasash
class Post(models.Model):
# ...
objects = PostQuerySet.as_manager()
as_manager() β PostQuerySetning hamma metodini avtomatik Managerga ko'chiradi. Endi Post.objects.chop_etilgan() ham, Post.objects.muallif(u) ham to'g'ridan-to'g'ri ishlaydi, alohida Manager klassi yozish shart emas.
# 2-usul: Manager'ga qo'shimcha metod ham kerak bo'lsa
class PostManager(models.Manager.from_queryset(PostQuerySet)):
def statistika(self):
# faqat Manager darajasidagi maxsus mantiq
return {"jami": self.count(), "chop": self.chop_etilgan().count()}
class Post(models.Model):
# ...
objects = PostManager()
from_queryset(PostQuerySet) β PostQuerySetning hamma metodini olib keladigan Manager baza klassini yasaydi; siz unga faqat qo'shimcha metod qo'shasiz. Biz as_manager() va from_queryset() ikkalasini ham testda tekshirdik β ikkalasi ham chop_etilgan() ni to'g'ri qaytardi.
Qachon qaysi: Faqat zanjirli filtrlar kerak bo'lsa β as_manager(). Manager darajasida ham metod (masalan butun jadval statistikasi) kerak bo'lsa β from_queryset().
Diqqat β
update()signal yubormaydi: Custom QuerySet bilanPost.objects.chop_etilgan().update(soz_soni=0)chaqirsangiz, bu to'g'ridan-to'g'ri SQLUPDATEqiladi βpre_save/post_saveishlamaydi. Xuddi shubulk_create()uchun ham amal qiladi. Agar signalga tayanayotgan bo'lsangiz,update()/bulk_create()mantiqni chetlab o'tadi. Bu β signalga emas, aniq metodga tayanish foydasiga yana bir dalil.
Cheksiz tsikl tuzog'i va undan qochish¶
Eng mashhur signal xatosi: post_save ichida xuddi shu obyektni yana save() qilish.
# β XATO -- cheksiz tsikl (RecursionError yoki bazaga to'xtovsiz yozish)
@receiver(post_save, sender=Post)
def yomon(sender, instance, **kwargs):
instance.soz_soni += 1
instance.save() # -> yana post_save -> yana save() -> ...
save() β post_save β save() β post_save β ... cheksiz. Yechimlar:
# β
Yechim 1: update_fields bilan ma'lum maydonni saqlab, shartni tekshirish
@receiver(post_save, sender=Post)
def yaxshi(sender, instance, update_fields, **kwargs):
if update_fields and "soz_soni" in update_fields:
return # bu o'zimiz qilgan save -- to'xta
Post.objects.filter(pk=instance.pk).update(soz_soni=instance.soz_soni + 1)
# .update() post_save yubormaydi -> tsikl yo'q
# β
Yechim 2: umuman save() chaqirmaslik, .update() ishlatish
# β
Yechim 3: bu mantiqni signalga emas, model metodiga ko'chirish (eng toza)
Eng yaxshi yechim β bunday "o'zini yangilash" mantig'ini umuman signalga qo'ymaslik. pre_save da maydonni o'zgartirish (instance.x = ..., save() siz) β tsikl yaratmaydi, chunki Django keyin bir marta yozadi.
Custom signal yasash¶
Tayyor signallar yetmasa, o'zingiznikini yasashingiz mumkin. Masalan, "post chop etildi" degan biznes hodisasi uchun (ORM save() dan ko'ra ma'noliroq).
shop/signals.py:
Modelda hodisa yuz berganda yuboramiz:
from .signals import post_chop_etildi
class Post(models.Model):
# ...
def chop_et(self):
from django.utils import timezone
self.holat = "chop"
self.chop_sana = timezone.now()
self.save(update_fields=["holat", "chop_sana"])
# 2. Signal yuboramiz -- ixtiyoriy nomli argumentlar bilan
post_chop_etildi.send(sender=self.__class__, post=self)
Qabul qiluvchi:
from django.dispatch import receiver
from .signals import post_chop_etildi
@receiver(post_chop_etildi)
def chop_etilganda(sender, post, **kwargs):
# masalan: obunachilarga xabar yuborish
print(f"'{post.sarlavha}' chop etildi!")
Biz custom signalni testda tekshirdik: post_chop_etildi.send(sender=Post, post=p) chaqirilganda qabul qiluvchi haqiqatan ishga tushdi va postni qabul qildi. send() har bir qabul qiluvchini sinxron chaqiradi va (receiver, javob) juftliklari ro'yxatini qaytaradi.
Custom signal β to'liq dekoplangan arxitektura uchun foydali, lekin yana bir bor: aniq metod chaqiruvi ko'pincha ravshanroq. Custom signalni bir nechta mustaqil joy bitta hodisaga reaksiya qilishi kerak bo'lganda ishlating.
Signalni vaqtincha o'chirish (test va migratsiyada)¶
Ba'zan signal xalaqit beradi β masalan, testda yoki ma'lumotni ommaviy yuklashda profil avtomatik yaralishini istamaysiz. disconnect()/connect():
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from shop import signals
post_save.disconnect(signals.profil_yarat, sender=User)
try:
User.objects.create_user("temir", password="x") # profil YARALMAYDI
finally:
post_save.connect(signals.profil_yarat, sender=User) # qayta ulaymiz
Biz buni testda tekshirdik: disconnect dan keyin yangi user uchun profil yaralmadi, try/finally esa signalni qayta uladi. finally ni unutmang β aks holda signal boshqa testlarda o'chiq qoladi.
To'liq misol: hammasini birlashtirish¶
Quyida bob bo'yicha yig'ma shop/signals.py (biz aynan shuni ishga tushirib tekshirdik):
from django.conf import settings
from django.db.models.signals import (
pre_save, post_save, pre_delete, m2m_changed,
)
from django.dispatch import receiver
from django.utils import timezone
from .models import Post, Profil, AuditLog
@receiver(pre_save, sender=Post)
def chop_sanani_belgila(sender, instance, **kwargs):
if instance.holat == "chop" and instance.chop_sana is None:
instance.chop_sana = timezone.now()
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def profil_yarat(sender, instance, created, **kwargs):
if created:
Profil.objects.create(user=instance)
@receiver(post_save, sender=Post)
def post_log(sender, instance, created, **kwargs):
if created:
AuditLog.objects.create(xabar=f"Yangi post: {instance.sarlavha}")
@receiver(pre_delete, sender=Post)
def post_ochirish_log(sender, instance, **kwargs):
AuditLog.objects.create(xabar=f"Post ochirildi: {instance.sarlavha}")
@receiver(m2m_changed, sender=Post.teglar.through)
def teg_ozgardi(sender, instance, action, pk_set, **kwargs):
if action == "post_add":
AuditLog.objects.create(
xabar=f"'{instance.sarlavha}' ga {len(pk_set)} ta teg qoshildi"
)
AppConfig.ready() da ulanadi, va pytest bilan to'liq oqim tekshirildi: foydalanuvchi yaratilganda profil paydo bo'ldi, post chop etilganda chop_sana belgilandi, log yozildi, teg qo'shilganda m2m log yozildi, post o'chirilganda o'chirish logi yozildi. Hammasi 8 passed.
Mashqlar¶
Oson¶
post_saveqabul qiluvchisi yozing: yangiTegyaratilgandaAuditLogga"Yangi teg: <nom>"yozsin.createdni to'g'ri ishlating.@propertyqo'shing:Post.soz_haqidaβ matndagi so'zlar sonini qaytarsin (len(self.matn.split())).pre_saveqabul qiluvchisi yozing:Teg.nomni har doim kichik harfga (lower()) aylantirib saqlasin.Postgaochilganmi()metodi qo'shing:holat == "chop"bo'lsaTrueqaytarsin.AppConfig.ready()ichida signal import qilinishini tushuntiring: nega yuqorida (fayl boshida) import qilish noto'g'ri?post_deletevapre_deletefarqini ayting: qaysidainstance.pkhali bazada mavjud?
O'rta¶
PostQuerySetgaqoralama()metodi qo'shing (holat="qoralama"filtri) vaManagerorqaliPost.objects.qoralama()ishlashini ta'minlang.m2m_changedda"post_remove"ni tutib,AuditLogga "teg o'chirildi" yozing.pk_setni ishlating.- Cheksiz tsiklga tushadigan
post_saveqabul qiluvchisini yozing (xato namuna), so'ng uniupdate_fieldsshartini tekshirib tuzating. as_manager()dan foydalanib,PostgaPostQuerySetni alohidaManagerklassisiz ulang. AvvalgiPostManagerbilan farqini ayting.- Signalni vaqtincha o'chiruvchi test yozing:
profil_yaratnidisconnectqilib, user yarating va profil yaralmaganini tasdiqlang.finallyda qayta ulang. Post.save()ni override qilib,chop_sanamantig'inipre_savesignalidan ko'chiring. Qaysi biri ravshanroq β fikr bildiring.
Qiyin¶
- Custom signal
post_chop_etildiyarating,Post.chop_et()da uni yuboring, va ikkita mustaqil qabul qiluvchi ulang (biri log yozadi, biriprintqiladi).send()qaytargan ro'yxatni tekshiring. transaction.on_commit()bilan ishlovchipost_saveqabul qiluvchisi yozing: log faqat transaksiya muvaffaqiyatli yakunlanganda yozilsin (pytestdatransaction=Truesozlamasini eslang).from_queryset()bilanPostManageryasab, unga faqat Manager darajasidagistatistika()metodini qo'shing ({"jami": ..., "chop": ...}qaytarsin) vachop_etilgan()ham zanjirlanishini saqlang.
Yechimlar
Oson¶
1. Yangi teg logi:
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Teg, AuditLog
@receiver(post_save, sender=Teg)
def teg_log(sender, instance, created, **kwargs):
if created:
AuditLog.objects.create(xabar=f"Yangi teg: {instance.nom}")
2. So'z soni @property:
3. Teg nomini kichik harfga:
from django.db.models.signals import pre_save
@receiver(pre_save, sender=Teg)
def teg_kichik(sender, instance, **kwargs):
instance.nom = instance.nom.lower()
4. ochilganmi() metodi:
5. signals.py shunchaki yozilgan bo'lsa, biror joy import qilmaguncha @receiver dekoratorlari bajarilmaydi β demak signal ulanmaydi. ready() Django ilovani yuklab bo'lgach chaqiriladi, shuning uchun u eng to'g'ri (va xavfsiz) import joyi. Faylning boshida import qilsangiz, app registry hali tayyor bo'lmaganda modellarga murojaat qilib AppRegistryNotReady chiqishi mumkin.
6. pre_delete da instance.pk hali bazada mavjud (o'chirish hali bo'lmagan) β uni o'qish/bog'liq ma'lumot olish mumkin. post_delete da bazada qator yo'q (Python obyektida pk qiymati hali turishi mumkin, lekin bazada topilmaydi).
O'rta¶
7. qoralama() metodi:
class PostQuerySet(models.QuerySet):
def chop_etilgan(self):
return self.filter(holat="chop", chop_sana__lte=timezone.now())
def qoralama(self):
return self.filter(holat="qoralama")
class PostManager(models.Manager):
def get_queryset(self):
return PostQuerySet(self.model, using=self._db)
def chop_etilgan(self):
return self.get_queryset().chop_etilgan()
def qoralama(self):
return self.get_queryset().qoralama()
Post.objects.qoralama() endi qoralamalarni qaytaradi.
8. post_remove ni tutish:
@receiver(m2m_changed, sender=Post.teglar.through)
def teg_ozgardi(sender, instance, action, pk_set, **kwargs):
if action == "post_add":
AuditLog.objects.create(
xabar=f"'{instance.sarlavha}' ga {len(pk_set)} ta teg qoshildi")
elif action == "post_remove":
AuditLog.objects.create(
xabar=f"'{instance.sarlavha}' dan {len(pk_set)} ta teg ochirildi")
9. Xato va tuzatish:
# β Cheksiz tsikl
@receiver(post_save, sender=Post)
def yomon(sender, instance, **kwargs):
instance.soz_soni += 1
instance.save() # -> yana post_save -> ...
# β
Tuzatilgan
@receiver(post_save, sender=Post)
def yaxshi(sender, instance, update_fields, **kwargs):
if update_fields and "soz_soni" in update_fields:
return
Post.objects.filter(pk=instance.pk).update(soz_soni=instance.soz_soni + 1)
# .update() post_save yubormaydi
10. as_manager():
Farqi: as_manager() PostQuerySetning hamma metodini avtomatik Manager'ga ko'chiradi β alohida PostManager klassi va har metodni qo'lda takrorlash kerak emas. Faqat zanjirli filtrlar kerak bo'lsa bu eng qisqa yo'l. Manager darajasida boshqa-ya mantiq (masalan butun jadval statistikasi) kerak bo'lsagina to'liq Manager yoki from_queryset() kerak bo'ladi.
11. Signalni o'chiruvchi test:
import pytest
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from shop import signals
from shop.models import Profil
@pytest.mark.django_db
def test_signal_uzish():
post_save.disconnect(signals.profil_yarat, sender=User)
try:
u = User.objects.create_user("temir", password="x")
assert not Profil.objects.filter(user=u).exists()
finally:
post_save.connect(signals.profil_yarat, sender=User)
12. save() override:
class Post(models.Model):
# ...
def save(self, *args, **kwargs):
if self.holat == "chop" and self.chop_sana is None:
self.chop_sana = timezone.now()
super().save(*args, **kwargs)
save() override ravshanroq: kodni o'qiyotgan dasturchi Post klassini ochib, mantiqni darhol ko'radi. pre_save signalida bu mantiq alohida faylda "yashiringan" bo'lardi β bu o'z modelimizning asosiy mantig'i bo'lgani uchun, signal emas, model ichida turishi to'g'riroq.
Qiyin¶
13. Custom signal:
# signals.py
import django.dispatch
post_chop_etildi = django.dispatch.Signal()
from django.dispatch import receiver
@receiver(post_chop_etildi)
def chop_log(sender, post, **kwargs):
AuditLog.objects.create(xabar=f"Chop etildi: {post.sarlavha}")
return "log_yozildi"
@receiver(post_chop_etildi)
def chop_print(sender, post, **kwargs):
print(f"'{post.sarlavha}' chop etildi!")
return "printed"
# models.py: Post.chop_et()
def chop_et(self):
self.holat = "chop"
self.chop_sana = timezone.now()
self.save(update_fields=["holat", "chop_sana"])
return post_chop_etildi.send(sender=self.__class__, post=self)
# foydalanish: natija = post.chop_et()
# natija -> [(chop_log, "log_yozildi"), (chop_print, "printed")]
send() har bir qabul qiluvchini chaqiradi va (funksiya, qaytarilgan qiymat) juftliklarini ro'yxat qilib qaytaradi.
14. transaction.on_commit():
from django.db import transaction
from django.db.models.signals import post_save
@receiver(post_save, sender=Post)
def commitdan_keyin_log(sender, instance, created, **kwargs):
if created:
transaction.on_commit(
lambda: AuditLog.objects.create(
xabar=f"(commit) post: {instance.sarlavha}")
)
on_commit ichidagi funksiya faqat transaksiya muvaffaqiyatli COMMIT bo'lganda ishlaydi β rollback bo'lsa umuman chaqirilmaydi. pytest-django da buni tekshirish uchun @pytest.mark.django_db(transaction=True) ishlatiladi (oddiy django_db har testni rollback qiladi, shuning uchun on_commit ishlamaydi).
15. from_queryset():
class PostManager(models.Manager.from_queryset(PostQuerySet)):
def statistika(self):
return {"jami": self.count(), "chop": self.chop_etilgan().count()}
class Post(models.Model):
# ...
objects = PostManager()
# Post.objects.chop_etilgan() -> zanjirli filtr (QuerySet'dan)
# Post.objects.statistika() -> {"jami": N, "chop": M} (Manager metodi)
from_queryset(PostQuerySet) PostQuerySetning hamma metodini olib keladigan baza Manager yasaydi; biz unga faqat statistika() ni qo'shamiz. Shu tariqa zanjir ham, Manager darajasidagi maxsus mantiq ham birga ishlaydi.
β¬ οΈ Oldingi: 18 β DRF filtrlash, paginatsiya, nested Β· π README Β· Keyingi: 20 β Keshlash va performance β‘οΈ