آموزش جنگو : جلسه سی | بررسی کوئری های ساده و پیچیده با Q در جنگو

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

آموزش جنگو : جلسه سی | بررسی کوئری های ساده و پیچیده با Q در جنگو
آموزش جنگو : جلسه سی | بررسی کوئری های ساده و پیچیده با Q در جنگو


ایجاد query های پیچیده با Q

جستجو هایی مانند ()filter و غیره همگی آرگومان هایی را به صورت Keyword دریافت می کنند و آنها را به صورت AND اجرا می کنند . این یعنی هر کدام از رکورد ها در نتیجه باید همزمان تمام مقادیر درون جستجو را داشته باشد . اگر نیاز داشته باشید نوع های پیچیده تری از query را بنویسید که برای مثال به صورت OR کار کنند (یعنی یا مقدار اول را دارا باشند یا مقدار دوم) می توانید از Q استفاده کنید .

کلاس Q , یک کلاس است که از آن نمونه سازی می شود . آن را می توانید از django.db.models.Q داشته باشید . شما با این کلاس می توانید queryset خود را به همراه lookup درونش , کپسوله سازی کنید .

برای مثال شی کلاس Q زیر , یک LIKE را کپسوله سازی می کند . (lookup و query درون آن قرار دارند)

from django.db.models import Q
Q(question__startswith='What')

اشیا Q را می توان با عملگر های ^ و & و | ترکیب نمود . وقتی یک عملگر روی دو شی Q استفاده می شود ,یک شی Q جدید را ایجاد می کند . (عملگر ها می توانند شی های Q را با هم ترکیب کنند)

به عنوان مثال ,این عبارت یک شی Q را می سازد که OR را بر روی دو query حاوی question__startswith ,اجرا می کند . (عملگر ‘یا’ استفاده شده است پس هر کدام از شروط زیر برقرار باشد , رکورد برگردانده می شود)

Q(question__startswith='Who') | Q(question__startswith='What')

کد بالا معادل دستور SQL زیر است :

WHERE question LIKE 'Who%' OR question LIKE 'What%'

می توانید با ترکیب عملگر های مختلف ,عبارات جستجوی پیچیده تری بنویسید . همچنین می توانید از ~ در قبل شی Q استفاده کنید تا دستور NOT اجرا شود و شرط شما نفی شود . برای مثال :

Q(question__startswith='Who') | ~Q(pub_date__year=2005)

هر نوع از lookup functions و یا همان متد های جستجو (query) که از شما Keword دریافت می کنند ,مانند ()filter و یا ()exclude همچنین می توانند یک یا چند نمونه Q را به عنوان آرگومان دریافت کنند . اگر چندین نمونه Q را با هم به تابع یا متد ارسال کنید ,دستور AND روی آنها فراخوانی خواهد شد . مثلا :

Poll.objects.get(
    Q(question__startswith='Who'),
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)

کد بالا به صورت کد SQL زیر خواهد بود :

SELECT * from polls WHERE question LIKE 'Who%'
AND (pub_date = '2005-05-02' OR pub_date = '2005-05-06')

شما می توانید همچنین بجای استفاده از دو نمونه Q , از یک lookup Keyword و یک Q استفاده کنید و نتایج هنوز درست هستند . با این حال به یاد داشته باشید که همه ی آنها دستور AND را خواهند داشت (یعنی همه شروط و آرگومان ها در خروجی باید رعایت شوند) . برای مثال :

Poll.objects.get(
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6)),
    question__startswith='Who',
)
نکته :به یاد داشته باشید اگر کد شما به شکل بالا بود و همزمان دارای آرگومان های Keyword و Q بود ,نمونه های Q ابتدا قرار می گیرند . برای مثال کد بالا درست است . اما کد زیر یک کد نامعبتر است زیرا این دستور در آن رعایت نشده است .
# INVALID QUERY
Poll.objects.get(
    question__startswith='Who',
    Q(pub_date=date(2005, 5, 2)) | Q(pub_date=date(2005, 5, 6))
)


