متدهای مدل‌های رابطه‌ای


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

برای مدیریت و جستجو در این روابط، جنگو مجموعه‌ای از کوئری‌های مخصوص فیلدهای رابطه‌ای فراهم کرده است که امکان می‌دهد

  • داده‌های مرتبط را خیلی راحت استخراج نمود.

  • روی فیلدهای مدل‌های مرتبط فیلتر یا مرتب‌سازی انجام داد.

  • داده‌های وابسته را با کمترین تعداد Query ممکن بازیابی کرد.

این بخش دقیقا روی این موضوع تمرکز خواهد داشت و در نهایت یاد می‌گیریم چگونه با استفاده از کوئری‌ها:

  1. روابط یک به چند (One-to-Many) مثل نویسنده و کتاب

  2. روابط چند به چند (Many-to-Many) مثل پروژه‌ها و تکنولوژی‌ها

  3. روابط یک به یک (One-to-One) مثل کاربر و پروفایل

را مدیریت نمود. همچنین به نکات مهمی مثل استفاده از lookups و بهینه‌سازی Queryها با select_related و prefetch_related هم خواهیم پرداخت.

🔹 جمع‌بندی
  1. روابط در جنگو سه نوع اصلی دارند: ForeignKey، ManyToManyField، OneToOneField.

  2. می‌توانیم با استفاده از __ به فیلدهای مدل‌های مرتبط دسترسی پیدا کنیم.

  3. متدهای select_related و prefetch_related برای بهینه‌سازی Queryها ضروری هستند.

  4. مدیریت داده‌های رابطه‌ای در جنگو بسیار ساده و مشابه نوشتن کوئری‌های SQL است، اما با قابلیت‌های سطح بالاتر و امن‌تر.

 

رابطه‌ی ForeignKey


گفتیم که در مدل‌های رابطه‌ای داده‌ها، یکی از رایج‌ترین انواع روابط، رابطه یک به چند (One-to-Many) که در جنگو با ForeignKeyField تعریف می‌گردد. همجنین مدلی برای مدیریت پروژه‌ها ایجاد کردیم که هر پروژه از مدل فرزند Project به یک کاربر (مالک) از مدل والد User تعلق داشته باشد. ولی در عین حال، کاربران می‌توانستند مالک چندین پروژه باشند.

class Project(models.Model):
    owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name="projects")  # ManyToOne Field
    ...

✺✳ دسترسی مستقیم و معکوس (Direct & Reverse Access) ✳✺ 

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

 

از سمت فرزند به والد (Forward)


— دریافت مالک یک پروژه

projectObj = Project.objects.get(id="ac260bde5f1347449f239052420573a7")
owner = projectObj.owner

⚠️ در اینجا، owner نام فیلدی است که در مدل پروژه (Project) تعریف شده و یک رابطه یک‌به‌چند (OneToMany) یا همان ForeignKey را با مدل User ایجاد کرده است.

 

⮜ از سمت والد به فرزندان (Reverse)


— دریافت تمام پروژه‌های یک کاربر

owner = User.objects.get(id=1) 

# Method 1: If related_name is not set (Django default)
projects = owner.project_set.all()

# Method 2: When related_name is set to "projects" (recommended)
projects = owner.projects.all() 

⚠️ اگر در تعریف ForeignKey از پارامتر related_name استفاده نموده باشیم، نباید از نام پیش‌فرض (modelname_set) استفاده کنیم. در غیر این صورت با خطای AttributeError مواجه خواهیم شد.

 

✺✳ فیلتر کردن بر اساس روابط (Lookups Expressions) ✳✺ 

یکی از قابلیت‌های قوی ORM جنگو، lookup expressions است که اجازه می‌دهد تا با استفاده از دو زیرخط  (__)، داده‌ها را بر اساس فیلدهای مدل‌های مرتبط جستجو نمود.

 

از سمت فرزند به والد (Forward)


— دریافت تمام پروژه‌های یک کاربر

projectObjs = Project.objects.filter(owner__username="admin")

⚠️ در اینجا، owner نام فیلدی است که در مدل پروژه (Project) تعریف شده و یک رابطه یک‌به‌چند (OneToMany) یا همان ForeignKey را با مدل User ایجاد کرده است.

 

از سمت والد به فرزندان (Reverse)


— دریافت تمام کاربرانی که حداقل یک پروژه در حوزه "ai" دارند

