آموزش جنگو : جلسه بیست و نه | transform , Asynchronous , caching در جنگو

در این جلسه در رابطه با transform در فیلتر ها , برخی از میانبر ها یا شورتکات ها , مبحث کش ها و Asynchronous صحبت خواهیم کرد . با ما همراه باشید .

آموزش جنگو : جلسه بیست و نه | transform , Asynchronous , caching در جنگو
آموزش جنگو : جلسه بیست و نه | transform , Asynchronous , caching در جنگو


بررسی transform در فیلتر ها

جنگو از transform یا همان تبدیل نیز در lookup پشتیبانی می کند .

به عنوان مثال برای دریافت تمام رکورد های Entry که سال pub_date آنها با سال mod_date آنها یکسان باشد (یعنی سال انتشار آنها با سال آخرین تغییر یافتن آنها یکسان باشد) :

>>> from django.db.models import F
>>> Entry.objects.filter(pub_date__year=F('mod_date__year'))

برای دریافت اولین سالی که (قدیمی ترین) یک Entry منتشر شده است ,از کد زیر استفاده می کنیم . (بعدا بیشتر درباره این کد ها صحبت می کنیم)

>>> from django.db.models import Min
>>> Entry.objects.aggregate(first_published_year=Min('pub_date__year'))

کد زیر نیز بالاترین امتیاز (فیلد rate) که یک رکورد Entry دارد را به علاوه جمع تعداد comment از هر سال و تمام entry ,پیدا می کند و مقدار آن را محاسبه می کند . (در مورد تابع های ناشناخته داخل کد بیشتر صحبت می کنیم)

>>> from django.db.models import OuterRef, Subquery, Sum
>>> Entry.objects.values('pub_date__year').annotate(
..   top_rating=Subquery(
...  Entry.objects.filter(
...  pub_date__year=OuterRef('pub_date__year'),
...  ).order_by('-rating').values('rating')[:1]
...   ),
...  total_comments=Sum('number_of_comments'),
...  )


بررسی میانبر pk lookup

نکته : تفاوت primary key با id در آن است که اولی می تواند از پیش فرض که روی id است ,روی فیلد دیگری قرار بگیرد و از این پس در lookup استفاده از pk (primary key) از همان فیلد استفاده می شود .

برای راحتی، جنگو میانبر lookup pk را ارائه می دهد که مخفف “primary key" است. برای مثال در مدل Blog، کلید اصلی یا همان , primary key فیلد id است، بنابراین این سه عبارت یکسان هستند و یک کار را انجام می دهند :

>>> Blog.objects.get(id__exact=14) # Explicit form
>>> Blog.objects.get(id=14) # __exact is implied
>>> Blog.objects.get(pk=14) # pk implies id__exact

استفاده از pk محدود به exact نیست و می توانید هر query و با هر نوع lookup را با pk ترکیب کنید تا یک query روی مدل اصلی انجام بدهید .

# Get blogs entries with id 1, 4 and 7
>>> Blog.objects.filter(pk__in=[1,4,7])
# Get all blog entries with id > 14
>>> Blog.objects.filter(pk__gt=14)

همچنین آنها بدون استفاده از pk و با نام بردن از فیلد primary key نیز می توانند کار خود را انجام دهند . سه عبارت زیر برابر هستند :

>>> Entry.objects.filter(blog__id__exact=3) # Explicit form
>>> Entry.objects.filter(blog__id=3) # __exact is implied
>>> Entry.objects.filter(blog__pk=3) # __pk implies __id__exact


استفاده از علامت درصد و _ در LIKE

نکته : برای خواندن این بحث باید کمی به SQL مسلط باشید !

جستجوی ها یا همان query در جنگو هنگامی که از دستور LIKE استفاده می کنند (مثال iexact , contains , icontains , startwith , istartwith , endwith , iendswith) به طور خودکار علامات خاص (%و _) را در دستور جای گذاری می کنند . در یک عبارت LIKE ,علامت درصد و علامت _ برای مطابقت ها به کار می رود .

بنابراین شاید فکر کنید که نمی توان علامت درصد را در پارامتر ها استفاده کرد ولی اینطور نیست . جنگو به طور مستقیم کار می کند و آنها را هندل می کند . برای مثال , برای دریافت تمام رکورد هایی که در آنها علامت درصد به کار رفته است , کد زیر را بنویسید :

