Skip to content

Latest commit

 

History

History
908 lines (556 loc) · 71.8 KB

README.md

File metadata and controls

908 lines (556 loc) · 71.8 KB

مدل‌ها

در این بخش به بحث‌های زیر می‌پردازیم:

  • اهمیت مدل‌ها
  • نمودار کلاس‌ها
  • الگوی‌های ساختاری مدل
  • الگوهای رفتاری مدل
  • مایگریشن‌ها (مهاجرت‌ها)

من یک بار به یک استارت آپ تحلیل داده، در مراحل اولیه‌شان مشاوره دادم. با وجود اینکه گرفتن دیتا به یک بازه زمانی اخیر محدود شده بود، آن‌ها مشکلات کارایی(performance) داشتند. باز کردن برخی صفحات بعضی اوقات چند ثانیه طول می‌کشید. بعد از بررسی معماری‌شان، به تظر می‌آمد که مشکل از مدل داده‌شان بود. در عین حال، مهاجرت کردن(migrating) و تبدیل پتابایت‌هایی از دیتای ساختاریافته و زنده، غیر ممکن به نظر می‌رسید.

"فلوچارت خود را به من نشان بدهید و جداول خود را پنهان کنید و من همچنان مبهوت خواهم ماند. جداول خود را به من نشان دهید و من معمولاً نیازی به فلوچارت‌ها نخواهم داشت، آن‌ها آشکار خواهند بود." (فرد بروکز، The Mythical Man-month)

به طور سنتی، طراحی کد بر اساس داده‌های فکر شده همیشه توصیه می‌شود. اما در این عصر داده‌های بزرگ، این توصیه‌ها مرتبط‌تر هم شده است. اگر مدل داده شما ضعیف طراحی شده باشد، حجم داده‌ها در نهایت باعث مشکلات مقیاس پذیری و نگهداری می‌شود. توصیه می‌کنم از ضرب المثل زیر در مورد نحوه تعادل کد و داده استفاده کنید:

قانون بازنمایی (Rule of Representation): دانش را در دیتا قرار بدهید تا منطق برنامه قدرتمند و احمق باشد.

فکر کنید که چطور می‌توانید پیچیدگی را از کد به دیتا ببرید. همیشه فهمیدن منطق کد از فهمیدن منطق دیتا سخت‌تر است. یونیکس از همین فلسفه به خوبی استفاده کرده است تا تعداد زیادی ابزار ساده ایجاد شود که می‌توانند با هم ترکیب (پایپ) شوند و هر گونه تغییر روی دیتاهای متنی را انجام دهند.

در نهایت، داده‌ها طول عمر بیشتری نسبت به کد دارند. شرکت‌ها ممکن است تصمیم بگیرند کل پایگاه‌های کد را بازنویسی کنند زیرا دیگر نیازهای آن‌ها را برآورده نمی‌کنند، اما پایگاه‌های داده معمولاً نگهداری می‌شوند و حتی در بین برنامه‌ها به اشتراک گذاشته می‌شوند.

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

ام بزرگ تر از وی و سی بزرگ تر از وی است

در جنگو، مدل‌ها کلاس‌هایی هستند که روشی شیءگرا برای برخورد با پایگاه‌های داده ارائه می‌کنند. به طور معمول، هر کلاس به یک جدول پایگاه داده و هر ویژگی به یک ستون پایگاه داده اشاره دارد. می‌توانید با استفاده از یک API که به طور خودکار تولید می‌شود، کوئری‌هایی را در این جداول ایجاد کنید.

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

همچنین، مدل‌ها در مکان‌های بیشتری از آنچه انتظار دارید، استفاده می‌شوند. این به این دلیل است که جنگو را می‌توان به روش‌های مختلفی اجرا کرد. برخی از نقاط ورود جنگو به شرح زیر است:

  • جریان آشتای درخواست-پاسخ وب
  • شل اینترکتیو جنگو
  • دستورات مدیریتی (management commands)
  • اسکریپت‌های تست
  • صف‌های وظایف ناهمزمان همانند سلری

تقریباً در همه این موارد، ماژول‌های مدل وارد می‌شوند (به عنوان بخشی از django.setup()). از این رو، بهتر است مدل‌های خود را از هر گونه وابستگی غیر ضروری یا هر جزء دیگر جنگو، مانند view‌ها دور نگه دارید.

به طور خلاصه، طراحی درست مدل‌های شما، بسیار مهم است. حالا بیایید با طراحی مدل SuperBook شروع کنیم.

کیف قهوه‌ای نهار:

یادداشت نویسنده: پیشروی این پروژه سوپرکتاب در یک بخش مثل این نمایش داده خواهد شد. شاید شما از جعبه عبور کنید, ولی بینش‌ها و تجربه‌های زیاد و درامای کار کردن روی یک پروژه وب اپلیکیشن را از دست می‌دهید.

هفته اول استیو با مشتریش، هوش ابرقهرمانی و مانیتورینگ (شیم) به صورت کوتاه، خیلی قاطی پاتی بود. دفتر فوق‌العاده آینده‌نگر بود، اما انجام هر کاری به صدها تائید و امضا نیاز داشت.


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

با این حال، مدیر ارشد فناوری، هارت، کمک بزرگی بود. او درخواست کرد دستگاه تا یک ساعت دیگر با تمام سیستم‌های نصب‌شده سالم برگردانده شود. او همچنین پیش تأییدیه‌هایی را برای پروژه سوپربوک ارسال کرده بود تا از چنین موانعی در آینده جلوگیری کند.

بعد از ظهر همان روز، استیو یک کیف قهوه‌ای ناهار همراهش بود. یک کت بلیزر بژ و شلوار جین آبی روشن پوشیده بود. هارت به موقع رسید. علیرغم اینکه از بیشتر مردم بلندتر بود و سرش تراشیده بود، خونسرد و خوش برخورد به نظر می‌رسید. او پرسید که آیا استیو تلاش قبلی برای ساخت پایگاه داده ابرقهرمانی در دهه شصت را بررسی کرده است؟


استیو گفت: "اوه بله، پروژه Sentinel، درست است؟". "به نظر می‌رسد پایگاه داده به عنوان یک مدل *Entity-Attribute-Value* طراحی شده است، چیزی که من آن را یک ضد الگو می‌دانم. شاید آن‌ها در آن روزها تصور بسیار کمی در مورد ویژگی‌های یک ابرقهرمان داشتند".

هارت در زمان شنیدن آخرین جمله تقریباً خم شد. با صدای کمی آهسته تر گفت: "درست می‌گویی. من چنین تصوری ندارم. علاوه بر این، آن‌ها فقط دو روز به من فرصت دادند تا همه چیز را طراحی کنم. من معتقدم به معنای واقعی کلمه یک بمب هسته‌ای در جایی تیک تاک می‌کند."

دهان استیو کاملاً باز بود و ساندویچش در ورودی آن یخ زده بود. هارت لبخند زد. "مطمئنا بهترین کار من نیست. زمانی که از حدود یک میلیارد ورودی عبور کرد، روزها طول می‌کشد تا هر نوع تحلیلی را روی آن پایگاه داده لعنتی اجرا کنیم.
سوپر بوک در عرض چند ثانیه آن را انجام می‌دهد، درست است؟"

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

شکار مدل‌ها

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

./images/1.png

