آشنایی با فریمورک contenttypes و Generic relations و Generic Foreignkey در جنگو (Django)

با سلام و درود، سریع بریم سراغ مطلب مورد نظر 🙂 فقط اولش بگو مرجع اصلی داکیومنت های خود جنگو هست، ولی بغیر از اسناد جنگو جندین مطلب رو از سایت های دیگه جمع آوری کردم که مفید خواهند بود. امیدوارم از این مطلب به خوبی استفاده کنید. (نکته: اگر برای Generic relations ها به این مطلب رسید، قسمت های ابتدایی را اسکرول کرده و مستقیم برید سروقت Generic relations)

جنگو شامل یک اپلیکیشن contenttypes  می باشد که می تواند تمامی مدل های نصب شده روی پروژه مبتنی بر جنگو شما را رهگیری کند، که شامل یک interface عمومی سطح بالا برای کارکردن با مدل هایتان می باشد.

مقدمه

قلب اپلیکیشن contenttypes مدل ContentType می باشد که درون django.contrib.contenttypes.models.ContentType می باشد و زندگی می کند 🙂

یک instance از ContentType به ارايه و ذخیره سازی اطلاعات در مورد مدل های نصب شده در پروژه شما می پردازد، و یک نمونه جدید از ContentType ببهنگام ساخت مدل جدید، به صورت خودکار ساخته می شود.

مدل های ContentType متد هایی برای بازگردانی کلاسهای مدل ارائه شده و همچنین برای querying objects از آن مدل ها، دارد. همچنین شامل یک Custom Mnager می باشد که متدها را برای کار با ContentType اضافه کرده و همچنین نمونه های از ContentType برای یک کلاس خاص را بدست می آورد.

می توان از روابط بین مدل های شما و ContentType استفاده کرد تا رابطه های «generic» را بین یکی نمونه از مدل های شما و نمونه هایی از هر مدلی که نصب کرده اید را فعال کند.

نصب فریمورک Contenttype در جنگو

فریمورک contenttype به صورت پیش فرض در لیست INSTALLED_APPS  که توسط django-admin startproject ایجاد می شود شامل شده. اگر اون رو پاک کرده بودی یه به صورت دستی INSTALLED_APPS رو ساختید می توانید با افزودن ‘django.contrib.contenttypes’ بهINSTALLED_APPS در تنظیمات، آن را نصب کنید. معمولا پیشنهاد میشه فریمورک contnettypeرو نصب داشته باشید، چراکه چندین باندل اپلیکمیشن دیگر از جنگپ نیز از آن استفاده می کنند.

  • اپلیکیشن ادمین از جنگو از این فریمورک برای تاریخچه آخرین تغییرات از طریق پنل ادمین جنگو روی objectها استفاده می کند.
  • Django’s authentication framework از این فریمورک چهت محدود کردن دسترسی user به مدل های سطح دسترسی اش استفاده می کند.

مدل ContentType

class ContentType

هر نمونه از ContentType دارای دو فیلد می باشد که در کنار هم یک مدل نصب شده را به طور منحصربفرد تعریف می کنند:

app_label

نام اپلیکیشنی که این مدل قسمتی از آن است.این فیلد از ویژگی app_label مدل گرفته می شود، و فقط شامل قسمت آخر از مسیر import پایتون ، اپلیکیشن مورد نظر می باشد.

model

نام کلاس مدل می باشد.

همچنین ویژگی های زیر نیز در دسترس می باشد:

name

نام قابل خواندن برای انسان ها از contentTytpe . این اسم از ویژگی  verbose_name مدل گرفته می شود.

خب بیاید به یه مثال نگاه کنیم تا بهتر متوجه بشیم عملکرد به چه صورت می باشد. اگر شما در حال حاضر اپلیکیشن content type را نصب شده دارید اپلیکیشن sites را نیز به INSTALLED_APPS اضافه کنید و سپس manage.py migrate  را جهت نصب آن اجرا کنید.

django.contrib.sites.models.Site درون پایگاه داده شما نصب خواهد شد و نمونه جدیدی از ContentType با مقادیر زیر ساخته خواهد شد:

  • app_label  برابر با ‘sites’ خواهد شد (قسمت آخر از مسیر پایتون –  django.contrib.sites ).
  • model هم ‘site’ خواهد شد.

