lazy relationship تو ORM جنگو

اگر توی یک کلاس یک رابطه با کلاس دیگه ای که هنوز تعریف نشده بسازیم بهش میگیم lazy relationship مثل

چون کلاس Book بالاتر تعریف شده بنابراین کلاس Publisher دیده نمیشه و اگه توی ForeignKey کلاس Publisher رو توی دابل کوتیشن نزاریم ارور میگیریم و اینکه گذاشتیم توی دابل کوت “” یعنی گفتیم هنوز تعریف نشده و به این میگن lazy relationship

Hook چیه؟

توی برنامه نویسی شی‌گرا یا OOP که میشه از ارث‌بری استفاده کرد یکی از قابلیتهایی که مخصوصا توی کدهای فریم ورک ها میبینید اینه که توی یک کلاس یک متد تعریف شده و بدنه متد خالیه و عملا این رو گذاشتن که کلاس فرزند بتونه اون رو overwrite بازنویسی کنه که یا قابلیتهایی بهش اضافه کنه یا رفتار متد رو تغییر بده که به متدها میگن Hook.

مثلا توی کلاس BaseModel توی جنگو که کلاس Django.db.models.Model از اون ارثبری کرده یه سری متد وجود داره که میشه بازنوسی کرد مثلا با بازنوسی متد save() وقتی که مدل قراره توی جدول دیتابیس ذخیره میشه، این تابع اجرا میشه که میتونیم توی کلاس مدل اون رو بازنویسی کنیم که رفتارش رو عوض کنیم مثلا قبل از اینکه ذخیره کنه یه خط لاگ هم بندازه. توی این کلاس hook های زیر هم هستن

pre_save()

post_save()

__init__()

delete()

pre_delete()

post_delete()

__str__()

,…

Coupling یا وابستگی -سوال مصاحبه

توی یک مصاحبه سوال زیر از من پرسیده شد که به درستی نتونستم جواب بدم ولی بعدا توی مبحث Refactoring جوابش رو پیدا کردم.

سوال:

فرض کن دوتا App توی جنگو داری که به هم مرتبط هستند. مثلا اگر یک متد توی App اول اجرا میشه بعدش یک متد یا پارامتر توی App دوم رو استفاده میکنه. آیا این از دید مهندسی نرم افزار مشکل داره؟ اگر داره راه حل چیه؟

جواب:

توی بحث Refactoring یکی از مواردی که باید بهش توجه بشه Bad smell های کد هست که یکی از اونها coupling زیاد توی کد هست. یعنی ارتباط زیاد کلاسهای متفاوت با همدیگر به صورتی که مدیریت و تغییر کد را سخت کند.

توی این لینک توضیح داده که اگر یک متد از یک کلاس از دیتای کلاس دیگه استفاده کنه بهش Featuer Envy میگن.

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

این یک پاسخ کلی بود در مورد App های جنگو هم داستان همینه. هر App از نظر مفهومی باید جدا از App های دیگه باشه و هرچی لازم داره توی خود App موجود باشه. اما توی مستندات جنگو یه نکته ای گفته شده که نباید فراموش بشه، رابطه 2 تا مدل که هر کدوم توی یک App جداگانه هستند ممکنه و ایرادی نداره که البته از نظر مفهموی این موردی نداره مثل:

لینک زیر راه حل هایی که نوشتم رو بهتر توضیح داده

لینک 1

لینک 2

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

N+1 query problem وقتی است که برنامه دیتایی که با یک query می توانست به دست بیاره رو N بار بیشتر query بزنه. طبیعتا مشکل پرفورمنسیه.

توی مدیوم یه مقاله هست که این مشکل رو با یک مثال توضیح داده که باعث میشه خیلی خوب مشکل N+1 query رو بفهمیم.

فرض کنید که میخاید کیک درست کنید. یخچال توی آشپزخونه نیست و توی اتاق زیر شیروانیه بنابراین برای برداشتن هر چیزی از تو یخچال باید پله ها رو بالا بریم. شما توی آشپزخونه هستید ولی کتاب شیرینی پزی توی اتاق خوابه.

اول میرید از اتاق خواب کتاب آشپزی رو میارید به آشپزخونه بعد میبینید گفته که به 200 گرم آرد نیاز دارید. میرید اتاق زیرشیروانی و آرد رو میارید. دستور پخت نوشته 3 عدد تخم مرغ، دوباره میرید اتاق زیرشیروانی و 3 تا تخم مرغ میارید. دوباره میبینید که نوشته یه لیوان شیر و ….

میبینید که با این روش شما N بار اضافه رفتید بالا در حالی که همون دفعه اول که رفتید کتاب آشپزی رو بیارد بهتر بود همه وسایل مورد نیاز رو هم بیارید.

حالا یه مثال از ORM جنگو ببینیم که این مشکل رو چطور حل میکنه.

فرض کنید که یه مدل Post داریم که هر Post یه Author داره و طبیعتا هر Author میتونه چند تا Post داشته باشه. رابطه 1->N هست. حالا میخایم پست ها و نویسنده هاشون رو چاپ کنیم.

توی کد بالا مشکل N+1 وجود داره.

توی خط اول چند query به دیتابیس فرستاده میشه؟ هیچ به دلیل اینکه ORM جنگو Lazy هست (تا زمان درخواست داده query ارسال نمیشه)

توی خط دوم که توی حلقه درخواست داده شده یک query به دیتابیس ارسال میشه و لیست پست ها رو میگیره و میریزه توی queryset. توی قسمت print وقتی title رو میخات چاپ کنه دیگه query زده نمیشه چون title یکی از property های Post هست که قبلا گرفته شده ولی وقتی میخات author رو چاپ کنه دوباره یک query به دیتابیس زده میشه که Author مربوط به اون پست رو بگیره (چون دو تا model جدا با رابطه 1->N هستن) بنابراین توی هر iteration از حلقه یک query دیگه زده میشه و این پرهزینه است و بهش میگن N+1 problem.