بیایید یک لحظه مدل‌ها را فراموش کنیم و در مورد اشیایی که مدل سازی می‌کنیم صحبت کنیم. هر کاربر یک پروفایل دارد. یک کاربر می‌تواند چندین نظر یا چندین پست بگذارد. یک لایک می‌تواند مربوط به یک ترکیب کاربر/پست باشد.

توصیه می‌شود نموداری کلاسی مانند این از مدل‌های خود ترسیم کنید. ممکن است در این مرحله ویژگی‌های کلاس وجود نداشته باشد، اما می‌توانید بعداً آن‌ها را توضیح دهید. هنگامی که کل پروژه در نمودار نشان داده می‌شود، جداسازی برنامه‌ها آسان تر می‌شود.

اینجا چند نکته وجود دارد تا این بازنمایی را انجام بدهیم:

  • اسم‌ها معمولاً تبدیل به هویت مدل‌ها می‌شود.
  • مستطیل‌ها که هر موجودیت را نشان می‌دهند به مدل‌ها تبدیل می‌شوند.
  • خط‌های متصل کننده که دو جهتی هستند و سه نوع از روابط را در جنگو تعریف میکنند: یک-به-یک , یک-به-خیلی (با کلید خارجی یا Foreign Keys پیاده سازی می‌شوند) و خیلی-به-خیلی
  • بخشی که رابطه یک-به-خیلی را تعریف می‌کند در سمت Entity-relationship model (ER-model) قرار دارد. به عبارتی دیگر، طرف n جایی هست که کلید خارجی تعریف می‌شود.

نمودار کلاس‌ها می‌توانند به کدهای جنگو مانند زیر ارتباط داده شوند. (که بین چندین اپ، پخش خواهند شد):

class Profile(models.Model):
    user = models.OneToOneField(User)

class Post(models.Model):
    posted_by = models.ForeignKey(User)

class Comment(models.Model):
    commented_by = models.ForeignKey(User)
    for_post = models.ForeignKey(Post)

class Like(models.Model):
    liked_by = models.ForeignKey(User)
    post = models.ForeignKey(Post)

بعداً مستقیماً به User ارجاع نخواهیم داد، بلکه از settings.AUTH_USER_MODEL استفاده می‌کنیم. همچنین در این مرحله نگران ویژگی‌های فیلد مانند on_delete یا primary_key نیستیم. به زودی به این جزئیات خواهیم پرداخت.

تقسیم کردن فایل models.py به چندین فایل

مانند بسیاری از اجزای جنگو، یک فایل models.py بزرگ را می‌توان به چندین فایل در یک پکیج تقسیم کرد. یک پکیج به صورت دایرکتوری پیاده سازی می‌شود که می‌تواند حاوی چندین فایل باشد یکی از آن‌ها باید فایلی با نام خاص به نام __init__.py باشد. این فایل می‌تواند خالی باشد، اما باید وجود داشته باشد.

همه تعاریفی که باید در سطح پکیج نمایش داده شوند باید در __init__.py به صورت عمومی (global scope) تعریف شوند. به عنوان مثال، اگر models.py را به کلاس‌های جداگانه تقسیم کنیم، در فایل‌های مربوطه در داخل زیرشاخه مدل‌ها مانند postable.py، post.py، و comment.py، ساختار دایرکتوری به شکل زیر خواهد بود:

models/

  • comment.py
  • ــinitــ.py
  • postable.py
  • post.py

برای اطمینان از اینکه همه مدل‌ها به درستی فراخوانی شده اند فایل ، __init__.py باید خطوط زیر را داشته باشد:

from postable import Postable
from post import Post
from comment import Comment

اکنون می‌توانید models.Post را مانند قبل فراخوانی کنید. هر کد دیگری که در فایل __init__.py باشد هنگام فراخوانی پکیج، اجرا می‌شود. بنابراین، این فایل، محل ایده‌آلی برای تعریف مقادیر اولیه در سطح پکیج است.

الگوهای ساختاری

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

الگو‌ها — مدل‌های نرمال شده

مشکل: به صورت ساختاری, هر کپی از مدل‌ها، شامل داده‌های تکراری هستند که باعث ناسازگاری داده‌ها می‌شود

راه حل مدل‌های خود را از طریق نرمال سازی به مدل‌های کوچکتر تقسیم کنید. این مدل‌ها را با روابط منطقی به هم وصل کنید.

جزییات مشکل

تصور کنید کسی جدول پست ما را (با حذف ستون‌های خاص) به روش زیر طراحی کند:

./images/2.png

امیدوارم که به اسم‌های ابرقهرمان‌ها که به صورت ناسازگار در ستون اول( و کاپیتان تمپری که صبر ندارد) آمده توجه کرده‌باشید.

اگه به اولین ستون نگاه کنیم, ما مطمئن نیستیم که کدام روش هجی کردن درست است، Captain Temper یا Capt. Temper. این نوعی از افزونگی داده است که ما دوست داریم توسط نرمال سازی دیتا از بین ببریم.

جزییات راه حل

قبل از اینکه نگاهی به راه حل کاملا نرمال شده بیندازیم، اجازه دهید یک توضیح مختصر در مورد نرمال سازی پایگاه داده در زمینه مدل‌های جنگو داشته باشیم.

سه قدم در نرمال سازی

عادی سازی به شما کمک می‌کند تا داده‌ها را به طور موثر ذخیره کنید. هنگامی که مدل‌های شما به طور کامل نرمال‌سازی شدند، داده‌های اضافی نخواهند داشت و هر مدل باید حاوی داده‌هایی باشد که فقط از نظر منطقی به آن مرتبط هستند.

برای ارائه یک مثال سریع، اگر می‌خواهیم جدول پست را عادی کنیم تا بتوانیم بدون ابهام به ابرقهرمانی که آن پیام را ارسال کرده است اشاره کنیم، باید جزئیات کاربر را در یک جدول جداگانه جدا کنیم. جنگو قبلاً جدول کاربر را به طور پیش فرض ایجاد می‌کند. بنابراین، همانطور که در جدول زیر نشان داده شده است، فقط باید به شناسه کاربری که پیام را در ستون اول ارسال کرده است مراجعه کنید:

./images/3.png

اکنون نه تنها مشخص است که سه پیام توسط یک کاربر ارسال شده است (با یک شناسه کاربری دلخواه)، بلکه می‌توانیم با جستجوی جدول کاربر نام صحیح آن کاربر را نیز پیدا کنیم.

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

در مثال بعدی,ما یک جدول را نرمال سازی می‌کنیم و مدل‌های جنگو متناظر را میسازیم. صفحه گسترده‌ای به نام Sightings را تصور کنید که اولین باری که فردی یک ابرقهرمان را با استفاده از قدرت یا توانایی مافوق بشری می‌بیند، وی را در این صفحه، فهرست می‌کند. هر ورودی در این صفحه گسترده، به منشاء ابرقهرمان، نوع قدرت وی و محل اولین مشاهده که شامل از جمله طول و عرض جغرافیایی است، اشاره می‌کند:

./images/4.png

دیتای جغرافیای زیر از http://www.golombek.com/locations.html به دست آمده است.

فرم نرمال نوع اول (1NF)

  • هیچ خصوصیتی(سلول) با داده تکراری وجود نداشته باشد
  • یک کلید اصلی(پرایمری) به صورت یک ستون یا چندین ستونی(کامپوزیت کی) تعریف شود.

بیایید سعی کنیم صفحه گسترده خود را به یک جدول پایگاه داده تبدیل کنیم. بدیهی است که ستون Power ما قانون اول را زیر پا می‌گذارد.