متدهای روی نمونه های ContentType

هر نمونه از contenttype دارای متدهایی است که به شما اجازه می دهد تا از نمونه ContentType بگیرید و به مدل ارائه دهنده اش دهید یا برای استخراج Objectها از آن مدل استفاده کنید.

ContentType.get_object_for_this_type(**kwargs)

مجموعه ای از آزگومان های جستجوی معتبر برای مدل ContnetType ارائه شده می گیرد و دارای یک lookup از نوع get() روی آن مدل می باشد که object متناظر را بر می گرداند.

ContentType.model_class()

کلاس مدل ارائه شده توسط این نمونه از ContentType را باز میگرداند.

برای مثال، می توانیم contentTypeرا برای مدل Userجستجو کنیم:

>>> from django.contrib.contenttypes.models import ContentType
>>> user_type = ContentType.objects.get(app_label='auth', model='user')
>>> user_type
<ContentType: user>

و سپس از این جهت کوئری برای یکی User خاص استفاده کنیم یا برای گرفتن دسترسی به کلاس مدل User :

>>> user_type.model_class()
<class 'django.contrib.auth.models.User'>
>>> user_type.get_object_for_this_type(username='Guido')
<User: Guido>

آشنایی با Generic relations

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

توضیح خود داکیومنت جنگو:

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

Generic ForeinKey چیست؟ تلاش می کند تا به شما یک کلید خارجی دهد، منتهی بجای اینکه متصل به یک نوع objectباشد، این کار را برای مجموعه ای از objectها فراهم می کند(به همین دلیل با دو ستون تعریف می شود، یکی برای نگهداری primary_key و دیگری برای نگهداری content_type)

Generic Relation برعکس GenericForeignKey می باشد، چراکه جنگو به صورت خودکار رابطه معکوس GenerikForeignKey را نمی سازد (بر خلاف ForeignKeys)، شما باید به صورت دستی آنها را تنظیم کنید.

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

class Word(Model):
    name = CharField(max_length=55)
    nouns = GenericRelation('WordNoun', content_type_field='noun_ct', object_id_field='noun_id')

class WordNoun(Model):
    word = ForeignKey(Word)
    noun_ct = ForeignKey(ContentType,
        on_delete=models.CASCADE,
        #this is optional
        limit_choices_to = {"model__in": ('EnNoun', 'PerNoun')}
    )
    noun_id = PositiveIntegerField()
    noun = GenericForeignKey('noun_ct', 'noun_id')

class EnNoun(Model):
    word = OneToOneField(Word)

class PerNoun(Model):
    word = ForeignKey(Word)
    gender = CharField()

مدلی ساختیم که روابط word-noun را نگهداری می کند:

# Having some word
word = Word.objects.get(pk=1)

# With 1 query you can get a list with
# all WordNoun objects for this word.
word_nouns = word.nouns.all()

مشکل این رویکرد چیست؟ بعد زا اینکه شکا لیست word_nouns را گرفتید، دسترسی به یک نمونه noun تنها نیاز به کوئری جدید دارد.

for word_noun in word.nouns.all():
    print word_noun.noun #this will make a query

یکی روش برای بهبود این قضیه، استفاده ازprefetch_related می باشد، بنابراین اگر یک word تنها دارای سه word_nouns باشد (بیاید فرض کنید یک EnNoun و دو تا PerNoun)

سپس بجای اینکه ۴ کوئری – ۱ برای word_nouns و ۳ تا برای هر nounباشیم، ما به ۳ کوئری بهبودش می دهیم، ۱ برای word_nouns و ۲ برای هر contenty_type (EnNoun یا PerNoun).

for word_noun in word.nouns.all().prefetch_related('noun'):
    print word_noun.noun #this will not make a query

تفاوت اینجاست که تعدا کوئری ها در حال حاضر بجای بستگی به تعداد objectهای wordNBounمرتبط، به تعداد ContentType ها متفاوت بستگی دارد.

این مقیاس زیباست اگر شما چندین Nouns از یک content_type درون لیست prefetch شده داشته باشید، اما اگر به ازای هر content_type یک Noun داشته باشید هیچ فرقی نخواهد داشت.