>>> Entry.objects.filter(headline__contains='%')

جنگو کد بالا را به صورت زیر درمیاورد و آن را می تواند بسیار ساده در دیتابیس اجرا کند .

SELECT ... WHERE headline LIKE '%\%%';

همین مراحل برای علامت _ نیز تکرار می شوند . پس لازم نیست نگران آن باشید !


بررسی Querysets و caching

هر QuerySet دارای یک cache یا کش یا همان حافظه نهان است تا اطلاعات را در آن ذخیره کند و میزان تعدد دسترسی به دیتابیس را به حداقل برساند . درک نحوه کار این قابلیت به شما امکان می دهد تا کد کارآمدتری بنویسید .

در یک QuerySet که تازه ایجاد شده باشد ,کش خالی است . هنگامی که برای اولین بار یک QuerySet اجرا می شود ,یک Query در دیتابیس اجرا می شود و جنگو نتایج آن را در حافظه پنهان QuerySet شما ذخیره می کند و سپس نتایج را به شما باز میگرداند (و اگر همان QuerySet دوباره تکرار شود از کش استفاده می کند) .

این رفتار ذخیره سازی را در یاد داشته باشید زیرا اگر آن را درست مدیریت نکنید , ممکن است برای شما مشکلاتی را ایجاد کند ! به عنوان مثال موارد زیر دو QuerySet را ایجاد می کند ,آنها را در دیتابیس اجرا می کند و در آخر آنها را دور میندازد .

>>> print([e.headline for e in Entry.objects.all()])
>>> print([e.pub_date for e in Entry.objects.all()])

کد بالا دو بار Query را در دیتابیس اجرا می کند (از کش آن استفاده نمی کند) و باعث می شود کار دیتابیس سنگین تر باشد . این همچنین باعث می شود تا رکورد های بازگشتی یکسان نباشند , زیرا ممکن است یک رکورد در هنگام اجرای دو Query بین آنها تقسیم شود و حذف شود !

برای جلوگیری از این مشکل باید فقط یک QuerySet را ایجاد کرده و هر بار روی آن پیمایش کنید . برای مثال :

>>> queryset = Entry.objects.all()
>>> print([p.headline for p in queryset]) # Evaluate the query set.
>>> print([p.pub_date for p in queryset]) # Reuse the cache from the evaluation.
نکته مهم : به یاد داشته باشید که QuerySet همیشه نتایج خود را کش نمی کنند . هنگام اجرای QuerySet تنها بخشی از جستجو ,کش بررسی میشود تا پر نشده باشد ! پس مواردی که در Query بعدی برگردانده می شود ,کش شده نیستند . این به معنی آن است که هنگامی که شما نتایج بازگشتی query را محدود کنید (مثلا با ایندکس های آرایه ها یا slice دادن) , query شما هیچگاه در کش ذخیره نخواهد شد .

به عنوان مثال کد زیر یک slice آرایه ای را روی QuerySet شما اعمال کرده است پس در هر خط به جای استفاده از کش , جنگو آن را دوباره در دیتابیس اجرا می کند .

>>> queryset = Entry.objects.all()
>>> print(queryset[5]) # Queries the database
>>> print(queryset[5]) # Queries the database again

ولی ,اگر QuerySet قبلا در دیتابیس اجرا شده باشد ,این بار حافظه کش آن بررسی می شود و این بهتر است !

>>> queryset = Entry.objects.all()
>>> [entry for entry in queryset] # Queries the database
>>> print(queryset[5]) # Uses cache
>>> print(queryset[5]) # Uses cache

در اینجا یک مثال دیگر نیز آورده ایم که در آن QuerySet ابتدا در دیتابیس اجرا می شود و در استفاده های بعدی از کش آن استفاده می شود .

>>> [entry for entry in queryset]
>>> bool(queryset)
>>> entry in queryset
>>> list(queryset)
نکته : استفاده از دستور ()print کش را پر نمی کند !


بررسی مفهوم جستجو های Asynchronous

نکته : قبل از مطالعه این بخش و بخش های بعدی ابتدا با مفهوم synchronous (اجرای کدها به صورت همزمان) و asynchronous (اجرای کد ها به صورت ناهمزمان) در پایتون آشنا شوید!

