آشنایی با Query و QuerySet در جنگو (قسمت سوم) – select_related() و prefetch_related()

زمان مطالعه: 8 دقیقه
آشنایی با کوئری ست union() در جنگو :

union(*other_qsall=False)

از عملگر union در SQLاستفاده می کند تا نتایج دو یا چند کوئری ست را با هم ترکیب کند، برای مثال:

>>> qs1.union(qs2, qs3)

عملگر UNION به صورت پیش فرض فقط مقادیر distinct را انتخاب می کند. برای اجازه دادن به داده های تکراری باید از آرگومان all=True استفاده کنید.

union()intersection(), و difference() نمونه مدلی از نوع QuerySet اول را بر می گرداند، حتی اگر آرگومان ها کوئری ستی از مدل های دیگر باشد. برای مثال:

>>> qs1 = Author.objects.values_list('name')
>>> qs2 = Entry.objects.values_list('headline')
>>> qs1.union(qs2).order_by('name')
آشنایی با select_related() در جنگو کوئری ست:

select_related(*fields)

کوئری ستی بر می گرداند که روابط کلید خارجی (foreign-key relationships) را دنبال می کند، زمانی که کوئری را اجرا می کند، داده های Objectهای مرتبط را انتخاب می کند. از لحاظ کارایی select_related از نوع بوستر می باشد، چرا که اگر چه کوئری پیچیده ای دارد، منتهی برای استفاده از روابط کلید خارجی در آینده نیازی به کوئری روی پایگاه داده ها نیست و بسیار مفید است.

مثال پیش رو، تفاوت بین جستجوی ساده و جستجوی select_relatedرا نشان می دهد:

# Hits the database.
e = Entry.objects.get(id=5)

# Hits the database again to get the related Blog object.
b = e.blog

و اینجا استفاده از select_related :

# Hits the database.
e = Entry.objects.select_related('blog').get(id=5)

# Doesn't hit the database, because e.blog has been prepopulated
# in the previous query.
b = e.blog

شما می توانید select_related() را با هر کوئری ستی از Objectsها استفاده کنید:

from django.utils import timezone

# Find all the blogs with entries scheduled to be published in the future.
blogs = set()

for e in Entry.objects.filter(pub_date__gt=timezone.now()).select_related('blog'):
    # Without select_related(), this would make a database query for each
    # loop iteration in order to fetch the related blog for each entry.
    blogs.add(e.blog)

ترتیب filter() و select_related() مهم نیست. کوئری ست زیر مساوی هم هستند:

Entry.objects.filter(pub_date__gt=timezone.now()).select_related('blog')
Entry.objects.select_related('blog').filter(pub_date__gt=timezone.now())

شما می توانید کلید های خارجی را به روش مشابه دنبال کنید تا آنها را کوئری کنید. بر فرض اگر شما مدل های زیر را داشته باشید:

from django.db import models

class City(models.Model):
    # ...
    pass

class Person(models.Model):
    # ...
    hometown = models.ForeignKey(
        City,
        on_delete=models.SET_NULL,
        blank=True,
        null=True,
    )

class Book(models.Model):
    # ...
    author = models.ForeignKey(Person, on_delete=models.CASCADE)

حال، یک فراخوانی به Book.objects.select_related(‘author__hometown’).get(id=4) ، شخص (Person) و شهر ( City) مرتبط را نیز کَش می کند.

# Hits the database with joins to the author and hometown tables.
b = Book.objects.select_related('author__hometown').get(id=4)
p = b.author         # Doesn't hit the database.
c = p.hometown       # Doesn't hit the database.

# Without select_related()...
b = Book.objects.get(id=4)  # Hits the database.
p = b.author         # Hits the database.
c = p.hometown       # Hits the database.

شما می توانید به هر کلید خارجی (ForeignKey) یا یک به یک ( OneToOneField) در لیست فیلد های داده شده به select_related() مراجعه داشته باشید.

شما همچنین می توانید به جهت برعکس از یک OneToOneField  که درون لیست فیلد های ورودی به select_relatedداده شده است، مراجعه کنید. به این معنی که شما می توانید OneToOneField را به شی ای که فیلد بر روی آن تعریف شده است، برگردانید. به جای مشخص کردن نام فیلد، از نام مرتبط برای فیلد روی شئ مرتبط استفاده کنید.

شرایطی وجود دارد که شما می خواهید روی تعداد زیادی از objectهای مرتبط select_related بزنید یا اینکه اصلا ندونید چند تا relationوجود دارد، در این موارد می توانید select_related() را بدون آرگومان بفرستید. در این حالت تمام رابطه هایی که کلید خارجی شان خالی نباشد را دنبال می کند. این کار در همه جا پیشنهاد نمی شود، چرا که منجر به کوئری بسیار پیچیده ای می شود.