رویکرد دیگر استفاده از ساختار مدل زیر می باشد:

class Word(Model):
    name = CharField(max_length=75)

class WordNoun(Model):
    LANG_CHOICES = (
        ('en', 'English'),
        ('per', 'Persian'),
    )
    word = ForeignKey(Word)
    lang = models.CharField(max_length=2, choices=LANG_CHOICES)
    gender = CharField(max_length=2, blank=True, default='')


#Now accessing all word_nouns would as easy as:
word_nouns = word.wordnoun_set.all()

کارایی از لحاظ SQL بهنگام استفاده از GenericForeignKeys :

تو این زمینه چند لغت از David Cramer توسعه دهنده Disqus داریم:

Generic Relation ها خوب هستند. آنها کند نیستند، فقط مدیریت آنها در کد شما سخت می باشد.

Generic relations are fine. They are not slow, just more difficult to manage in your code base.

David Cramer

به عنوان کارایی، به ازای هر بار استخراج روابط GenericForeignKey شما سه کوئری پایگاه داده خواهید داشت:

  • SELECT object_id_field, object_id from myapp_a WHERE id=1;
  • SELECT app_label, model FROM django_content_type WHERE id=A.object_type_field;
    • در کد های اپلیکیشن، محاسبه نام جدول model + _ + app_label
  • SELECT A.object_id_field FROM TABLE_NAME;

به این مقاله نیز مراجعه داشته باشد، من سعی می کنیم قسمت هایی از اون رو در ادامه بیارم.

در جنگو، یک GenericForeignKey ویژگی می باشد که اجازه می دهد یک مدل به چندین مدل در سیستم مرتبط شود، مخالف با ForeignKeyکه به یک مدل مشخص مرتبط می شود.

در ادامه به GenericForeignKey هایی می پردازیم که دردآفریبن و خطر ناک می باشند :))

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

  • بازرسی Generic ،که تغییرات پایگاه داده روی جداول جداگانه ای صورت میگیرد – برای این مورد، بعضی از معایبی که در پایینتر مطرح می کنیم مهم نمی باشد، و ممکن است حتی مزیت محسوب شوند (مثلا توانایی اشاره به ردیف های حذف شده)
  • generic tagging apps
  • دیگر اپلیکیشن های عمومی که که در آنها هیچجایگزین واقع دیگری ندارید، چون واقعا نمی دانید چه مدل هایی یا حتی چه تعداد مدل متفاوت برای اشاره کردن بهشان نیاز دارید.

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

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

اغلب موارد این مطلب به وضعیت اول پرداخته است. اما مورد دوم را نیز به مختصر بهش پرداخته ایم..ابتدا با یک اپلیکیشن نمونه پیش می رویم:

اپلیکیشن مورد نظر ما، taskها را مدیریت می کند. Taskها می توانند owned (متعلق به ) توسط یک شخص یا یک گروه باشند (اما هر دو حالت با هم نه). شما ممکن است تا وسوه شوید تا از GenericForeignKey برای این حالت به روش زیر استفاده کنید:

class Person(models.Model):
    name = models.CharField()


class Group(models.Model):
    name = models.CharField()
    creator = models.ForeignKey(Person)  # for a later example


class Task(models.Model):
    description = models.CharField(max_length=200)

    # owner_id and owner_type are combined into the GenericForeignKey
    owner_id = models.PositiveIntegerField()
    owner_type = models.ForeignKey(ContentType, on_delete=models.PROTECT)

    # owner will be either a Person or a Group (or perhaps
    # another model we will add later):
    owner = GenericForeignKey('owner_type', 'owner_id')

در این مورد، تنها دو حالت (option) برای owner وجود دارد. ت.جه داشته باشید، الگوی فوق را من پیشنهاد نمی کنم و دلیلش رو در پایین مطرح کرده ام:

چرا روش فوق بد است؟

طراحی دیتابیس

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

(اگر پاراگراف فوق خیلی قانع کننده به ظنر نمی رسد، ممکن است بخواهید این بخش را بخوانید، که این بخش برای کل این مطلب اهمیت دارد)

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

برای مثال فوق، این چیزی است که دیتابیس شما به نظر می رسد.