اگر در حال نوشتن کدها و ویو های asynchronous هستید ,شما نمی توانید از ORM برای query به روشی که در بالا توضیح دادیم ,استفاده کنید . زیرا نمی توان یک کد synchronous را از داخل یک قطعه کد asynchronous مسدود یا block کرد (متوقف) . به احتمال زیاد نیز جنگو در این مواقع یک خطای SynchronousOnlyOperation برای جلوگیری از این کار ایجاد خواهد کرد .

خوشبختانه جنگو راه های زیادی پیش پای شما گذاشته است تا بتوانید به صورت asynchronous کد های خود را بنویسید . هر متدی که باعث می شود تا کد های شما block شوند (مانند ()get یا ()delete) یک ورژن دیگر نیز دارد که برای نوشتن کدهای asynchronous به کار می روند (برای مثال ()aget و ()adelete) و هنگامی که از آنها استفاده کنید , میتوانید روی نتایج بازگشتی از آنها به صورت asynchronous پیمایش کنید !

در صفحه بعد تا حدودی با این روش ها آشنا خواهید شد .


پیمایش روی query

جنگو به صورت پیش فرض پیمایش یا همان iteration روی synchronous انجام می دهد . به همین علت هنگامی که بخواهید به صورت معمولی و در خود ویو با حلقه for پیمایشی را روی یک query انجام دهید ,جنگو نتایج را block و متوقف می کند . این به این علت است که جنگو همزمان با پیمایش ,نتایج را نیز لود می کند (شما قطعا این را نمی خواهید) . برای درست کردن این مشکل فقط کافی است آن را به صورت async اجرا کنید .

نکته : شما این مشکل را فقط زمانی دارید که در ویو خود بخواهید روی query پیمایش کنید و for زدن در تمپلیت های جنگو باعث ایجاد این مشکل نمی شود (یعنی اگر ابتدا query را به تمپلیت بفرستید و با تمپلیت تگ ها روی آن پیمایش کنید)
async for entry in Authors.objects.filter(name__startswith=&quotA&quot):
    ...

توجه داشته باشید که در حالت async نمی توانید کار های دیگری مثل تبدیل کردن query به لیست انجام دهید یا هر کاری که موجب شود QuerySet شما دوباره روی دیتابیس اجرا شود .

دلیل اینکه می توانیم از متد های ()filter یا ()exclude استفاده کنیم آن است که این متد ها در واقع فقط query را ایجاد می کنند و آن را تا زمانی که از آن استفاده نکنید روی دیتابیس اجرا نمی کنند . پس شما می توانید آزادانه هر زمانی که نیاز داشتید در حالت async استفاده کنید . بعدا بیشتر درباره انواع متد هایی که این قابلیت را دارند صحبت خواهیم کرد .


بررسی QuerySet و متد های manager

زمانی که بخواهید کد ها را به صورت asynchronous بنویسید ,باید توجه کنید که برخی از متد ها مانند ()get و ()first اجرای query ساخته شده را اجباری می کنند و به همین علت همان لحظه توسط جنگو متوقف می شوند. این در حالی است که برخی دیگر از متد ها مانند ()filter و ()exclude اجرای در لحظه query را اجباری نمی کنند و می توانند در asynchronous به کار بروند . اما چگونه باید تفاوت میان آنها را تشخیص بدهید ؟

یک راه این است که به اول آنها نگاه کنید . برای متد هایی که باعث متوقف شدن کد خواهند شد یک ورژن از آنها در نظر گرفته شده است که این را اصلاح می کند . برای مثال ()get و ()aget . ولی شما هیچگاه کدی به شکل ()afilter نخواهید دید ! گرچه جنگو پیشنهاد بهتری دارد و فقط کافی است در صفحه زیر بررسی کنید که با چه نوعی از متدها طرف هستید ؟

