مشکل کندی در اپلیکیشن

اخیرا یک پروژه با بک اند Django رو review کردیم که مشکل کندی داشت، کارفرما دسترسی به سرور رو نداد و قرار شد توی کد ببینیم مشکل از کجاست.

به خوبی از امکانات جنگو استفاده شده بود، از کتابخانه خارجی بی مورد استفاده نشده بود و از ماژولهای داخلی جنگو استفاده شده بود. ساختار پروژه استاندارد بود و تغییرات من در آوردی (به اسم خلاقیت) نداشت و توسعه دهنده کاملا طبق مستندات رسمی جلو رفته. Model ها روابط پیچیده نداشتند، به بهترین شکل از ORM پیشفرض جنگو استفاده شده بود، مشکلی توی query هایی که توی View ها زده بود دیده نمی شد (مشکل n+1 رو توی دسترسی ها به دیتابیس رو بررسی کردیم، Query تکراری نداشت ) و …

Docker رو تست کردیم کانشکن بین container ها کند نبود، منابع کم نبود.

در نهایت به سرور دیتابیس و production سری زدیم، کاری که از اول باید میکردیم. توی dockerfile خود اپلیکیشن دیدیم اون رو با runserver اجرا کرده که طبیعیه که سرعت خیلی پایین باشه چون این صرفا برای توسعه و تست هست نه production. با Gunicorn دیپلوی کردیم و مشکل کندی حل شد.

Gunicorn پردازش concurrent رو میده و میشه بهش بگی روی چند تا worker اجرا بشه. Scalable هست و یه سری قابلیت دیگه که برای محیط عملیاتی نیازه مثل مدیریت لاگ و ارورها و …

ریزکاریهای تست جنگو با TransactionTestCase و TestCase

در پست قبل دیدیم که برای نوشتن تست در جنگو میتونیم از کلاس های django.test.TestCase و django.test.TransactionTestCase استفاده کرد (البته که از خود unittest پایتون هم می توان استفاده کرد اما این کار در جنگو منطقی نیست چرا که کلاسهای تعریف شده برای تست در جنگو قابلیت های زیادی برای تست اضافه کرده اضافه کرده. مثلا اگر با unittest پایتون استاندارد بخاید تستی بنویسید که نیاز به دسترسی دیتابیس یا اجرا اپلیکیشن وب داشته باشد قطعا به مشکل میخورید بنابراین هیچ کس این کار رو نمیکنه). دیدیم که یک کلاس دیگه به اسم django.test.LiveTestCase هم وجود داره که میتونید توی اون از کلاینت Selenium استفاده کنید که تست های لایو توی مرورگر رو داشته باشید.

سوالی که پیش میاد اینه که فرق TestCase و TransactionTestCase چیه؟ توی خود کد جنگو نوشته:

میگه هردو مثلا همه و توی بیشتر شرایط باید TestCase رو ترجیح بدید چون سریع تره مگر در شرایطی خاص باید TransactionTestCase رو استفاده کنید مثلا اونجا که تستی که مینویسید تراکنش به دیتابیس داره و باید انجام بشه. میگه توی دیتابیسی که از تراکنش پشتیبانی نمیکنه عملا هر دو یکی هستن مثلا دیتابیس MySQL با موتور MyISAM.

توی مستندات جنگو گفته که TransactionTestCase از SimpleTestCase ارثبری کرده و چنتا قابلیت استفاده کرده. مثلا دیتابیس رو بعد از هر متد تست به حالت تعریف شده بر میگردونه که این توی تست مهمه مثلا 100 تا تابع تست داریم باید بعد از هر کدوم دیتابیس ریست بشه که روی تست های دیگه تاثیر نذاره. TransactionTestCase اطلاعات رو ممکنه commit و rollback کنه و بعد از هر تست هم حتما دیتابیس رو truncate میکنه اما TestCase برای سرعت بیشتر truncate نداره و از تراکنش ها استفاده میکنه یعنی rollback میکنه. این کار باعث میشه توی تستهای که نیاز به تراکنش هست به مشکل بخورید مثلا select_for_update ها.

در کل چیزی که من تجربه کردم اگر توی توابع تست یک کلاس تستکیس، update یا insert دیتابیس دارید از TransactionTestCase استفاده کنید. اگر فقط select دارید از TestCase استفاده کنید که سرعت بیشتری داره. اگر کلا با دیتابیس کاری ندارید از SimpleTestCase استفاده کنید.

کار کردن بهینه با دیتابیس جنگو

راه های بهینه استفاده کردن دیتابیس:

1.توی query هایی که با استفاده از ORM بر روی دیتابیس اجرا میکنیم بهتر است خود SQL آن را ببینیم با استفاده از پرینت کردن query

2.از Indexing در ORM استفاده کنیم

3.وضعیت بهینگی query ها را ببینیم با استفاده از ابزارهایی مثل django-debug-tools

4.مراقب مشکلاتی مثل N+1 problems باشیم

5.از caching استفاده کنیم

6. از قابلیت query laziness در QuerySet های Django نهایت استفاده را بکنیم

7.Query تکراری اجرا نکنیم

8.QuerySet.explain() بهینگی و زمان اجرا query را به من نشان می دهد