# Method 1: If related_name is not set (Django default)
userObjs = User.objects.filter(project__area__icontains="ai")

# Method 2: When related_name is set to "projects" (recommended)
users = User.objects.filter(projects__area__icontains="ai")

⚠️ project نام مدل مرتبط است ( در صورت عدم تعریف پارامتر related_name، جنگو به‌صورت پیش‌فرض، از نام مدل به صورت کوچک‌شده استفاده می‌کند). اما اگر related_name="projects" تعریف شده باشد، باید از آن استفاده کرد.

رابطه‌ی OneToOne


در مدل‌های رابطه‌ای، رابطه یک به یک (One-to-One) زمانی استفاده می‌شود که هر رکورد از جدول A دقیقا با یک رکورد از جدول B مرتبط باشد و برعکس. این نوع رابطه در جنگو با استفاده از فیلد OneToOneField پیاده‌سازی می‌شود.

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

رابطه یک به یک در جنگو، یک راه‌حل تمیز و ایمن برای گسترش مدل‌های موجود (به‌ویژه مدل User) بدون تغییر ساختار اصلی آن‌هاست. با استفاده از OneToOneField، می‌توان به‌راحتی اطلاعات جانبی را در مدل جداگانه‌ای ذخیره کرده و همچنان از تمام قابلیت‌های ORM جنگو (مانند دسترسی معکوس و فیلتر پیشرفته) بهره برد.

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE) # OneToOne Field

✺✳ دسترسی مستقیم و معکوس (Forward & Reverse Access) ✳✺ 

در OneToOneField، تفاوت منطقی بین forward و reverse محو می‌شود چرا که هر دو سمت یک شیء واحد را برمی‌گردانند. پس نیازی به تفکیک خاصی نیست و از دید کاربر، انگار دو طرف رابطه هم‌سطح هستند.

💡 در رابطه یک‌به‌یک، رابطه معکوس همیشه یک رکورد خواهد بود و نه لیستی از رکوردها ( QuerySet ) پس نیازی به _set و حتی .all() نیست (چون چندتا نیست!). و نام آن همان نام مدل کوچک‌شده است ( همانند profile، برای مدل Profile ). 

⮜ دسترسی مستقیم از سمت هر دو مدل - Forward


— دسترسی به پروفایل از طریق کاربر

user = User.objects.get(username="admin")
profile = user.profile
⚠️ اگر پروفایل برای کاربر وجود نداشته باشد، این خط با خطای DoesNotExist مواجه می‌شود. برای جلوگیری، می‌توان از hasattr یا get_or_create استفاده نمود.
 

— دسترسی به کاربر از طریق پروفایل

profile = Profile.objects.get(id=1)
user = profile.user

⚠️ این دسترسی همیشه ممکن است (چون هر پروفایل حتما به یک کاربر مرتبط است).

 

✺✳ فیلتر کردن بر اساس روابط (Lookups Expression) ✳✺ 

یادآور می‌شویم که در رابطه‌های OneToOne نیز جنگو همچنان اجازه می‌دهد تا با استفاده از lookupهای معکوس (__)، بر اساس فیلدهای مدل‌های مرتبط جستجو کنیم.

 

— دریافت کاربرانی که قبل از سال ۲۰۰۰ متولد شده‌اند.

users = User.objects.filter(profile__birth__lte="2000-01-01")

⚠️ اگر related_name را در OneToOneField تغییر داده باشیم (مثلا به user_profile)، باید از همان نام در lookup استفاده کنیم

 

— دریافت تمام پروفایل‌هایی که کاربری‌شان بعد از تاریخ خاصی ایجاد شده‌اند.

profiles = Profile.objects.filter(user__date_joined__gte="2023-01-01")

رابطه‌ی ManyToMany


در مدل‌های رابطه‌ای داده‌ها، رابطه چند به چند (Many-to-Many) زمانی استفاده می‌شود که هر رکورد از جدول A بتواند با چندین رکورد از جدول B مرتبط باشد و برعکس. این نوع رابطه در جنگو با استفاده از فیلد ManyToManyField پیاده‌سازی می‌شود.

یکی از رایج‌ترین سناریوهای استفاده از ManyToManyField در پروژه‌های جنگو، برچسب‌گذاری (tagging) است. در مدلی که تعریف نمودیم، هر پروژه می‌تواند دارای چندین برچسب باشد (مانند «Django»، «JavaScript»، «Python») و هر برچسب نیز می‌تواند به چندین پروژه تعلق داشته باشد. در این حالت، رابطه بین مدل Project و مدل Tag به‌وضوح از نوع چند به چند است.