مقایسه شی ها

برای مقایسه دو شی با هم از یک مدل ,از عملگر استاندارد پایتون (==) استفاده کنید . در پشت صحنه ,جنگو مقدار primary key هر دو شی را با یکدیگر مقایسه خواهد کرد .

پس با توجه به این ,دو عبارت زیر برابر خواهند بود :

>>> some_entry == other_entry
>>> some_entry.id == other_entry.id

اگر فیلد id شما primary key نباشد و شما آن را تغییر داده باشید ,مشکلی نیست . هر فیلدی که primary key مدل شما باشد , برای مقایسه استفاده خواهد شد . برای مثال اگر فیلد primary key شما name باشد ,دو عبارت زیر برابر هستند :

>>> some_obj == other_obj
>>> some_obj.name == other_obj.name


حذف شی ها

متد ()delete برای حذف یک شی به کار می رود . این متد بلافاصله شی را حذف می کند و سپس تعداد اشیا حذف شده را به همراه یک دیکشنری و تعداد حذفیات در هر نوع شی درون آن باز میگرداند .

>>> e.delete()
(1, {'blog.Entry': 1})

متد ()delete علاوه بر این می تواند به صورت گروهی نیز اشیا را حذف کند . هر QuerySet یک متد ()delete دارد که تمام اعضای آن QuerySet را برای شما حذف خواهد کرد .

برای مثال ,کد زیر تمام اشیا مدل Entry را که فیلد pub_date آنها برابر با 2005 است را حذف می کند :

>>> Entry.objects.filter(pub_date__year=2005).delete()
(5, {'webapp.Entry': 5})

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

وقتی جنگو یک شی را حذف می کند رفتار ON DELETE CASCADE را در SQL شبیه سازی می کند . برای مثال :

b = Blog.objects.get(pk=1)
# This will delete the Blog and all of its Entry objects.
b.delete()

در مثال بالا هر شی که به وسیله ForeignKey به سمت این شی رابطه داشته باشد ,نیز حذف خواهد شد . این رفتار از طریق آرگومان on_delete می تواند تغییر پیدا کند .

به یاد داشته باشید که دلیل وجود نداشتن متد ()delete روی manager و اینکه کد ها به شکل ()Entry.obejcts.delete وجود ندارند این است که جنگو می خواهد از تصادفی حذف کردن تمام اشیا به طور اشتباهی جلوگیری کند . پس برای حذف یک دسته از اشیا , باید ابتدا آنها را از دیتابیس دریافت کنید و سپس متد ()delete را از QuerySet فراخوانی کنید .


کپی کردن اشیا

اگر چه جنگو روش خاصی را برای کپی کردن یک شی و ایجاد یک شی جدید از همان ندارد ولی , شما می توانید به راحتی فیلد pk را روی None و state.adding_ را روی True تنظیم کنید و شی جدیدی را با مقادیر فیلد های شی قدیمی ایجاد کنید . (کپی می شود) برای مثال :

blog = Blog(name='My blog', tagline='Blogging is easy')
blog.save() # blog.pk == 1
blog.pk = None
blog._state.adding = True
blog.save() # blog.pk == 2

گرچه اوضاع هنگامی که از ارث بری استفاده کنید ,پیچیده تر می شود . برای مثال اگر مدل زیر زیرکلاسی از Blog باشد :

class ThemeBlog(Blog):
    theme = models.CharField(max_length=200)
    django_blog = ThemeBlog(name='Django', tagline='Django is easy', theme='python')
     django_blog.save() # django_blog.pk == 3

با توجه به اینکه وراثت شما چگونه کار می کند باید pk و id را به صورت همزمان روی None و state.adding_ را روی True قرار دهید . به مثال زیر نگاه کنید :

django_blog.pk = None
django_blog.id = None
django_blog._state.adding = True
django_blog.save() # django_blog.pk == 4