اگر قصد دارید لیست فیلد های مرتبط که توسط فراخوانی select_related قبلی روی QuerySet اضافه شده اند را پاک کنید، می توانید None را به عنوان پارامتر بفرستید:

>>> without_relations = queryset.select_related(None)
آشنایی به prefetch_related() در کوئری ست های Django:

prefetch_related(*lookups)

یک کوئری ست بر می گرداند که به صورت خودکار ، در یک batch تنها، object های مرتبط با هر جستجوی مشخص را استخراج می کند.

هدفی مشابه با select_related وجود دارد، بدین صورت که هر دو طراحی شده اند تا سیل کوئری های دیتابیس را که به دلیل دسترسی به objectهای مرتبط انجام می شود را متوقف کنند، اما استراتژی مقداری متفاوت است.

select_related با ایجاد یک SQL join و شامل شدن فیلد های objectمرتبط در دستور SELECT کار می کند. به همین دلیل، select_related اشیاء مربتط را در یک کوئری یکسان می گیرد (نیاز به کوئری دوم نداریم). گرچه، جهت پرهیز از مجموعه نتایج خیلی بزرگ که نتایج حاصل از join شدن از طریق روابط manyمی باشند، select_related محدود به روابط از نوع OneToOne و ForeighKey شده است.

از سوی دیگر، prefetch_related برای هر رابطه (relation) یک جستجو (lookup) جداگانه انجام می دهد و عمل «joining» را در پایتون انجام میدهد. این امکان را فراهم می کند تا objectهای many_to_many و many_to_one را prefetch کند، که توسط select_related قابل انجام نیست، (علاوه بر روابط foreign key و one-to-one که در select_related پشتیبانی می شود.)

همچنین از prefetch کردن GenericRelation و GenericForeignKey پشتیبانی می کند. گرچه حتما بابد به مجموعه نتایج homogeneous محدود شده باشد. برای مثال، prefetch کردن اشیائی(Objects) که توسط GenericForeignKey ارجاع شده اند، تنها در حالتی پشتیبانی می شوند که کوئری به یک ContentType محدود شده باشد.

برای مثال، فرض کنید دارای مدل های زیر هستید:

from django.db import models

class Topping(models.Model):
    name = models.CharField(max_length=30)

class Pizza(models.Model):
    name = models.CharField(max_length=50)
    toppings = models.ManyToManyField(Topping)

    def __str__(self):
        return "%s (%s)" % (
            self.name,
            ", ".join(topping.name for topping in self.toppings.all()),
        )

و اجرا کنید:

>>> Pizza.objects.all()
["Hawaiian (ham, pineapple)", "Seafood (prawns, smoked salmon)"...

مشکل با این کد، به ازای هر باری که Pizza.__str__()  از self.toppings.all() درخواست می کند، کوئری روی دیتابیس داریم، در نتیجه Pizza.objects.all() کوئری ای روی Toppings table برای هر آیتم ای که در کوئری ست Pizzaمی باشد، انجام می دهد.

ما می توانیم تنها به دو کوئری با استفاده از prefetch_related تقلیل دهیم:

>>> Pizza.objects.all().prefetch_related('toppings')

این به معنای self.toppings.all() برای هر پیتزا است. الان، هردفعه که self.toppings.all() فراخوانی شود، بجای اینکه برای هر دفعه به دیتابیس مراجعه کنیم، آنها را در یک کَش کوئری ست prefetched که توسط یه کوئری جمع شده است، پیدا می کند.

به این معنا که تمامی toppings مربوطه، در یک کوئری واکشی می شوند و استفاده می شوند تا کوئری ست هایی که دارای یک حافظه پنهان از قبل پر شده نتایج مروبطه هستند را بسازد، این کوئری ست ها سپس در فراخوانی های self.toppings.all() استفاده می شوند.

کوئری های اضافی در prefetch_related()  پس از اینکه QuerySet شروع به ارزیابی می شود و کوئری اصلی اجرا می شود، اجرا می شوند.

اگر شما نمونه مدل های تکرار پذیر داشته باشید، شما می توانید صفت های مرتبط رو آن نمونه ها را با استفاده از تابع prefetch_related_objects() ، واکشی- prefetch کنید.

توجه داشته باشید، که حافظه پنهان نتیجه حاصل از کوئری ست اولیه و تمامی object های مرتبط مشخص شده است، سپس به صورت کامل داخل memoryبارگیری خواهند شد. این عمل، رفتار طبیعی کوئری ست که به صورت عادی تلاش می کند تا از بارگیری همه ی objectها داخل memory قبل از نیاز به آنها جلوگیری کند را تغییر می دهد، حتی بعد از اجرای کوئری روی پایگاه داده.

نکته:

توجه داشته باشید که همواره با کوئری ست ها، هر متد زنجیره ای بعدی، که دلالت بر کوئری پایگاه داده متفاوت داشته باشد، نتایج قبلی داخل حافظه پنهان را نادیده می گیرد و داده را به استفاده از یک کوئری پایگاه داده تازه نفس، مجدد استخراج می کند. در نتیجه اگر مانند زیر بنویسید:

>>> pizzas = Pizza.objects.prefetch_related('toppings')
>>> [list(pizza.toppings.filter(spicy=True)) for pizza in pizzas]

این حقیقت که pizza.toppings.all() به صورت prefetch شده است، به درد شما نخواهد خورد و به شما کمکی نمی کند. prefetch_related(‘toppings’) به طور ضمنی pizza.toppings.all() را انجام می دهد، اما pizza.toppings.filter() یک کوئری جدید و متفاوت است. حافظه پنهان prefetch اینجا کمکی نمی کند؛ در حیقت به کارایی لطمه وارد می کند، چون شما یک کوئری روی پایگاه داده انجام داده اید که از آن استفاده ای نکرده اید. پس از این ویژگی با دقت استفاده نمایید.

همچنین اگر شما متدهای تغییر پایگاه داده add(), remove(), clear() یا set(), یا related managers ها را فراخوانی نمایید، هر حافظه پنهان prefetchشده که برای رابطه ها کش شده است، پاک می شود.


شما همچنین از Joinعادی برای فیلد های مرتبط روی فیلدهای مرتبط استفاده می کنید. در نظر بگیرید که ما یک مدل اضافه در مثال فرضی بالا داریم:

class Restaurant(models.Model):
    pizzas = models.ManyToManyField(Pizza, related_name='restaurants')
    best_pizza = models.ForeignKey(Pizza, related_name='championed_by', on_delete=models.CASCADE)

دستور پیش رو، مجاز است:

>>> Restaurant.objects.prefetch_related('pizzas__toppings')

تمامی pizza های مرتبط با رستوران ها را prefetxch کرده و تمامی toppingهای متعلق به اون Pizza ها را. این نتیجه حاصل از سه کوئری روی پایگاه داده می شود- یکی برای رستوران ها – یکی برای pizza ها و یکی برای toppingها.

>>> Restaurant.objects.prefetch_related('best_pizza__toppings')

این خط کد، بهترین پیتزا و تمامی topping های بهترین پیتزا را برای هر رستوران واکشی(fetch) می کند. این کار با سه کوئری روی پایکاه داده انجام می شود. یکی برای رستوران ها – یکی برای بهترین پیتزا ها و یکی برای topping ها.

رابطه best_pizza همچنین توسط select_related می تواند واکشی شود تا تعداد کوئری ها به دو کاهش یابد:

>>> Restaurant.objects.select_related('best_pizza').prefetch_related('best_pizza__toppings')

به دلیل آنکه prefetch بعد از کوئری اصلی اجرا شده است (که join های مورد نیاز توسط select_related را در نظر گرفته است)، می تواند تشخیص دهد کهobjectهای best_pizzad همچنین fetchشده اند و از fetchکردن مجدد آنها پرهیز کند.

زنجیره ای کردن فراخوانی های prefetch_related جستجو های (lookups) از قبل prefetch شده را جمع می کند. برای پاک کردن هرگونه رفتاری از prefetch_related ، از فرستادن پارامتر None استفاده نمایید.

>>> non_prefetched = qs.prefetch_related(None)

یک تفاوتی که به هنگام استفاده از prefetch_related باید به آن توجه کنید این است که، object های ایجاد شده توسط یک کوئری، می تواند بین objectهای مختلف که با آنها مرتبط است به اشتراک گذاشته شود. یک نمونه مدل تکی از Python می تواند در بیش از یک نقطه از درخت اشیایی که برگشت داده شده اند، ظاهر شود. این معمولا با روابط ForeignKey رخ می دهد. به طور معمول، این رفتار یک مشکل نخواهد بود، و در حقیقت منجر به صرفه جویی هم در زمان پردازش (CPU Time) و هم در Memory خواهد شد.

prefetch_related در اکثر مواقع با استفاده از SQL کوئری شامل عملگر IN ساخته می شود. به این معنا است که برای کوئری ست های بزرگ، عبارت INبزرگی ایجاد خواهد شد که بستگی به پایگاه داده دارد و از جهت کارایی نیز می تواند به هنگام اجرای SQL کوئری مشکل ساز باشد.

توجه داشته باشید که اگر از iterator() برای اجرای کوئری استفاده کرده اید،فراخوانی های prefetch_related() در نظر گرفته نخواهند شد، چرا که این دو فانکشن مرتبط با بهبود عملکرد، با یکدیگر کار نمی کنند.

در ساده ترین فرم Prefetch برابر با جستجوی های مبتنی بر متن سنتی می باشد:

>>> from django.db.models import Prefetch
>>> Restaurant.objects.prefetch_related(Prefetch('pizzas__toppings'))

شما می توانید یک کوئری ست کاستوم همراه با آرگومان های کوئری آپشنال بسازید.می توان از این برای تغییر دادن چینش پیشفرض کوئری ست، استفاده کرد:

>>> Restaurant.objects.prefetch_related(
...     Prefetch('pizzas__toppings', queryset=Toppings.objects.order_by('name')))

یا در صورت لزوم select_related()  را فراخوانی کنید تا حتی بیشتر تعدادکوئری ها را کاهش دهید:

>>> Pizza.objects.prefetch_related(
...     Prefetch('restaurants', queryset=Restaurant.objects.select_related('best_pizza')))

شما همچنین می توانید نتایج prefetch شده را به یک custom attribute همراه با آرگومان آپشنال to_attr انتصاب دهید. نتایج به صورت مستقیم داخل یک لیست ذخیره خواهند شد.

این کار اجازه می دهد تا یک ارتباط – relationمشابه را چندین بار با کوئری ست متفاوت، prefetch کنید، برای نمونه:

>>> vegetarian_pizzas = Pizza.objects.filter(vegetarian=True)
>>> Restaurant.objects.prefetch_related(
...     Prefetch('pizzas', to_attr='menu'),
...     Prefetch('pizzas', queryset=vegetarian_pizzas, to_attr='vegetarian_menu'))

Lookup های ساخته شده با to_attr سفارشی، همچنان می توانند طبق معمول توسط سایر lookupها استفاده شوند:

>>> vegetarian_pizzas = Pizza.objects.filter(vegetarian=True)
>>> Restaurant.objects.prefetch_related(
...     Prefetch('pizzas', queryset=vegetarian_pizzas, to_attr='vegetarian_menu'),
...     'vegetarian_menu__toppings')

استفاده از to_attr به هنگام فیلتر کردن نتیجه پیش فرض prefetch پیشنهاد می شود زیرا ابهام کمتری نسبت به ذخیره کردن یک نتیجه فیلتر شده درون یک حافظه نهان related manager دارد.

>>> queryset = Pizza.objects.filter(vegetarian=True)
>>>
>>> # Recommended:
>>> restaurants = Restaurant.objects.prefetch_related(
...     Prefetch('pizzas', queryset=queryset, to_attr='vegetarian_pizzas'))
>>> vegetarian_pizzas = restaurants[0].vegetarian_pizzas
>>>
>>> # Not recommended:
>>> restaurants = Restaurant.objects.prefetch_related(
...     Prefetch('pizzas', queryset=queryset))
>>> vegetarian_pizzas = restaurants[0].pizzas.all()

prefetching سفارشی، همچنین با ارتباطات تک رابطه ای همچون ForeignKey یا OneToOneField کار می کند. معمولا شما برای این نوع روابط می خواهید که از  select_related() استفاده کنید، اما مواردی وجود دارد که prefecth کردن توسط یک کوئری ست سفارشی کاربردی تر می باشد:

  • شما می خواهید از یک کوئری ست استفاده کنید که prefetching های بعدی را روی مدل های مرتبط انجام دهد.
  • شما می خواهید تنها قسمتی از objectهای مرتبط را prefetch کنید.
  • شما نیاز دارید از تکنیک های بهبود کارایی همانند  deferred fields استفاده کنید.
>>> queryset = Pizza.objects.only('name')
>>>
>>> restaurants = Restaurant.objects.prefetch_related(
...     Prefetch('best_pizza', queryset=queryset))

زمانی که زا چندین پایگاه داده استفاده می کنید، Prefetch به انتخاب پایگاه داده شما احترام می گذارد. اگر کوئری داخلی پایگاه داده ای را مشخص نکند، از پایگاه داده انتخاب شده توسط کوئری خارجی استفاده می کند. تمامی موارد زیر معتبر می باشد:

>>> # Both inner and outer queries will use the 'replica' database
>>> Restaurant.objects.prefetch_related('pizzas__toppings').using('replica')
>>> Restaurant.objects.prefetch_related(
...     Prefetch('pizzas__toppings'),
... ).using('replica')
>>>
>>> # Inner will use the 'replica' database; outer will use 'default' database
>>> Restaurant.objects.prefetch_related(
...     Prefetch('pizzas__toppings', queryset=Toppings.objects.using('replica')),
... )
>>>
>>> # Inner will use 'replica' database; outer will use 'cold-storage' database
>>> Restaurant.objects.prefetch_related(
...     Prefetch('pizzas__toppings', queryset=Toppings.objects.using('replica')),
... ).using('cold-storage')

افکار خود را به اشتراک گذارید