class Project(models.Model):
    ...
    tags = models.ManyToManyField(Tag, related_name='projects')
    ...

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

رابطه چند به چند در جنگو، امکان مدل‌سازی انعطاف‌پذیر و واقع‌گرایانه‌ای را فراهم می‌کند. با استفاده از ManyToManyField، می‌توان به‌راحتی ارتباطات پیچیده بین موجودیت‌ها را مدیریت کرد و از قابلیت‌های پیشرفته ORM جنگو — از جمله دسترسی معکوس، فیلتر کردن بر اساس روابط، و افزودن/حذف برچسب‌ها — بهره برد، بدون نیاز به مدیریت دستی جدول واسط (junction table).


✺✳ دسترسی مستقیم و معکوس (Forward & Reverse Access) ✳✺ 

 

⮜ دسترسی معکوس - Reverse


— دریافت پروژه‌های دارای یک برچسب

tag = Tag.objects.get(name="ِDjango")

# Method 1: If related_name is not set (Django default)
projects = tag.project_set.all()

# Method 2: When related_name is set to "projects" (recommended)
projects = tag.projects.all()

⚠️ اگر در تعریف ManyToManyField از پارامتر related_name استفاده نموده باشیم، نباید از نام پیش‌فرض (modelname_set) — جنگو به‌صورت پیش‌فرض، از نام مدل به صورت کوچک‌شده بهره می‌برد — استفاده کنیم. در غیر این صورت با خطای AttributeError مواجه خواهیم شد.

 

⮜ دسترسی مستقیم - Forward


— دریافت تمام برچسب‌های مرتبط با یک پروژه خاص

project = Project.objects.get(id="ac260bde-5f13-4744-9f23-9052420573a7")
tags = project.tags.all()

⚠️ در اینجا، tags نام فیلدی است که در مدل پروژه (Project) تعریف شده و یک رابطه چندبه‌چند (ManyToMany) را با مدل Tag ایجاد کرده است.

 

✺✳ فیلتر کردن بر اساس روابط (Lookups Expression) ✳✺ 

lookupهای معکوس با مشخصه دو زیرخط  (__)، در رابطه‌های ManyToMany نیز جنگو همچنان اجازه می‌دهند بر اساس فیلدهای مدل‌های مرتبط جستجو کنیم.

 

⮜ فیلتر کردن معکوس - Reverse Lookup


— دریافت تمام پروژه‌هایی که دارای یک برچسب خاص هستند (فیلتر کردن برچسب‌ها بر اساس پروژه)

# Method 1: If related_name is not set (Django default)
tags = Tag.objects.filter(project__area="Sport")

# Method 2: When related_name is set to "projects" (recommended)
tags = Tag.objects.filter(projects__area="Sport")
 
⚠️این کوئری ممکن است برچسب‌ها را تکراری نشان دهد اگر به چند پروژه مرتبط باشند. برای جلوگیری از تکرار، می‌بایست از .distinct() استفاده کنیم
tags = Tag.objects.filter(projects__title__icontains="AI").distinct()

 

⮜ فیلتر کردن مستقیم - Forward Lookup


— دریافت تمام پروژه‌هایی که دارای یک برچسب خاص هستند (فیلتر کردن برچسب‌ها بر اساس پروژه)

projects = Project.objects.filter(tags__name="Python")
projects = Project.objects.filter(tags__name__icontains="java")

⚠️ در اینجا، tags نام فیلدی است که در مدل پروژه (Project) تعریف شده و یک رابطه چندبه‌چند (ManyToMany) را با مدل Tag ایجاد کرده است.

 

✺✳ متدهای RelatedManager ✳✺ 

فرآیند افزودن (add) و حذف (remove) رکوردها در یک رابطه ManyToManyField در جنگو بسیار ساده و شهودی است، چون جنگو به‌طور خودکار یک سیستم مدیریت ویژه (RelatedManager) برای این فیلد ایجاد می‌کند که متدهای کاربردی‌ مثل add(), remove(), clear(), و set() را فراهم می‌کند.

 

+ افزودن رکوردها به رابطه (add)


—  به صورت مستقیم (Forward) از سمت مدل Project 