(https://docs.djangoproject.com/en/4.1/ref/models/querysets/)

در این صفحه شما دو گروه از متد ها را مشاهده خواهید کرد :

1-متد هایی که یک QuerySet جدید را بازمیگردانند (قبلا در مورد چگونگی آن صحبت کرده ایم) . این ها همان متد هایی هستند که متوقف نمی شوند و نسخه های asynchronous ندارند . شما می توانید در هر شرایطی از آنها استفاده کنید .

2-متد هایی که QuerySet جدیدی را بازمیگردانند . این متد ها توسط جنگو در زمان asynchronous بودن کد متوقف می شوند و برای آنها نسخه های async در نظر گرفته شده است تا در آن حالت بتوانید استفاده کنید .

برای استفاده از نسخه async گروه دوم فقط کافی است پیشوند a را به اول اسم آنها اضافه کنید . با استفاده از تمایز ها می توانید بفهمید چگونه کد های asynchronous بنویسید . مثال زیر یک کد asynchronous معتبر است :

user = await User.objects.filter(username=my_input).afirst()

در کد بالا , ()filter یک queryset جدید را بازمیگرداند پس شما می توانید آن را بدون هیچ مشکلی در محیط asynchronous استفاده کنید . ولی متد ()first مجبور است تا query را در دیتابیس اجرا کند و رکرود اول آن را به شما بازگرداند پس شما لازم است برای استفاده از آن در محیط asynchronous از ورژن ()afirst آن استفاده کنید . در آخر نیز باید کد را به صورت await به شکل بالا قرار دهید تا همه چیز درست کار کند . (مباحث wait و await جزو دروس ما نیستند)


بررسی Transactionsدر async

در محیط asynchronous شما نمی توانید از transactions استفاده کنید . این به این دلیل است که جنگو هنوز به درستی از آنها در این محیط پشتیبانی نمی کند . اگر شما این کار را انجام بدهید , با خطای SynchronousOnlyOperation برخورد می کنید .

اگر شما نیاز به استفاده از Transactions در این محیط دارید ,پیشنهاد می شود آن را به صورت ORM و به صورت یک تابع synchronous بنویسید و سپس آن را با استفاده از sync_to_async فراخوانی کنید .


بررسی query در JSONField

جستجو در میان JSONField متفاوت است (به دلیل وجود Key Transformations) . برای بررسی این موضوع در ادامه از مثال زیر استفاده خواهیم کرد .

from django.db import models

class Dog(models.Model):
    name = models.CharField(max_length=200)
    data = models.JSONField(null=True)
    
    def __str__(self):
        return self.name


بررسی None در JSONField

همانطور که می دانید ذخیره کردن نوع داده ای None به عنوان مقدار یک فیلد باعث می شود تا در دیتابیس مقدار آن به صورت SQL NULL ذخیره شود . با اینکه توصیه نمی شود ولی شما می توانید نوع داده ای null (به صورت JSON) را بجای SQL NULL وارد دیتابیس کنید . فقط کافی است از Value(‘null’) استفاده کنید .

البته که هنگامی که داده ها از دیتابیس دریافت می شوند ,پایتون جفت SQL NULL و JSON null را به صورت None یا خالی نمایش می دهد . پس تمایز بین آنها کمی دشوار است .

البته باید گفت که اگر None درون یک لیست یا دیکشنری باشد همیشه به عنوان JSON null در نظر گرفته می شود . همچنین هنگام query و جستجو مقدار None همیشه به عنوان JSON null تفسیر می شود . برای جستجو و query با SQL NULL از isnull استفاده کنید .

>>> Dog.objects.create(name='Max', data=None) # SQL NULL.
<Dog: Max>
>>> Dog.objects.create(name='Archie', data=Value('null')) # JSON null.
<Dog: Archie>
>>> Dog.objects.filter(data=None)
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data=Value('null'))
<QuerySet [<Dog: Archie>]>
>>> Dog.objects.filter(data__isnull=True)
<QuerySet [<Dog: Max>]>
>>> Dog.objects.filter(data__isnull=False)
<QuerySet [<Dog: Archie>]>
نکته : اگر مطمئن نیستید که قرار است با مقادیر null کار داشته باشید بهتر است null را در فیلد روی False قرار داده و مقدار پیش فرضی را برای فیلد های خالی به صورت default=dict تنظیم کنید .
نکته : ذخیره کردن JSON null ,مقدار null=False را نقض نمی کند .


مفهوم Key , index و path در transforms

مقدار JSON در پایتون مانند یک دیکشنری است . پس برای استفاده از آن فقط کافی است تا Key را به عنوان نام lookup استفاده کنید . برای مثال به مثال زیر نگاه کنید :

>>> Dog.objects.create(name='Rufus', data={
...  'breed': 'labrador',
...  'owner': {
...  'name': 'Bob',
...  'other_pets': [{
...  'name': 'Fishy',
...  }],  },  })
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': None})
<Dog: Meg>
>>> Dog.objects.filter(data__breed='collie')
<QuerySet [<Dog: Meg>]>