جدول به روز شده در اینجا اولین فرم نرمال بودن را برآورده می‌کند. کلید اصلی (با علامت *) ترکیبی از Name و Power است که باید برای هر ردیف منحصر به فرد باشد:

./images/5.png

./images/5-2.png

فرم نرمال نوع دوم (2NF)

فرم نرمال نوع دوم باید تمام شرایط فرم نرمال اول را برآورده کند. علاوه بر این، باید این شرط را برآورده کند که تمام ستون‌های کلید غیر اصلی، باید به کل کلید اصلی وابسته باشند.

در جدول قبلی توجه کنید که Origin فقط به ابرقهرمان یعنی Name بستگی دارد. مهم نیست در مورد کدام Power صحبت می‌کنیم. بنابراین، Origin کاملاً به کلید اولیه ترکیبی - Name و Power وابسته نیست.

بیایید فقط اطلاعات مبدا را در یک جدول جداگانه به نام Origin استخراج کنیم، همانطور که در اینجا نشان داده شده است:

./images/6.png

حالا جدول Sightings را طوری تغییر می‌دهیم که با فرم نرمال نوع دوم هم تطابق داشته باشد.

./images/7.png

فرم نرمال نوع سوم (3NF)

در فرم نرمال نوع سوم، جداول باید فرم نرمال نوع دوم را برآورده کنند و علاوه بر این باید شرایطی را داشته باشند که تمام ستون‌های کلید غیراصولی باید مستقیماً به کل کلید اصلی وابسته باشند و در ضمن باید مستقل از یکدیگر باشند.

برای لحظه‌ای به ستون Country فکر کنید. با توجه به Longitude و Latitude، می‌توانید به راحتی ستون Country را استخراج کنید. حتی اگر کشوری که در آن یک ابرقدرت دیده شده است به کلید اولیه ترکیبی Name-Power وابسته است، اما فقط به طور غیرمستقیم به آن‌ها وابسته است.

بنابراین، اجازه دهید جزئیات مکان را در جدول جداگانه کشورها به صورت زیر، جدا کنیم:

./images/8.png

حالا جدول Sightings ما در سومین نوع نرمال سازی قرار دارد:

./images/9.png

مانند قبل، نام ابرقهرمان را با User ID مربوطه جایگزین کرده ایم که می‌تواند برای ارجاع به جدول کاربر استفاده شود.

مدل‌های جنگو

حالا می‌توانیم نگاه کنیم که این حدول‌های نرمال سازی شده چطور می‌توانند به صورت مدل‌های جنگو نمایش داده بشوند. کلیدهای ترکیبی یا کامپوزیت کی‌ها به صورت مستقیم در جنگو پشتیبانی نمی‌شوند.راه حل استفاده شده در اینجا اعمال کلیدهای جایگزین و مشخص کردن ویژگی unique_together در کلاس Meta است:

class Origin(models.Model):
    superhero = models.ForeignKey(
    settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    origin = models.CharField(max_length=100)

    def __str__(self):
        return "{}'s orgin: {}".format(self.superhero, self.origin)

class Location(models.Model):
    latitude = models.FloatField()
    longitude = models.FloatField()
    country = models.CharField(max_length=100)

    def __str__(self):
        return "{}: ({}, {})".format(
            self.country,
            self.latitude,
            self.longitude
            )

    class Meta:
        unique_together = ("latitude", "longitude")

class Sighting(models.Model):
    superhero = models.ForeignKey(
    settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    power = models.CharField(max_length=100)
    location = models.ForeignKey(Location, on_delete=models.CASCADE)
    sighted_on = models.DateTimeField()

    def __str__(self):
        return "{}'s power {} sighted at: {} on {}".format(
        self.superhero,
        self.power,
        self.location.country,
        self.sighted_on
        )

    class Meta:
        unique_together = ("superhero", "power")

کارایی و نرمال سازی نکردن (denormalization)

نرمال سازی می‌تواند بر کارایی، تأثیر منفی بگذارد. با افزایش تعداد مدل‌ها، تعداد پیوست‌های مورد نیاز برای پاسخ به یک کوئری نیز افزایش می‌یابد. به عنوان مثال، برای یافتن تعداد ابرقهرمانان با قابلیت Freeze در ایالات متحده، باید به چهار جدول درخواست ارسال شود. قبل از نرمال سازی، می‌توانستیم همه اطلاعات را با کوئری فرستادن به یک جدول، به دست بیاوریم.

شما باید مدل‌های خود را طوری طراحی کنید که داده‌ها نرمال نگه داشته شوند. این کار، یکپارچگی داده‌ها را حفظ می‌کند. با این حال، اگر سایت شما با مشکلات مقیاس پذیری مواجه است، می‌توانید به طور انتخابی داده‌ها را از آن مدل‌ها استخراج کنید تا داده‌های دی‌نرمال ایجاد کنید.

بهترین الگوها

در حال طراحی نرمال‌سازی کنید, ولی برای بهینه سازی دی نرمالایز کنید

به عنوان مثال، اگر تعداد مشاهدات در یک کشور خاص بسیار زیاد است، آن را به عنوان یک فیلد اضافی به مدل Location اضافه کنید. اکنون، می‌توانید کوئری‌های دیگر را با استفاده از object-relational mapping (ORM)، در جنگو، بر خلاف مقدار ذخیره شده، اضافه کنید.

با این حال، هر بار که یک مشاهده را اضافه یا حذف می‌کنید، باید این تعداد را به روز کنید. شما یا باید این محاسبه تعداد را به متد save در مدل Sighting اضافه کنید یا یک سیگنال اضافه کنید یا با استفاده از یک روش انجام کار ناهمزمان، محاسبات را انجام دهید.

اگر کوئری پیچیده‌ای دارید که چندین جدول را در بر می‌گیرد، مانند تعداد ابرقدرت‌ها بر اساس کشور، ایجاد یک جدول دی‌نرمال شده جداگانه ممکن است عملکرد را بهبود بخشد. به طور معمول، این جدول در یک پایگاه داده دررون-حافظه یا کش سریعتر، اجرا می‌شود. مانند قبل، هر بار که داده‌های مدل‌های نرمال‌شده شما تغییر می‌کند، باید این جدول دی‌نرمال شده را به‌روزرسانی کنیم (در غیر این صورت با مشکل دوست‌نداشتنی Cache-Invalidation مواجه خواهید شد).

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

آیا همیشه باید نرمال‌سازی کنیم؟

نرمال‌سازی بیش از حد لزوماً چیز خوبی نیست. گاهی اوقات، می‌تواند باعث به وجود آمدن جداول غیر ضروری شود که به روز رسانی‌ها و جستجوها را پیچیده کند.

به عنوان مثال، مدل کاربری شما ممکن است چندین فیلد برای آدرس خانه آن‌ها داشته باشد. به طور دقیق، می‌توانید این فیلدها را به یک مدل آدرس نرمال کنید. با این حال، در بسیاری از موارد، معرفی یک جدول اضافی به پایگاه داده غیر ضروری خواهد بود.

به‌جای هدف نرمال‌سازی‌شده‌ترین طرح، هر فرصتی را برای نرمال‌سازی با دقت بسنجید و قبل از ایجاد آن، فواید و مضراتش را در نظر بگیرید.

الگو — مدل‌های میکسین

مشکل: مدل‌های متمایز دارای فیلدها و/یا روش‌های مشابه هستند که اصل DRY را نقض می‌کنند.

راه حل: زمینه‌ها و روش‌های مشابه و تکراری را به مدل‌های میکسین‌ قابل استفاده مجدد، تقسیم کنید.

جزییات مشکل

هنگام طراحی مدل‌ها، ممکن است ویژگی‌ها یا رفتارهای مشترک مشخصی را پیدا کنید که در کلاس‌های مدل به اشتراک گذاشته شده است. به عنوان مثال، یک مدل پست و نظر باید تاریخ ایجاد و تاریخ اصلاح آن را پیگیری کند. کپی و چسباندن دستی فیلدها و روش مرتبط با آن‌ها یک رویکرد بسیار DRY نیست.

از آنجایی که مدل‌های جنگو کلاس هستند، رویکردهای شی گرا مانند ترکیب و ارث راه حل‌های ممکن هستند. با این حال، ترکیبات (با داشتن یک ویژگی که حاوی نمونه‌ای از کلاس مشترک است) برای دسترسی به فیلدها به یک سطح غیرمستقیم اضافی نیاز دارند.

ارث می‌تواند مشکل ساز شود. ما می‌توانیم از یک کلاس پایه مشترک برای پست و نظرات استفاده کنیم. با این حال، سه نوع ارث در جنگو وجود دارد: عینی concrete، انتزاعی abstract و پروکسی proxy.

وراثت عینی با ارث بری از کلاس پایه درست مانند آنچه که معمولاً در کلاس‌های پایتون انجام می‌دهید کار می‌کند. با این حال، در جنگو، این کلاس پایه در یک جدول جداگانه ثبت می‌شود. هر بار که به فیلدهای پایه دسترسی پیدا می‌کنید، به یک عملیات join نیاز است. این اتفاق منجر به افت شدید کارایی می‌شود.

وراثت پراکسی فقط می‌تواند رفتار جدیدی را به کلاس والد اضافه کند. شما نمی توانید فیلدهای جدید اضافه کنید. از این رو برای این وضعیت چندان مفید نیست. در نهایت، ما با وراثت Abstract باقی می‌مانیم.

جزییات راه‌ حل

وراثت انتزاعی یک راه حل ظریف است که از کلاس‌های پایه Abstract ویژه برای به اشتراک گذاشتن داده‌ها و رفتار بین مدل‌ها استفاده می‌کند. وقتی یک کلاس پایه انتزاعی را در جنگو تعریف می‌کنید، که با کلاس‌های پایه انتزاعی (ABC) در پایتون یکسان نیست، هیچ جدول مربوطه در پایگاه داده ایجاد نمی کند. در عوض، این فیلدها در کلاس‌های غیر انتزاعی مشتق شده ایجاد می‌شوند.

دسترسی به فیلدهای کلاس پایه انتزاعی نیازی به دستور JOIN ندارد. جداول به دست آمده نیز دارای فیلدهای مدیریت شده هستند. با توجه به این مزایا، اکثر پروژه‌های جنگو از کلاس‌های پایه انتزاعی برای پیاده سازی فیلدها یا روش‌های مشابه و تکراری استفاده می‌کنند.

محدودیت‌های مدل‌های انتزاعی به شرح زیر است:

  • نمی توانند کلید خارجی یا فیلد چند به چند از مدل دیگری داشته باشند
  • از آن‌ها نمی توان نمونه (instance) تهیه کرد یا آن‌ها را ذخیره کرد
  • آن‌ها را نمی توان مستقیماً در کوئری استفاده کرد زیرا مدیری (class manager) ندارند

در اینجا نحوه طراحی کلاس‌های پست و نظرات، در ابتدا با یک کلاس پایه انتزاعی، آمده است:

class Postable(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now=True)
    message = models.TextField(max_length=500)

    class Meta:
        abstract = True

class Post(Postable):
    ...

class Comment(Postable):
    ...

برای تبدیل یک مدل به یک کلاس پایه انتزاعی، باید abstract = True را در کلاس Meta در داخل مدل اضافه کنید. در اینجا، Postable یک کلاس پایه انتزاعی است. با این حال، خیلی قابل استفاده مجدد نیست.

در واقع، اگر کلاسی وجود داشته باشد که فقط فیلد created و modified را داشته باشد، می‌توانیم تقریباً در هر مدلی که به مهر زمانی نیاز دارد، از آن عملکرد مهر زمانی مجدداً استفاده کنیم. در چنین مواردی، ما معمولا یک مدل میکسین را تعریف می‌کنیم.

میکسین‌های مدل

میکسین‌های مدل، کلاس‌های انتزاعی هستند که می‌توانند به عنوان کلاس والد یک مدل اضافه شوند. پایتون بر خلاف زبان‌های دیگر مانند جاوا از چندین وراثت پشتیبانی می‌کند. از این رو، می‌توانید هر تعداد کلاس والد را برای یک مدل فهرست کنید.

میکسین‌ها باید بسیار واضح باشند و به راحتی ترکیب شوند. اگر یک میکسین را به صورت کلاس‌های پایه تعریف کنید باید به درستی کار کند. از این نظر رفتار آن‌ها بیشتر به ترکیب شبیه است تا وراثت.

میکسین‌های کوچکتر بهتر هستند. هر زمان که یک میکسین بزرگ شد و اصل مسئولیت واحد را نقض کرد، آن را در کلاس‌های کوچک‌تر تقسیم کنید. اجازه دهید یک میکسین یک کار را انجام دهد و آن را به خوبی انجام دهد.

در مثال قبلی، مدل میکسین مورد استفاده برای به‌روزرسانی زمان created و modified را می‌توان به راحتی فاکتور گرفت، همانطور که در کد زیر نشان داده شده است:

class TimeStampedModel(models.Model):
    created = models.DateTimeField(auto_now_add=True)
    modified = models.DateTimeField(auto_now =True)

    class Meta:
        abstract = True

class Postable(TimeStampedModel):
    message = models.TextField(max_length=500)
    ...

    class Meta:
        abstract = True

class Post(Postable):
    ...

class Comment(Postable):
    ...

ما الان دو کلاس پایه داریم. با این حال، عملکردها به وضوح از هم جدا شده است. میکسین را می‌توان به عنوان یک ماژول جدا تعریف کرد و در اپ‌های دیگر دوباره استفاده کرد.

الگو — پروفایل‌های کاربر

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

راه حل: یک کلاس پروفایل کاربری با یک رابطه یک به یک با مدل کاربر ایجاد کنید.

جزییات مشکل

جنگو، یک مدل بسیار مناسب برای تعریف کردن کاربر، ارائه می‌دهد. شما می‌توانید از آن در هنگام ایجاد یک کاربر super user یا ورود به رابط کاربری استفاده کنید. دارای چند فیلد اساسی مانند نام کامل، نام کاربری و ایمیل است.

با این حال، اکثر پروژه‌های دنیای واقعی، اطلاعات بسیار بیشتری را در مورد کاربران، مانند آدرس، فیلم‌های مورد علاقه یا توانایی‌های ابرقدرت آن‌ها نگه می‌دارند. از جنگو 1.5 به بعد، مدل کاربر پیش فرض را می‌توان گسترش داد یا جایگزین کرد. با این حال، اسناد رسمی اکیداً توصیه می‌کنند که فقط داده‌های احراز هویت را حتی در یک مدل کاربر سفارشی ذخیره کنید (این بخش مربوط به اپلیکیشن auth است).

پروژه‌های خاص به چندین نوع کاربر نیاز دارند. به عنوان مثال، سوپربوک می‌تواند توسط ابرقهرمانان و غیر ابرقهرمانان استفاده شود. ممکن است فیلدهای مشترک و برخی فیلدهای متمایز بر اساس نوع کاربر وجود داشته باشد.

جزییات راه‌ حل

راه حل رسمی توصیه شده، ایجاد یک مدل پروفایل کاربر است. این مدل باید با مدل کاربری شما رابطه یک به یک داشته باشد. تمام اطلاعات اضافی کاربر در این مدل ذخیره می‌شود:

class Profile(models.Model):
    user = models.OneToOneField(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        primary_key=True
        )

توصیه می‌شود برای جلوگیری از مشکلات همزمانی در برخی از پایگاه‌های پشتیبان مانند PostgreSQL، مقدار primary_key را به طور واضح روی True تنظیم کنید. بقیه مدل می‌تواند شامل هر گونه جزئیات دیگر کاربر مانند تاریخ تولد، رنگ مورد علاقه و غیره باشد.

هنگام طراحی مدل پروفایل، توصیه می‌شود که تمام فیلدهای جزئیات پروفایل باید nullable یا حاوی مقادیر پیش فرض باشند. به طور شهودی، ما می‌توانیم درک کنیم که یک کاربر نمی تواند هنگام ثبت نام، تمام جزئیات نمایه خود را پر کند. علاوه بر این، ما اطمینان حاصل می‌کنیم که کنترل کننده سیگنال در هنگام ایجاد نمونه پروفایل، هیچ پارامتر اولیه‌ای را پاس نمی کند.

سیگنال‌ها

در حالت ایده آل، هر بار که یک نمونه مدل کاربر ایجاد می‌شود، یک نمونه پروفایل کاربر مربوطه نیز باید ایجاد شود. این‌کار معمولاً با استفاده از سیگنال انجام می‌شود.

برای مثال، می‌توانیم سیگنال post_save را از مدل کاربر، با استفاده از کنترل‌کننده سیگنال زیر در profiles/signals.py گوش کنیم:

from django.db.models.signals import post_save
from django.dispatch import receiver
from django.conf import settings
from . import models

@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_profile_handler(sender, instance, created, **kwargs):
    if not created:
        return
    # Create the profile object, only if it is newly created
    profile = models.Profile(user=instance)
    profile.save()

مدل Profile هیچ پارامتر اولیه اضافی به جز user=instance را ارسال نکرده است.

قبلاً مکان خاصی برای مقداردهی اولیه کد سیگنال وجود نداشت. به طور معمول، آن‌ها در models.py فراخوانی یا پیاده سازی می‌شدند (که قابل اعتماد نبود). با این حال، با ویژگی app-loading refactor در جنگو 1.7، مکان کدهای اولیه در برنامه به خوبی تعریف شده است.

ابتدا، متد ProfileConfig را در فایل apps.py در اپ پروفایل تغییر دهید و درون متد ready، سیگنال را تعریف کنید:

# apps.py
from django.apps import AppConfig

class ProfilesConfig(AppConfig):
    name = "profiles"
    verbose_name = 'User Profiles'

    def ready(self):
        from . import signals

سپس در بخش INSTALLED_APPS، خطی که مسیر اپ را تعریف می‌کند به کمک آدرس دهی نقطه‌ای به AppConfig متصل می‌کنیم. فایل تنظیمات به شکل زیر خواهد شد:

INSTALLED_APPS = [
    'profiles.apps.ProfilesConfig',
    'posts',
    ...

با تنظیم سیگنال‌ها، دسترسی به user.profile باید یک شی Profile را از طریق هر کاربر، حتی کاربران تازه ایجاد شده، برگرداند.

Admin

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

برای راحتی، ادمین پروفایل را می‌توان با تعریف یک UserAdmin سفارشی در profiles/admin.py، به صورت زیر به ادمین پیش فرض کاربر، اضافه کرد:

from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from .models import Profile
from django.contrib.auth.models import User

class UserProfileInline(admin.StackedInline):
    model = Profile

class NewUserAdmin(UserAdmin):
    inlines = [UserProfileInline]

admin.site.unregister(User)
admin.site.register(User, NewUserAdmin)

گونه‌های مختلف پروفایل

فرض کنید به چندین نوع کاربر و پروفایل‌های مربوط به آن‌ها در برنامه خود نیاز دارید - باید یک فیلد برای ردیابی نوع پروفایل کاربر وجود داشته باشد. خود داده‌های profile باید در مدل‌های جداگانه یا یک مدل یکپارچه ذخیره شوند.

یک رویکرد تجمیعی برای Profile توصیه می‌شود زیرا انعطاف پذیری برای تغییر انواع Profile بدون از دست دادن جزئیات آن‌ها را می‌دهد و پیچیدگی را به حداقل می‌رساند. در این رویکرد، مدل Profile شامل یک ابرمجموعه از تمام فیلدها از همه انواع Profile است.

برای مثال، SuperBook به یک پروفایل نوع ابرقهرمانی و یک پروفایل معمولی (غیر ابرقهرمانی) نیاز دارد. می‌توان آن را با استفاده از یک مدل پروفایل یکپارچه به صورت زیر پیاده سازی کرد:

class BaseProfile(models.Model):
    USER_TYPES = (
        (0, 'Ordinary'),
        (1, 'SuperHero'),
    )
    user = models.OneToOneField(settings.AUTH_USER_MODEL, primary_key=True)
    user_type = models.IntegerField(max_length=1, null=True, choices=USER_TYPES)
    bio = models.CharField(max_length=200, blank=True, null=True)

    def __str__(self):
        return "{}: {:.20}". format(self.user, self.bio or "")]

    class Meta:
        abstract = True

class SuperHeroProfile(models.Model):
    origin = models.CharField(max_length=100, blank=True, null=True)

    class Meta:
        abstract = True

class OrdinaryProfile(models.Model):
    address = models.CharField(max_length=200, blank=True, null=True)

    class Meta:
        abstract = True

class Profile(SuperHeroProfile, OrdinaryProfile, BaseProfile):
    pass

ما جزئیات پروفایل را در چندین کلاس پایه انتزاعی گروه بندی کردیم تا موضوعات را از هم جدا کنیم. کلاس BaseProfile شامل تمام جزئیات پروفایل رایج، صرف نظر از نوع کاربر است. همچنین دارای یک قسمت user_type است که پروفایل فعال کاربر را ردیابی می‌کند.

کلاس SuperHeroProfile و کلاس OrdinaryProfile به ترتیب حاوی جزئیات Profile مخصوص کاربران ابرقهرمانی و غیرقهرمانی هستند. در نهایت، کلاس Profile از تمام این کلاس‌های پایه برای ایجاد یک ابرمجموعه از جزئیات پروفایل مشتق می‌شود.

برخی از جزئیاتی که در هنگام استفاده از این روش باید رعایت شود به شرح زیر است:

  • تمام فیلدهای Profile که متعلق به کلاس یا کلاسهای پایه انتزاعی آن هستند باید nullable یا دارای مقدار پیش فرض باشند.
  • این رویکرد ممکن است فضای پایگاه داده بیشتری را به ازای هر کاربر مصرف کند، اما انعطاف پذیری فوق العاده‌ای می‌دهد.
  • فیلدهای فعال و غیرفعال برای نوع Pofile باید خارج از مدل مدیریت شوند. برای مثال، فرمی برای ویرایش نمایه باید فیلدهای مناسب را بر اساس نوع کاربر فعال فعلی نشان دهد.

Pattern – service objects

مشکل: مدل‌ها می‌توانند بزرگ و غیرقابل مدیریت شوند. تست و نگهداری آن‌ها سخت تر می‌شود زیرا یک مدل بیش از یک کار را انجام می‌دهد.

راه‌حل: مجموعه‌ای از متد‌های مرتبط یا یک مدل را در یک شیء تخصصی خدماتی به نام Service جای دهید.

جزئیات مشکل

مدل‌های چاق، ویوی لاغر ضرب‌المثلی است که معمولاً برای مبتدیان جنگو گفته می‌شود. در حالت ایده آل، ویوهای شما نباید حاوی چیزی غیر از منطق برنامه باشد.

با این حال، با گذشت زمان، کدهایی که نمی توانند در جای دیگری قرار گیرند، تمایل پیدا می‌کنند درون مدل‌ها قرار گیرند. به زودی، مدل‌ها تبدیل به محل تخلیه کد می‌شوند.

اگر مدل شما حاوی هر یک از موارد زیر است، یک شی Service برای آن نیاز دارد:

  1. تعامل با سرویس‌های خارجی، به عنوان مثال، بررسی اینکه آیا کاربر واجد شرایط دریافت پروفایل SuperHeroProfile هست یا نه، به کمک یک وب‌سرویس.
  2. کارهای کمکی که با پایگاه داده سروکار ندارند، به عنوان مثال، ایجاد یک URL کوتاه یا کپچای تصادفی برای یک کاربر
  3. ساختن یک شی با عمر کوتاه بدون نیاز به پایگاه داده، به عنوان مثال، ایجاد یک پاسخ JSON برای یک تماس AJAX
  4. عملکردی که چندین نمونه مدل را در بر می‌گیرد اما به هیچکس تعلق ندارد
  5. وظایف طولانی مدت مانند وظایف Celery

مدل‌ها در جنگو از الگوی Active Record پیروی می‌کنند، یعنی هر نمونه از کلاس، مربوط به یک ردیف در جدول پایگاه داده است. در حالت ایده‌آل، آن‌ها هم دسترسی به پایگاه داده و هم منطق برنامه (یا دامنه) را محصور می‌کنند. با این حال، منطق برنامه را در حداقل ممکن، نگه دارید.

در حین آزمایش، اگر متوجه شدیم که پایگاه داده را حتی در حالی که از آن استفاده نمی‌کنیم، به کار‌ می‌گیریم، باید کلاس مدل را تجزیه کنیم. استفاده از یک شیء Service در چنین شرایطی توصیه می‌شود.

جزییات راه حل

اشیاء سرویس Plain Old Python Objects (POPO) یا اشیاء ساده قدیمی پایتون، هستند که یک سرویس یا تعاملات با یک سیستم را محصور می‌کنند. آن‌ها معمولاً در یک فایل جداگانه با نام services.py یا utils.py نگهداری می‌شوند.

به عنوان مثال، بررسی یک وب سرویس گاهی اوقات در یک متد مدل به شرح زیر قرار می‌گیرد:

class Profile(models.Model):
    ...
    def is_superhero(self):
        url = "<http://api.herocheck.com/?q={0}>".format(
            self.user.usernam
        )
        return webclient.get(url)

این متد می‌تواند با تغییر به یک شیء سرویس به شکل زیر بازنویسی شود:

from .services import SuperHeroWebAPI

def is_superhero(self):
    return SuperHeroWebAPI.is_hero(self.user.username)

آبژکت‌های سرویس می‌توانند در فایلی به نام services.py به شکل زیر جمع‌آوری شوند:

API_URL = "<http://api.herocheck.com/?q={0}>"

class SuperHeroWebAPI:
    ...
    @staticmethod
    def is_hero(username):
        url = API_URL.format(username)
        return webclient.get(url)

در بیشتر موارد، متدهای یک شیء سرویس بدون حالت هستند، یعنی عمل را صرفاً بر اساس آرگومان‌های تابع بدون استفاده از ویژگی‌های کلاس انجام می‌دهند. از این رو، بهتر است آن‌ها را به صراحت به عنوان متدهای استاتیک (ایستا) تعریف کنیم (همانطور که برای is_hero انجام دادیم).

در نظر بگیرید که منطق کسب و کار یا منطق دامنه خود را از مدل‌ها به اشیاء خدماتی تبدیل کنید. به این ترتیب، می‌توانید از آن‌ها در خارج از برنامه جنگو نیز استفاده کنید.

تصور کنید یک دلیل تجاری وجود دارد که برخی از کاربران را بر اساس نام کاربری خود از تبدیل شدن به ابرقهرمانان، در لیست ممنوعه قرار دهید. شی سرویس ما را می‌توان به راحتی برای پشتیبانی از این موضوع، تغییر داد:

class SuperHeroWebAPI:
    ...
    @staticmethod
    def is_hero(username):
        blacklist = set(["syndrome", "kcka$$", "superfake"])
        url = API_URL.format(username)
        return username not in blacklist and webclient.get(url)

در حالت ایده آل، اشیاء سرویس، مستقل هستند. این باعث می‌شود که آن‌ها را بتوان بدون به کارگرفتن، مثلاً پایگاه داده، به سادگی آزمایش کرد. آن‌ها همچنین می‌توانند به راحتی مورد استفاده مجدد قرار گیرند.

در جنگو، سرویس‌های وقت گیر به صورت ناهمزمان با استفاده از صف‌های وظیفه مانند Celery اجرا می‌شوند. به طور معمول، اقدامات شیء سرویس به عنوان وظایف Celery اجرا می‌شوند. چنین کارهایی را می‌توان به صورت دوره‌ای یا با تاخیر اجرا کرد.

الگوهای بازیابی

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

الگو - فیلد ویژگی یا property field

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

راه حل: از دکوراتور ویژگی در چنین روش‌هایی استفاده کنید.

جزئیات مشکل

فیلدهای یک مدل، ویژگی‌های هر نمونه از آن مدل را، مانند نام، نام خانوادگی، تاریخ تولد و غیره، در خود ذخیره می‌کنند. آن‌ها همچنین در پایگاه داده ذخیره می‌شوند. با این حال، ما باید به برخی از ویژگی‌های مشتق شده مانند نام کامل یا سن دسترسی داشته باشیم.

آن‌ها را می‌توان به راحتی از فیلدهای پایگاه داده محاسبه کرد، بنابراین نیازی به ذخیره جداگانه نیست. در برخی موارد، آن‌ها فقط می‌توانند یک بررسی مشروط مانند واجد شرایط بودن برای پیشنهادات بر اساس سن، امتیاز عضویت و وضعیت فعال باشند.

یک راه ساده برای پیاده سازی این فیلد، تعریف توابعی مانند get_age به شکل زیر است:

class BaseProfile(models.Model):
    birthdate = models.DateField()
    #...
    def get_age(self):
        today = datetime.date.today()
        return (today.year - self.birthdate.year) - int(
            (today.month, today.day) <
            (self.birthdate.month, self.birthdate.day))

فراخوانی profile.get_age() سن کاربر را بر اساس تفاوت بین سال تولد و تاریخ امروز بر اساس سال و ماه و روز، محاسبه می‌کند. (یعنی اگر تولد امسال هنوز فرا نرسیده باشد، امسال به عدد سن اضافه نمی‌شود).

این می‌تواند توسط یک فراخوانی تابع احضار شود. با این حال، بسیار خواناتر (و پایتونیک) است که آن را profile.age نامید.

جزئیات راه حل

کلاس‌های پایتون می‌توانند با استفاده از دکوراتور property، یک تابع را به‌عنوان یک ویژگی در نظر بگیرند. مدل‌های جنگو نیز می‌توانند از آن استفاده کنند. در مثال قبلی، خط تعریف تابع را با زیر جایگزین کنید:

 @property
 def age(self):

اکنون می‌توانیم با profile.age به سن کاربر دسترسی پیدا کنیم. توجه داشته باشید که نام تابع نیز کوتاه شده است.

یک نقص مهم یک property این است که برای ORM نامرئی است، درست مانند متدهای تعریف شده در مدل. شما نمی‌توانید آن را در یک شی QuerySet استفاده کنید. به عنوان مثال، این دستور کار نمی‌کند، Profile.objects.exclude(age__lt=18). با این حال، برای ویوها یا تمپلیت‌ها قابل مشاهده است.

در صورت نیاز به استفاده از آن در یک شیء QuerySet، ممکن است بخواهید از عبارت Query استفاده کنید. از تابع annotate برای اضافه کردن یک عبارت کوئری، برای استخراج یک فیلد محاسبه شده از فیلدهای موجود خود استفاده کنید.

یک دلیل خوب برای تعریف یک property، پنهان کردن جزئیات کلاس‌های داخلی است. این موضوع به طور رسمی به عنوان Law of Demeter (LoD) یا قانون دیمیتر شناخته می‌شود. به بیان ساده، این قانون می‌گوید که شما فقط باید به اعضای مستقیم خود دسترسی داشته باشید یا فقط از یک نقطه برای دسترسی به اعضا، استفاده کنید.

به عنوان مثال، به جای دسترسی به profile.birthdate.year، بهتر است ویژگی profile.birthyear را تعریف کنید. این کار به شما کمک می‌کند ساختار زیربنایی فیلد birthdate را از این طریق پنهان کنید.

Best Practice

از LoD استفاده کنید تا وقتی به یک property دسترسی پیدا می‌کنید فقط با یک نقطه به آن برسید.

یک عارضه جانبی نامطلوب این قانون این است که منجر به ایجاد چندین ویژگی پوششی (wrapped properties) در مدل می‌شود. این موضوع می‌تواند مدل‌ها را متورم کند و نگهداری از آن‌ها را سخت کند. از این قانون برای بهبود API مدل خود استفاده کنید و هر جا که منطقی است، اتصال را کاهش دهید.

ویژگی‌های ذخیره شده در حافظه پنهان (Cached)

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

from django.utils.functional import cached_property

    #...
    @cached_property
    def full_name(self):
        # Expensive operation e.g. external service call
        return "{0} {1}".format(self.firstname, self.lastname)

مقدار cached به عنوان بخشی از نمونه پایتون (python instance) در حافظه ذخیره می‌شود. تا زمانی که نمونه وجود دارد، همان مقدار برگردانده می‌شود.

به‌عنوان یک مکانیسم ایمن، ممکن است بخواهید اجرای Expensiveoperation را مجبور کنید تا اطمینان حاصل کنید که مقادیر قدیمی برنمی‌گردند. در چنین مواردی، یک آرگومان کلمه کلیدی مانند cached=False تنظیم کنید تا از بازگرداندن مقدار cached جلوگیری کنید.

الگو - مدیریت سفارشی مدل (custom managers)

مشکل: برخی از کوئری‌های مربوط به مدل‌ها به طور مکرر و بدون رعایت اصل DRY، در سراسر کد تعریف شده و مورد دسترسی قرار می‌گیرند.

راه‌حل: مدیریت سفارشی را تعریف کنید تا نام‌های معنی‌داری به پرس و جوهای رایج بدهند.

جزئیات مشکل

هر مدل جنگو دارای یک مدیر پیش فرض به نام objects است. فراخوانی objects.all()، تمام ورودی‌های آن مدل را در پایگاه داده برمی گرداند. معمولاً ما فقط به یک زیرمجموعه از همه ورودی‌ها علاقه‌مند هستیم.

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

public = Posts.objects.filter(privacy="public")

این معیار ممکن است در آینده تغییر کند. برای مثال، ممکن است بخواهیم بررسی کنیم که آیا پست برای ویرایش علامت‌گذاری شده است یا خیر. این تغییر ممکن است به صورت زیر باشد:

    public = Posts.objects.filter(privacy=POST_PRIVACY.Public, draft=False)

با این حال، این تغییر باید در هر جایی که به یک پست عمومی نیاز است انجام شود. این می‌تواند بسیار خسته کننده باشد. فقط باید یک مکان برای تعریف چنین کوئری‌های پرکاربرد بدون نقض قانون repeating oneself وجود داشته باشد.

جزئیات راه حل

کلاس QuerySet یک کلاس انتزاعی بسیار قدرتمند است. آن‌ها تنها در صورت نیاز با تنبلی (lazily) ارزیابی می‌شوند. از این رو، ساخت QuerySet طولانی‌تر با روش زنجیره‌ای (شکلی از رابط روان) بر عملکرد آن‌ها تأثیر نمی گذارد.

در واقع، با اعمال فیلتر بیشتر، مجموعه داده نتیجه کوچک می‌شود. این کار معمولا مصرف حافظه برای به‌دست آمدن نتیجه را کاهش می‌دهد.

مدیر یک مدل (model manager)، رابط مناسب برای یک مدل برای دریافت شیء QuerySet است. به عبارت دیگر، آن‌ها به شما کمک می‌کنند از ORM جنگو برای دسترسی به پایگاه داده زیربنایی استفاده کنید. در واقع، مدیران به عنوان پوشش‌های بسیار نازک در اطراف یک شیء QuerySet پیاده سازی می‌شوند. به این دو رابط یکسان توجه کنید:

    >>> Post.objects.filter(posted_by__username="a")
    [<Post: a: Hello World>, <Post: a: This is Private!>]
    >>> Post.objects.get_queryset().filter(posted_by__username="a")
    [<Post: a: Hello World>, <Post: a: This is Private!>]

مدیر پیش‌فرض ایجاد شده توسط جنگو، objects، چندین متد دارد، مانند all، filter یا exclude که یک QuerySet را برمی‌گرداند. با این حال، آن‌ها فقط یک API سطح پایین برای پایگاه داده شما تشکیل می‌دهند. از مدیران سفارشی برای ایجاد یک API سطح بالاتر مخصوص دامنه استفاده می‌شود. این نه تنها قابل خواندن‌تر است، بلکه کمتر تحت تأثیر جزئیات پیاده سازی قرار می‌گیرد. بنابراین، شما می‌توانید در سطح بالاتری از انتزاع که دقیقاً با دامنه خود شما مدل شده است، کار کنید.

مثال قبلی ما برای پست‌های عمومی را می‌توان به راحتی به یک مدیر سفارشی به شرح زیر تبدیل کرد:

# managers.py
from django.db.models.query import QuerySet

class PostQuerySet(QuerySet):

    def public_posts(self):
        return self.filter(privacy="public")

PostManager = PostQuerySet.as_manager

این میانبر مناسب برای ایجاد یک مدیر سفارشی از یک شی QuerySet، در جنگو 1.7 ظاهر شد. برخلاف سایر روش‌های قبلی، این شیء PostManager مانند مدیر پیش‌فرض objects قابل اتصال به کمک زنجیره کوئری‌ها است.

گاهی اوقات منطقی است که مدیر پیش فرض objects را با مدیر سفارشی خود جایگزین کنیم، همانطور که در کد زیر نشان داده شده است:

from .managers import PostManager

class Post(Postable):
    ...
    objects = PostManager()

با انجام این کار، برای دسترسی به public_posts، کد ما به میزان قابل توجهی به شکل زیر ساده می‌شود:

public = Post.objects.public_posts()

از آنجایی که مقدار بازگشتی یک QuerySet است، می‌توان آن‌ها را بیشتر فیلتر کرد:

public_apology = Post.objects.public_posts().filter(message_startswith="Sorry")

شیء QuerySet چندین ویژگی جالب دارد. در چند بخش بعدی، می‌توانیم به برخی از الگوهای رایج که شامل ترکیب QuerySet‌ها هستند نگاهی بیندازیم.

عملیات را در QuerySets تنظیم کنید

شیء QuerySets مطابق با نام خود (یا بهتر بگوییم نیمه دوم نام خود) از بسیاری از ویژگی‌های مجموعه ریاضی، پشتیبانی می‌کند. برای مثال، دو QuerySets را در نظر بگیرید که شامل اشیاء کاربر است:

    >>> q1 = User.objects.filter(username__in=["a", "b", "c"])
    [<User: a>, <User: b>, <User: c>]
    >>> q2 = User.objects.filter(username__in=["c", "d"])
    [<User: c>, <User: d>]

برخی از عملیات مجموعه‌ای که می‌توانید بر روی آن‌ها انجام دهید به شرح زیر است:

  • Union: این عملیات موارد تکراری را ترکیب و حذف می‌کند. استفاده از q1 | q2 برای دریافت [<User: a>، <User: b>، <User: c>، <User: d>].

  • Intersection: این عملیات موارد مشترک را پیدا می‌کند. برای دریافت [<User: c>] از q1 و q2 استفاده کنید.

  • Difference: این عملیات عنصرهای موجود در مجموعه دوم را از مجموعه اول حذف می‌کند. هیچ عملگر منطقی برای این کار وجود ندارد. در عوض از q1.exclude(pk__in=q2) برای دریافت [<User: a>، <User: b>] استفاده کنید.

همین عملیات را می‌توان در QuerySets با استفاده از اشیاء Q انجام داد:

    from django.db.models import Q

    # Union
    >>> User.objects.filter(Q(username__in=["a", "b", "c"]) |
    Q(username__in=["c", "d"]))
    [<User: a>, <User: b>, <User: c>, <User: d>]
    # Intersection
    >>> User.objects.filter(Q(username__in=["a", "b", "c"]) &
    Q(username__in=["c", "d"]))
    [<User: c>]
    # Difference
    >>> User.objects.filter(Q(username__in=["a", "b", "c"]) &
    ~Q(username__in=["c", "d"]))
    [<User: a>, <User: b>]

تفاوت با استفاده از & (and) و ~ (نفی) اجرا می‌شود. اشیاء Q بسیار قدرتمند هستند و می‌توان از آن‌ها برای ساخت کوئری‌های بسیار پیچیده استفاده کرد.

با این حال، رفتار Set و QuerySets کاملاً یکسان نیست، QuerySetsها بر خلاف مجموعه‌های ریاضی، مرتب‌شده هستند. بنابراین، آن‌ها از این نظر به ساختار داده لیست در پایتون، نزدیکتر هستند.

زنجیره‌سازی چندین QuerySets

تاکنون، QuerySets از همان نوع متعلق به یک کلاس پایه را با هم ترکیب کرده ایم. با این حال، ممکن است لازم باشد QuerySets را از مدل‌های مختلف ترکیب کنیم و عملیاتی را روی آن‌ها انجام دهیم.

به عنوان مثال، جدول زمانی فعالیت یک کاربر شامل تمام پست‌ها و نظرات آن‌ها به ترتیب زمانی معکوس است. روش‌های قبلی ترکیب QuerySets کار نمی کند. یک راه حل ساده لوحانه، تبدیل آن‌ها به لیست، الحاق و مرتب کردن آن‌ها به شرح زیر است:

 >>>recent = list(posts)+list(comments)
 >>>sorted(recent, key=lambda e: e.modified, reverse=True)[:3]
 [<Post: user: Post1>, <Comment: user: Comment1>, <Post: user: Post0>]

متأسفانه، این عملیات هر دو شیء تنبل QuerySet را ارزیابی کرده است. استفاده از حافظه برای ترکیب این دو لیست می‌تواند بسیار زیاد باشد. علاوه بر این، تبدیل QuerySets بزرگ به لیست می‌تواند بسیار کند باشد.

یک راه حل بسیار بهتر استفاده از iterator‌ها برای کاهش مصرف حافظه است. از روش itertools.chain برای ترکیب چند QuerySets به صورت زیر استفاده کنید:

 >>> from itertools import chain
 >>> recent = chain(posts, comments)
 >>> sorted(recent, key=lambda e: e.modified, reverse=True)[:3]

هنگامی که یک QuerySet را ارزیابی می‌کنید، هزینه ورود به پایگاه داده می‌تواند بسیار زیاد باشد. بنابراین، مهم است که آن را تا جایی که ممکن است با انجام عملیاتی که QuerySets را بدون ارزیابی باز می‌گرداند به تأخیر بیندازید.

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

مهاجرت‌ها (migrations)

مهاجرت‌ها به شما کمک می‌کند تا با اطمینان در مدل‌های خود تغییراتی ایجاد کنید. مهاجرت مدل، در جنگو 1.7 معرفی شد، مهاجرت برای یک گردش کار توسعه روشمند، ضروری است. روند کار جدید اساساً به شرح زیر است:

  1. اولین باری که کلاس مدل خود را تعریف می‌کنید، باید موارد زیر را اجرا کنید:

        python manager.py makemigrations <app_label>
    
    
  2. با این کار اسکریپت‌های مهاجرت در پوشه app/migrations ایجاد می‌شود

  3. دستور زیر را در همان محیط (توسعه) اجرا کنید:

        python manager.py migrate <app_label>
    
    
  4. این کار تغییرات مدل را در پایگاه داده اعمال می‌کند. گاهی اوقات، سؤالاتی برای رسیدگی به مقادیر پیش فرض، تغییر نام و غیره پرسیده می‌شود.

  5. اسکریپت‌های مهاجرت را به محیط‌های دیگر انتشار دهید. به طور معمول، ابزار کنترل نسخه شما، به عنوان مثال Git، این کار را انجام می‌دهد. همانطور که آخرین منبع بررسی می‌شود، اسکریپت‌های مهاجرت جدید نیز ظاهر می‌شوند.

  6. دستور زیر را در این محیط‌ها اجرا کنید تا تغییرات مدل اعمال شود:

        python manager.py migrate <app_label>
    
    
  7. هر زمان که در کلاس مدل‌ها تغییراتی ایجاد کردید، مرحله 1 تا مرحله 5 را تکرار کنید.

اگر app_label را در دستورات حذف کنید، جنگو تغییرات اعمال نشده را در هر برنامه پیدا می‌کند و آن‌ها را migrate می‌کند.

خلاصه

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

در فصل بعد، الگوهای طراحی رایجی را که هنگام کار با نماها و تنظیمات URL با آن مواجه می‌شویم، بررسی خواهیم کرد.