fsProject = Project.objects.get(title="Fluent Speech")
jsTag = Tag.objects.get(name="JavaScript")
djTag = Tag.objects.get(name="Django")

fsProject.tags.add(djTag)
fsProject.tags.add(jsTag, djTag)

—  به صورت معکوس (Reverse) از سمت مدل Tag (با استفاده از related_name)

djTag = Tag.objects.get(name="Django")
fsProject= Book.objects.get(title="Fluent Speech")
ygProject = Book.objects.get(title="Yoga Academy")

djTag.projects.add(fsProject, ygProject)
 

— حذف رکوردها از رابطه (remove)


—  به صورت مستقیم (Forward) از سمت مدل Project 

fsProject = Project.objects.get(title="Fluent Speech")
djTag = Tag.objects.get(name="Django")

fsProject.tags.remove(djTag)

 

—  به صورت معکوس (Reverse) از سمت مدل Tag (با استفاده از related_name)

djTag = Tag.objects.get(name="Django")
fsProject= Book.objects.get(title="Fluent Speech")

djTag.projects.remove(fsProject)

💡 اگر رابطه‌ای وجود نداشته باشد، remove() خطا نمی‌دهد — ساکت عمل می‌کند.

💡 فقط رابطه حذف می‌شود، رکوردهای اصلی (Project یا Tag) پاک نمی‌شوند.

 

× پاک کردن تمام رابطه‌ها (clear)


—  به صورت مستقیم (Forward) از سمت مدل Project 

fsProject = Book.objects.get(title="Fluent Speech")
fsProject.tags.clear()

 

—  به صورت معکوس (Reverse) از سمت مدل Tag (با استفاده از related_name)

djTag = Tag.objects.get(name="Django")
djTag.projects.clear()

💡 clear() فقط رابطه‌ها را پاک می‌کند، نه خود رکوردها.

 

جایگزینی کامل مجموعه (set)


—  به صورت مستقیم (Forward) از سمت مدل Project 

fsProject= Book.objects.get(title="Fluent Speech")
tagObjs = Tag.objects.filter(name__in=["Django", "JavaScript"])

fsProject.tags.set(tagObjs)

💡 تمام رابطه‌های قبلی پاک می‌شوند.

💡 فقط رابطه‌های جدید ایجاد می‌شوند.

💡 می‌توانید لیستی از شیءها یا حتی لیستی از IDها بدهید

fsProject.tags.set(["ab2050e16c94483db24aeab26b0c4330", "41de4854a25b40ac860c5b28ff4b8c17"])  # id = UUID
💡 اگر clear=False را نیز اضافه نماییم، فقط رابطه‌های جدید اضافه خواهند شد و عملکردی همانند add،خواهیم داشت، اما پیش‌فرض clear=True می‌باشد

بهینه‌سازی جستجوهای رابطه‌ای


در جنگو، وقتی از ORM برای بازیابی داده‌ها استفاده می‌کنیم، اگر روابط بین مدل‌ها (مثل ForeignKey یا ManyToManyField) زیاد باشد، ممکن است تعداد زیادی query به پایگاه داده ارسال شود. این پدیده به عنوان "N+1 Query Problem" شناخته می‌شود.

برای جلوگیری از این اتفاق، جنگو دو ابزار قدرتمند در اختیار قرار داده است:

  • select_related برای روابط ForeignKey / OneToOneField

  • prefetch_related برای روابط ManyToMany / reverse ForeignKey


— مشکل N+1


فرض کنیم می‌خواهیم همه‌ی پروژه‌ها را به همراه نام مالک آن‌ها نمایش دهیم:

projects = Project.objects.all()
for project in projects:
    print(project.title, project.owner.username)

🔻 اتفاقی که می‌افتد:

  • ابتدا یک query برای گرفتن همه‌ی پروژه‌ها زده می‌شود.

SELECT * FROM project;
  • سپس برای هر پروژه، یک query جدا برای owner (کاربر مرتبط) اجرا می‌شود.

SELECT * FROM user WHERE id = 1;
SELECT * FROM user WHERE id = 2;
SELECT * FROM user WHERE id = 3;

اگر ۱۰۰ پروژه داشته باشیم ← ۱۰۰ + ۱ = ۱۰۱ Query به پایگاه داده ارسال می‌گردد و این N+1 problem نام دارد و عملکرد را به شدت کند می‌کند.

استفاده از select_related و یا prefetch_related، تعداد کوئری‌ها را به حداقل می‌رساند .

 