می توانید همچنین چندین Key را به هم متصل کنید تا lookup دقیق تری شکل بگیرد .

>>> Dog.objects.filter(data__owner__name='Bob')
<QuerySet [<Dog: Rufus>]>

اگر به عنوان Key یک عدد را بدهید , جنگو آن را به عنوان ایندکس در نظر خواهد گرفت .

>>> Dog.objects.filter(data__owner__other_pets__0__name='Fishy')
<QuerySet [<Dog: Rufus>]>

اگر Key شما با نام lookup دیگری در تضاد باشد ,میتوانید به جای آن از contains استفاده کنید .

برای جستجو missing Keys یا کلید های گمشده از isnull استفاده کنید :

>>> Dog.objects.create(name='Shep', data={'breed': 'collie'})
<Dog: Shep>
>>> Dog.objects.filter(data__owner__isnull=True)
<QuerySet [<Dog: Shep>]>
نکته : شما همچنین می توانید Key را با lookup هایی مانند icontains, endswith, iendswith, iexact, regex, iregex, startswith, istartswith, lt, lte, gt نیز متصل کنید .
نکته : بخاطر روشی که Key-path استفاده می کند ,متد های ()filter و ()exclude ضمانت نمی شوند که بتوانند رکورد های بازگشتی را درست برگردانند . اگر مسیر Key مشخص نیست ,بهتر است از isnull استفاده کنید .
نکته : در دیتابیس های Oracle و MariaDB استفاده از متد ()order_by رکورد ها را بر اساس str مقدار ها مرتب می کند . دلیل آن این است که این دو ,تابعی درون خود ندارند که بتواند مقادیر JSON را به SQL تبدیل کند .


مفهوم Key lookups و containment

در این بخش درباره اتصال lookup و Key صحبتی خواهیم داشت . (چند lookup اضافه نیز برای JSONField در نظر گرفته شده است .)

  • یک-contains :این متد روی JSONField به صورت متفاوتی کار می کند و بازنویسی شده است . آنها به عنوان مقدار lookup یک دیکشنری را دریافت می کنند تا آن را مقادیر درون فیلد مطابقت بدهند و اگر مقدار داخل فیلد حاوی آنها بود ,رکورد را بازمیگردانند . برای مثال :
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.create(name='Fred', data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contains={'owner': 'Bob'})
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>
>>> Dog.objects.filter(data__contains={'breed': 'collie'})
<QuerySet [<Dog: Meg>]>
نکته : contains در دیتابیس های Oracle و SQLite پشتیبانی نمی شود .
  • دو-contained_by :این دقیقا برعکس contains است . یعنی رکورد هایی را بازمیگرداند که با مقدار ارسال شده توسط شما یا دقیقا مطابقت داشته باشد و یا خالی باشد ! به مثال زیر نگاه کنید :
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.create(name='Fred', data={})
<Dog: Fred>
>>> Dog.objects.filter(data__contained_by={'breed': 'collie', 'owner': 'Bob'})
<QuerySet [<Dog: Meg>, <Dog: Fred>]>
>>> Dog.objects.filter(data__contained_by={'breed': 'collie'})
<QuerySet [<Dog: Fred>]>
نکته : contained_by در دیتابیس های Oracle و SQLite پشتیبانی نمی شود .
  • سه-has_key :رکورد هایی را باز میگرداند که حاوی Key داده شده هستند . برای مثال :
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_key='owner')
<QuerySet [<Dog: Meg>]>
  • چهار-has_any_keys :رکورد هایی را باز میگرداند که هرکدام ار پارامتر های داده شده به عنوان Key در آنها وجود داشته باشند (همان قبلی است ولی چند Key را میتواند بپذیرد) . برای مثال :
>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
<Dog: Rufus>
>>> Dog.objects.create(name='Meg', data={'owner': 'Bob'})
<Dog: Meg>
>>> Dog.objects.filter(data__has_any_keys=['owner', 'breed'])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>


در این جلسه به بررسی چند مفهوم به صورت کوتاه در جنگو پرداختیم . در جلسه بعدی در رابطه با مفهوم متد Q و چندین متد و روش دیگر در جنگو صحبت خواهیم کرد .