CREATE TABLE "gfks_task" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "description" varchar(200) NOT NULL,
    "owner_id" integer unsigned NOT NULL,
    "owner_type_id" integer NOT NULL REFERENCES "django_content_type" ("id")
);
CREATE INDEX "gfks_task_618598c8"
    ON "gfks_task" ("owner_type_id");

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

CREATE TABLE "django_content_type" (
    "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
    "app_label" varchar(100) NOT NULL,
    "model" varchar(100) NOT NULL);
)
CREATE UNIQUE INDEX "django_content_type_app_label_76bd3d3b_uniq"
    ON "django_content_type" ("app_label", "model");

بیاید به محتوای این جداول در اپ دمو نگاه بیاندازیم:

با چند تا حدسیات خوب، هر کسی که در آینده به داده نگاه بیاندازد، ممکن است حدس بزند که چطور کار می کند، که به صورت زیر خواهد بود:

  • gfks_task.owner_type_id به ردیفی از django_content_type اشاره می کند
  • با کنار هم گذاشتن app_label و model از این ردیف، ما می توانیم نام جدول را با افزودن خطوط زیر به عنوان مثال مشخص کنیم: اگر gfks_task.owner_type_id == 8 ما نیاز داریم که به gfks_person نگاهی بیاندازیم.
    • در حقیقت این اشتباه است، برای انجام به روش درست، با حقیقتا نیاز داریم تا به مدل نگاه بیاندزایم. ما نیاز داریم تا gfks.models.Person را import کرده و به ویژگی ._meta.db_table نگاه بیاندازیم. اگر Meta.db_table attribute به طور صریح برای یک مدل تنظیم شده باشد، این یک گیر کوچک و اند و تیز می باشد، و به این معنی است که برای درک پایگاه داده خود وابستگی نسبتا زشتی به کد های برنامه پایتون داریم 🙁
  • در حال حاضر ما یک جدول داریم، که می توانیم رکوردی که PK مطابق با مقدار owner_id باشد را جستجو کنیم.

موارد واضحی وجود دارد که می توان در مورد آنها اظهار نظر کرد:

  • به طور واضح بسیار پیچیده تر از جستجوی کلید خارجی در یک جدول است.
  • مکانیسم فوق نوشتن SQL کاستوم جهت کوئری کردن داده ها بسیار پیچیده تر است – شرط Join بسیار ناخوشایند است زیرا نام جدول خود به مقداری تبدیل شده که باید محاسبه شود تا بفهمیم نام جدول چیست

اما بزرگترین مشکل در اول از همه شمای پایگاه داده است چرا که مدت زمان زیادی نمی تواند داده های شما را تشریح کند.

یکپارچگی مرجع – Referential integrity

حتی مهم تر از مشکل بالا، مشکل یکپارچگی مرجع می باشد، اسماً شما چیزی ندارید.

احتمالا این بزرگترین و مهمترین مشکل می باشد. قوام و یکپارچگی (consistency and integrity) داده ها در پایگاه داده از اهمیت بالایی برخوردار هستند و توسط GenericForeignKey به نسبت کلید خارجی، شدیدا افت پیدا می کنند.

چون owner_id فقط یک integer است، ممکن است junk در آن باشد که به این معنی است به هیچ داده واقعی اشاره نمی کند. این در حالی اتفاق می افتد که فیلد به صورت دستی ویرایش شده باشد.

کارایی

مسئله اصلی با GenericForeignKey کارایی آن می باشد.

برای گرفتن یک object همراه با generic related object، با چند تا lookupخواهیم داشت:

  1. گرفتن Objectاصلی (در مثال اینجا، Task)
  2. گرفتن Object از نوع contentType که با Task.owner_type نشان داده شده است (این جدول معمولا توسط جنگو کش می شود)
  3. از طریق ContentType Object ، مدل را پیدا کرده و در نتیجه نام جدول را بدست می آوریم.
  4. با دانستن نام جدول از قسمت قبل (۳) و شناسه Objectاز قسمت اول (۱)، می توان Objectمرتبط را پیدا کرد.

این پیچیده تر از یک کلید خارجی نرمال می باشد و روی کارایی تاثیر منفی می گذارد، مخصوصا وقتی دسته ای از Objectها باشند.