رابطه‌ی 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 می‌باشد