— استفاده از select_related


برای روابط ForeignKey یا OneToOneField 

projects = Project.objects.select_related('owner').all()
for project in projects:
    print(project.title, project.owner.username)

اکنون فقط یک کوئری به پایگاه داده زده می‌شود و جنگو داده‌های هر پروژه را به‌ همراه مالک آن، همزمان دریافت می‌کند. در حقیقیت جنگو، با JOIN داده‌ها در SQL، داده‌های Proejct و User را همزمان واکشی می‌کند. و زمانی که project.owner.username را صدا می‌زنیم، دیگر نیازی به کوئری جدید نیست — چون داده‌ها از قبل preload شده‌اند.

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

 

— استفاده از prefetch_related


برای روابط ManyToMany یا reverse ForeignKey 

projects = Project.objects.prefetch_related('tags').all()
for project in projects:
    print(project.title, [tag.name for tag in project.tags.all()])

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

  1. یک کوئری برای واکشی تمام پروژه‌ها.

  2. یک کوئری دیگر برای واکشی تمام تگ‌های مرتبط با آن پروژه‌ها در یک مرحله.

با استفاده از prefetch_related، جنگو به‌صورت هوشمندانه در سطح پایتون، داده‌های واکشی‌شده را با هم تطبیق (match) می‌دهد و نتایج را در حافظه (Cache) ذخیره می‌کند تا به هنگام دسترسی به project.tags.all()، دیگر نیازی به کوئری جدید نباشد. در حقیقت جنگو، ابتدا مدل اصلی را fetch کرده، سپس یک کوئری جداگانه برای مدل مرتبط اجرا می‌کند و نتایج را در پایتون ترکیب می‌کند و در این مسیر از IN lookup برای محدود کردن نتایج استفاده می‌کند.

 

 — ترکیب select_related و prefetch_related


در پروژه‌های واقعی، معمولاً نیاز داریم همزمان با مدل‌های مرتبط یک‌به‌چند (ForeignKey) و روابط چند‌به‌چند (ManyToManyField) کار کنیم. در چنین شرایطی، ترکیب هوشمندانه select_related و prefetch_related بهترین راه‌حل برای جلوگیری از N+1 Query Problem است.

 

projects = Project.objects.select_related('owner').prefetch_related('tags')
for project in projects:
    print(f"{project.title} | {project.owner.username} | {[tag.name for tag in project.tags.all()]}")

📊 این ترکیب:

  • select_related ← فقط برای owner (ForeignKey)

  • prefetch_related ← برای tags (ManyToMany)

با این ترکیب، جنگو فقط سه کوئری به پایگاه داده ارسال می‌کند.

  1. یک JOIN بین داده‌های مدل‌های Project و User ← برای واکشی همزمان پروژه‌ها و اطلاعات مالک (owner) آن‌ها (با استفاده از select_related).
  2. یک کوئری برای واکشی idهای پروژه‌ها (در عمل، این بخش در کوئری اول گنجانده می‌شود).
  3. یک کوئری جداگانه برای واکشی تگ‌های مرتبط ← شامل دو جدول:
    • جدول میانی (مثلا project_tags) که ارتباط بین پروژه و تگ را نگه می‌دارد
    • جدول Tag برای گرفتن نام و سایر فیلدهای تگ‌ها ← این کوئری از WHERE tag_id IN (...) استفاده می‌کند تا فقط تگ‌های مرتبط با پروژه‌های انتخاب‌شده را بگیرد (با استفاده از prefetch_related).

در نتیجه، هیچ کوئری اضافی‌ در حلقه اجرا نمی‌شود — حتی اگر صدها پروژه و هزاران تگ وجود داشته باشد!

— اگر فقط از select_related استفاده می‌کردیم، دسترسی به project.tags.all() باعث ارسال یک کوئری جداگانه برای هر پروژه می‌شد.

— و اگر فقط از prefetch_related استفاده می‌کردیم، دسترسی به project.owner.username نیز همین مشکل را ایجاد می‌کرد.


ترتیب فراخوانی مهم نیست

💡 پیشنهاد: اگر نیاز به فیلتر یا مرتب‌سازی خاصی روی tags وجود داشته باشد، می‌توان از کلاس Prefetch استفاده نمود.

projects = Project.objects.select_related('owner').prefetch_related(
    Prefetch('tags', queryset=Tag.objects.filter(is_active=True).order_by('name'))
)