برای حل این مشکل باید برای پست ها Author ها رو هم یکجا با یک query بگیریم. برای حل این موضوع باید روی دو تا model یک join زده بشه.

ORM جنگو این مشکل رو با select_related() و prefetch_related() حل کرده.

select_related()

prefetch_related()

توی دو تا کد بالا فقط 1 query به دیتابیس زده میشه.

فرق select_related و prefetch_related چیه؟

select_related به یک Join تبدیل شده و به دیتابیس ارسال میشود و برای مدل هایی که ارتباط 1->N یا one to one دارند استفاده میشود اما prefetch_related دو داده را گرفته و به صورت پایتونی join میزنه در ضمن برای رابطه های چند به چند یا Generic استفاده میشه.

مثالی که توی مستندات جنگو هست:

مثال زیبای بعدی

معماری یک پروژه جنگو با Nginx و Gunicorn

به طوی کلی برای پیاده سازی یه پروژه که با backend پایتونی زده شده (مثل جنگو و فلسک) باید از یک اپلیکیشن سرور (مثل uWSGI یا Gunicorn) و یک وب سرور (که اینجا بهش پراکسی سرور گفته میشه استفاده کرد. مثل شکل زیر:

ولی چرا مثل یک پروژه که با php زده میشه نمیتونیم مستقیم از یه وب سرور استفاده کنیم و خلاص؟ به این دلیل که وب سرورها قابلیت اجرای وب اپلیکیشن های پایتونی رو به خوبی پیاده سازی نکردن. این روش مزایای زیادی داره یکی اینکه تمام منطق کد پایتونی توسط Gunicorn اجرا میشه و عملا Nginx فقط request رو به اون فوروارد میکنه. در عوض خود Nginx کارهای دیگه ای رو انجام میده و عملا باعث بهبود عملکرد اپلیکیشن سرور میشه مثلا درخواست های کاربر رو فیلتر می کنه و اونایی که معتبر هستن رو به اپلیکیشن سرور میده همچنین فایلهای static مثل html, css و… رو خود Nginx به کاربر میده و باعث میشه اپلیکیشن سرور کار پرهزینه انجام نده.

تصویر بالا معماری گفته شده رو نشون میده که یه قابلیت دیگه از این معماری رو میبینیم. همینطور که میبینیم 3 نمونه از پروژه با 3 worker اجرا شده که باعث افزایش کارایی اپلیکیشن میشه. در این حالت خود Nginx درخواست ها رو مدیریت می کنه و میدونه به کدوم worker بفرسته.

سایت دیجیتال اوشن جزئیات پیاده سازی این معماری رو توضیح داده

کدام دیتابیسها با سیستم migration جنگو سازگارترند؟

از بین دیتابیس های معمول برای استفاده در یک وب اپلیکیشن MySQL، PostgreSQLو SQLite پراستفاده ترند. اگر بخواهیم برای یک پروژه Django از یکی از اینها استفاده کنیم باید بدانیم سیستم Migration جنگو با همه آنها به صورت یکسان سازگار نیست. تفاوت سازگاری در آنها در چیست؟

PostgreSQL

سازگارترین دیتابیس با Migration جنگو PostgreSQL است

MySQL

این دیتابیس در عملیات تغییر schema در model ها از تراکنشها transactions پشتیبانی نمی کند (البته با موتور ذخیره سازی InnoDB از transactions هم پشتیبانی میکند به جز برخی عملیاتهای دیتابیس مانند تغییر در schema که در لینک میبینید) یعنی اگر در عملیات مایگریشن به خطایی برخورد بکند تغییرات قبلی rollback نمی شود و ممکن است دیتابیس را معیوب کند و باید تغییرات را به صورت دستی درست کنیم.

علاوه بر آن در هر عملیات مایگریشن این دیتابیس تمام اسکیماها و جدولها و ایندکس ها را از اول rewrite میکند که این در دیتابیسهای بزرگ پرهزینه است زیرا ممکن است دیتابیس production برای زمان زیادی پاسخگو نباشد.

نهایتا MySQL محدودیتهای دیگری هم دارد مانند طول نام ستونها و Index ها. این یعنی ممکن است اگر ایندکسی برای دیتابیس دیگر کار کند که طول نام آن از محدودیت MySQL بیشتر باشد با مایگریشن به این دیتابیس عملیات به خطا بخورد.

SQLite

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

ساخت کامند کاستوم در django-admin

توی جنگو میشه برای یک app کامند کاستوم custom command نوشت. use case های زیادی میتونه داشته باشه. فرض کنیم یه model داریم به اسم campaign که مربوط به قرعه کشی هایی هست که توی وبسایت فروشگاهمون وجود داره. این کمپین فقط عید نوروز فعاله و یه فیلد داره به اسم active که برابر با true میشه. حالا بعد از نوروز میخایم اون رو غیرفعال کنیم بهتره که بتونیم با یه کامند این تغییر رو توی دیتابیس بدیم.

برای این کار یک کامند برای campaign app میسازیم. از این به بعد هر پروژه ای که این app را به INSTALLED_APP اضافه کنه این کامند رو هم خواهد داشت. برای این کار باید توی app مورد نظر یه package با ساختار زیر بسازیم.

توی فایل campaigneactivation.py باید یه کلاس به اسم Command بسازیم که از کلاس BaseCommand ارث بری میکنه.

این کامند الان اضافه شده و میشه به شکل زیر ازش استفاده کرد.

کلاس BaseCommand پارامترهای زیادی داره که جزئیاتش تو این لینک هست