در طول فرایند کپی کردن ,فیلد های روابطی کپی نخواهند شد . پس شما نیاز دارید تا آنها را جدا کپی و وصل کنید . برای مثال Entry یک ManyToManyField دارد که با Author رابطه پیدا کرده است . بعد از تعریف کردن entry در کد زیر ,شما باید آن را برای شی جدید تنظیم کنید :

entry = Entry.objects.all()[0]
# some previous entry
old_authors = entry.authors.all()
entry.pk = None
entry._state.adding = True
entry.save()
entry.authors.set(old_authors)

البته توجه داشته باشید که برای OneToOneField این قانون به طور عادی اجرا نمی شود (به دلیل اینکه شما نمی توانید دو شی یکسان برای یک فیلد OneToOne داشته باشید) . برای مثال اگر entry تعریف شده در کد بالا را در مثال زیر نیز داشته باشیم , به صورت زیر باید عمل کنیم :(باید به صورت دستی خودمان شی ای را برای فیلد تنظیم کنیم)

detail = EntryDetail.objects.all()[0]
detail.pk = None
detail._state.adding = True
detail.entry = entry
detail.save()


آپدیت کردن چند فیلد همزمان (به یکباره)

گاهی می خواهید یک مقدار خاص را برای یک فیلد خاص ,در تمامی اشیا یک QuerySet تنظیم کنید . برای این کار از متد ()update استفاده می شود :

# Update all the headlines with pub_date in 2007.
Entry.objects.filter(pub_date__year=2007).update(headline='Everything is the same')

با استفاده از این روش فقط می توانید فیلد های غیر روابطی و ForeignKey را آپدیت کنید . برای آپدیت فیلد های غیر رابطه ای فقط کافیست مقدار جدید را به عنوان مقدار ثابت تنظیم کنید . برای آپدیت فیلد های ForeignKey نیز کافی است تا شی جدیدی که می خواهید به آن اشاره کنید را به عنوان مقدار تنظیم کنید .برای مثال :

>>> b = Blog.objects.get(pk=1)
# Change every Entry so that it belongs to this Blog.
>>> Entry.objects.update(blog=b)

متد ()update فورا بر دیتابیس اعمال می شود و تعداد سطرهایی را که با query منطبق هستند را بازمیگرداند (که ممکن است برابر با تعداد ستون های آپدیت شده نباشد زیرا بعضی از ستون ها از قبل مقدار جدید دارند) . تنها محدودیتی که در اینجا دارید این است که شما فقط روی جدول اصلی دیتابیس می توانید این عملیات ها را انجام دهید . این یعنی از فیلد های روابطی نمی توانید به طور reverse استفاده کنید . برای مثال :

>>> b = Blog.objects.get(pk=1)
# Update all the headlines belonging to this Blog.
>>> Entry.objects.filter(blog=b).update(headline='Everything is the same')

توجه کنید که استفاده از ()update مستقیما یک دستور SQL را تولید می کند , پس هیچ عملیات ()save یا سیگنال های pre_save و post_save را فراخوانی نمی کند (یا auto_now را نادیده می گیرد) . پس اگر می خواهید از ذخیره شدن عملیات بر روی دیتابیس مطمین شوید ,فقط کافیست روی QuerySet خود یک حلقه بزنید و آنها را تک به تک ()save کنید . برای مثال :

for item in my_queryset:
    item.save()

همچنین می توانید روی یک F expression نیز متد ()update را فراخوانی کنید . این زمانی مفید است که بخواهید مقدار یک فیلد را بر اساس یک فیلد دیگر از همان مدل به روز رسانی کنید . برای مثال برای افزایش تعداد pingback به ازای هر شی entry در مدل Blog :

>>> Entry.objects.update(number_of_pingbacks=F('number_of_pingbacks') +1)
نکته : اگر تلاش کنید از join (استفاده از _ برای اتصال به فیلد هایی از شی ارجاع داده شده در فیلد رابطه ای) در توابع ()F در زمان آپدیت استفاده کنید ,یک FieldError دریافت خواهید کرد :
# This will raise a FieldError
>>> Entry.objects.update(headline=F('blog__name'))


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