9.به حداقل رساندن DB hit با استفاده از prefetch_related و select_related

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

https://docs.djangoproject.com/en/4.2/topics/db/optimization/

کاربرد nested method در پایتون

کد زیر رو می تونید توی django.forms.fields.py ببینید. یه نکته جالب داره. متد split_url رو توی متد to_python تعریف کرده:

سوال اینجاست که چرا باید یه متد رو توی متدی دیگه تعریف کرد؟ کاربردش چیه؟ مزایاش چیه؟ و چجوری باید صدا زد اونها رو:

دلیل استفاده از این روش توی OOP گفته شده که اگر قرار باشه که یک کار رو چندین بار توی یک متد انجام داد باید اون رو یک به صورت یک تابع بنویسیم تا اینجا که واضحه (دلیل تابع). حالا فرض کنیم بخایم از یک تابع فقط توی یک متد از یک کلاس استفاده کنیم، یعنی اصلا حتی به متدهای دیگه همون کلاس ربطی نداره. اون وقته که منطقی میشه از این روش استفاده کرد و فقط هم باید توی همون متد to_python صدا زده بشه.

مزایاش که مشخصه، اگه کدی لازم نباشه به scope های دیگه نشون داده نمیشه و مزایای دیگه…

ترتیب اجرای متدهای فیلدها توی django.forms

در مورد صحتسنجی فیلدهایی که هنگام اجرای یک form توی جنگو طی میشه ترتیب زیر طی میشه که هر کدوم از اونها رو میشه overwrite کرد که رفتار form وقتی که دیتا میگیره تغییر بدیم

این توابع رو توی کلاس Field از django.forms میتونید ببینید. لینک زیر بدرد بخوره:

https://docs.djangoproject.com/en/4.2/ref/forms/validation/

F() expression توی جنگو

تفاوت دو کد زیر چیست؟

استفاده از F() expression توی کد جنگوی بالا این امکان رو میده که تغییر فیلد و increment کردنش سمت دیتابیس آماده انجام بشه بجای اینکه پایتون یک واحد بهش اضافه کنه. یعنی یک query میره سمت دیتابیس و مقدار فیلد هرچی هست همونجا یه واحد اضافه میشه. اینجوری دیگه لازم نیست یک بار از دیتابیس بگیره و اضافه کنه و آپدیت شده رو بفرسته سمت دیتابیس و دو query اجرا کنه. اما برای اینکه مقدار آپدیت شده سمت دیتابیس را داشته باشیم:

چالش ORM جنگو – spanning multivalued relationship

در رابطه های ManyToMany یا OneToMany استفاده کردن از spanning داخل filter به model دیگر این سوال رو ایجاد میکنه که هر دو شرط باید روی یک آبجکت اجرا شه یا هر کدام از شرط ها جدا؟ کد زیر توی مستندات جنگو روشن این موضوع رو نشون داده:

تکه کد ORM جنگو – Queryset slicing – سوال مصاحبه

آیا ORM جنگو با اجرای کد زیر به دیتابیس query میزند؟ اگر آره چند Hit دیتابیس داریم؟

ORM جنگو lazy است یعنی در هر حالتی به دیتابیس دسترسی نمیگیرد مگر اینکه بخواهد queryset را کامل از دیتابیس بگیرد. درحالتی که از slicing پایتون برای یک queryset استفاده می کنیم ORM یک queryset جدید برمیگردونه و اون رو اجرا نمیکنه. یعنی توی خط اول به دیتابیس Hit نداریم. قاعدتا باید توی خط دوم هم همین باشه اما slicing پایتون با step استثنا هست و توی خط دوم یه Hit به دیتابیس داریم.

تکه کد – سوال مصاحبه

کدهای زیر با هم چه تفاوتی دارند؟

در شرط اول QuerySet اجرا نشده و فقط یک دستور از دیتابیس گرفته شده که آیا این queryset عضوی دارد یا خیر. جنگو این بررسی رو در بهینه ترین و کوتاه ترین حالت انجام میده و از حالت دوم که queryset رو اجرا کرده کمی سریع تره. نکته اینه که در حالتی که میدونیم کهم در هر صورت در ادامه برنامه قراره my_queryset رو اجرا کنیم (چه عضو داشته باشه چه نه) بهتره که از حالت دوم استفاده کنیم که دیگه یک بار اضافه از exists استفاده نکرده باشیم. چون همینطور که میدونیم اگر my_queryset رو از دیتابیس بیگیریم دفعات بعدی دیگه از دیتابیس نمیگیریم و cache میشه.

همین داستان رو برای تابع contains() هم داریم.

مشکل N+1 – سوال مصاحبه (دوم)

کد زیر چند Hit دیتابیس دارد؟

همچنان N+2!!!

حتی با وجود اینکه از prefetch_related استفاده شده است دوباره در داخل حلقه به N دسترسی مجدد به دیتابیس وجود دارد. چرا که prefetch_related بالا برای Pizaa.objects.all بوده ولی پایین از filter استفاده کردیم که ORM جنگو دسترسی بالایی را در نظر نمیگیرد و دوباره query میزند. اتفاقا بدتره چرا که یه query بالا هم اضافی هست. راه حل زیر درست میشه:

یا