جستجوی رکوردها


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

سیستم ORM (Object-Relational Mapper) جنگو یکی از نقاط قوت اصلی آن است. این سیستم با انواع پایگاه داده‌ از جمله MySQL، PostgreSQL، SQLite، Oracle و MariaDB سازگار است و مدیریت داده‌ها را به‌شکل ساده و قدرتمند ممکن می‌سازد. هسته‌ی ORM جنگو بر پایه‌ی QuerySetها بنا شده است. بنابراین نخستین گام برای کار با داده‌ها، آشنایی کامل با مفاهیم Query و QuerySet است.

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

ساختار کلی QuerySet

یک QuerySet به‌طور معمول، به شکل زیر تعریف می‌گردد.

queryVariable = ModelName.objects.method()
  • queryVariable ↔ متغیری است که خروجی در آن ذخیره می‌شود.

  • ModelName ↔ نام مدلی است که قصد داریم داده‌های آن را بازیابی کنیم.

  • objects ↔ واسط پیش‌فرض برای دسترسی به داده‌های مدل.

متدهای متداول در QuerySet

  • all() ↔ تمام رکوردهای مدل را برمی‌گرداند.

  • get() ↔ یک رکورد خاص را بر اساس شرایط مشخص برمی‌گرداند. (اگر رکوردی پیدا نشود یا بیش از یک رکورد موجود باشد، خطا رخ می‌دهد.)

  • filter() ↔ رکوردهایی را برمی‌گرداند که با شرایط تعیین‌شده مطابقت دارند.

  • exclude() ↔ رکوردهایی را برمی‌گرداند که با شرایط تعیین‌شده مطابقت ندارند.

 

⚠️ قبل از ایجاد QuerySet روی یک مدل، باید مطمئن شویم که مدل مربوطه را import کرده‌ایم.
⚠️ از آنجا که وظیفه‌ی برقراری ارتباط با مدل‌ها بر عهده‌ی Viewهاست، این import معمولاً در فایل views.py انجام می‌شود.

در ادامه، مستندات کوئری‌های جنگو، به‌صورت یکپارچه و گسترده‌تر، ارائه می‌گردد. 

متدهای پایه


در اولین گام، نیاز است داده‌های ذخیره‌شده در پایگاه داده فراخوانی شوند — Retrieving Data. جنگو برای این کار مجموعه‌ای از متدهای پرس‌وجو (QuerySet methods) را در اختیار قرار می‌دهد که هر یک کارکرد مشخصی دارند: از جمله بازیابی تمام رکوردها، دریافت یک رکورد منحصربه‌فرد، فیلتر کردن نتایج بر اساس شرایط دلخواه و همچنین حذف رکوردهای نامرتبط از مجموعه نتایج.

این متدها پایه و اساس کار با داده‌ها در ORM هستند و در اکثر پروژه‌ها بیشترین استفاده را دارند.

 

⸺ دریافت همه رکوردها


projects = Project.objects.all()

✅ این متد تمام رکوردهای مدل project را برمی‌گرداند. در عمل معادل SELECT * FROM coreapp_project در SQL است.
📌 کاربرد: زمانی که بخواهیم کل داده‌های یک جدول را نمایش دهیم (به عنوان مثال لیست همه پروژه‌های اجرا شده).


 

⸺ دریافت یک رکورد خاص


projectObj = Project.objects.get(id="4f0928dd-5d68-484c-bd19-aa7a18d27d66")

✅ متد get() فقط یک رکورد را بر اساس شرط مشخص برمی‌گرداند.
⚠️ اگر هیچ رکوردی پیدا نشود ⇄ خطای DoesNotExist
⚠️ اگر بیش از یک رکورد پیدا شود ⇄ خطای MultipleObjectsReturned

📌 کاربرد: زمانی که مطمئن هستیم شرط دقیقاً یک رکورد را مشخص می‌کند (مثل id یا username که منحصربه‌فرد و یا به اصطلاح Unique هستند).


 

⸺ فیلتر کردن رکوردها


projectObjs = Project.objects.filter(subject="Health")

✅ متد filter()، لیستی از رکوردهایی که شرایط را احراز می‌کنند، برمی‌گرداند.
📌 کاربرد: گرفتن لیست داده‌هایی که ویژگی مشترک دارند. ( بعنوان مثال 🡠 همه پروژه‌های اجرا شده در موضوع سلامت Health )


 

⸺ حذف رکوردهای نامرتبط


projectObjs = Project.objects.exclude(subject="Sport")

✅ متد exclude()، برعکس filter() عمل می‌کند؛ یعنی رکوردهایی که مصداق شرط را ندارند، برمی‌گرداند.
📌 کاربرد: وقتی بخواهیم همه داده‌ها به جز یک دسته خاص را نمایش دهیم.( بعنوان مثال 🡠 همه پروژه‌های اجرا شده یه غیر از موضوع ورزش Sports )

متدهای مرتب‌سازی و محدودسازی


پس از بازیابی داده‌ها، گاهی نیاز داریم که رکوردها را مرتب‌سازی — Ordering — یا محدودسازی Limitingکنیم. به عنوان مثال محدودسازی ۱۰ کتاب آخر یک نویسنده یا مرتب‌کردن محصولات فروشگاه بر اساس قیمت. جنگو این کار را به‌سادگی با متدهای order_by و slicing فراهم می‌کند.

⸺ مرتب‌سازی صعودی


projects = Project.objects.all().order_by("title")

همهٔ پروژه‌ها را بر اساس عنوان (title) به‌صورت الفبایی صعودی (از الف به ی) مرتب می‌کند. یعنی پروژه‌هایی که عنوانشان با «آ» شروع می‌شود، اول لیست می‌آیند.


⸺ مرتب‌سازی نزولی


projects = Project.objects.all().order_by("-created")

همهٔ پروژه‌ها را بر اساس فیلد created (معمولاً تاریخ ایجاد) به‌صورت نزولی مرتب می‌کند. علامت منفی (-) نشان‌دهندهٔ ترتیب معکوس است؛ یعنی جدیدترین پروژه‌ها اول لیست ظاهر می‌شوند.


⸺ محدود کردن تعداد رکوردها


projects = Project.objects.all()[:5]

فقط 5 رکورد اول از لیست پروژه‌ها را برمی‌گرداند. این روش معمولاً برای نمایش «آخرین پروژه‌ها» یا «محدود کردن نتایج در صفحه‌بندی» استفاده می‌شود. این کار در پس‌زمینه با LIMIT 5 در SQL پیاده‌سازی می‌شود و بسیار کارآمد است.


⸺ رد کردن چند رکورد اول


projects = Project.objects.all()[5:]

اولین 5 رکورد را نادیده می‌گیرد و از ششمین رکورد به بعد را برمی‌گرداند. استفاده مستقیم از slice در پروژه‌های واقعی به‌جای Paginator جنگو توصیه نمی‌شود، چون Paginator امن‌تر و بهینه‌تر است.

متدهای کمکی پرکاربرد


گاهی تنها به یک رکورد خاص (مثل اولین یا آخرین مورد) یا اطلاعات خلاصه‌ای مانند تعداد کل رکوردها نیاز داریم. جنگو برای این موارد متدهای کمکی ساده‌ای مانند first(), last(), count() و exists() را فراهم کرده است تا بتوانیم بدون نوشتن کوئری‌های پیچیده یا اضافی، به‌صورت کارآمد و خوانا با داده‌ها کار کنیم.

 

⸺ اولین رکورد


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

projectObj = Project.objects.first()

⸺ آخرین رکورد


آخرین رکورد از مجموعه نتایج را برمی‌گرداند. مانند first()، در صورت عدم وجود داده، None بازمی‌گرداند.

projectObj = Project.objects.last()

💡 نکته: first() و last() بر اساس ترتیب تعیین‌شده توسط order_by() عمل می‌کنند. اگر order_by مشخص نشده باشد، ترتیب بر اساس primary key در نظر گرفته می‌شود.


⸺ تعداد رکوردها


تعداد کل رکوردها را به‌صورت یک عدد صحیح برمی‌گرداند. این متد بهینه‌تر از len(Project.objects.all()) است، چون در پس‌زمینه از SELECT COUNT(*) استفاده می‌کند و داده‌های واقعی را fetch نمی‌کند.

projectObj = Project.objects.count()

⸺ بررسی وجود رکورد


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

projectObj = Project.objects.filter(area="AI").exist()
 

متدهای مقایسه‌ای (Lookups)


اغلب اوقات نیاز خواهد بود که داده‌ها را بر اساس شرایط دقیق‌تری—مانند «بزرگ‌تر از»، «کوچک‌تر از»، «شامل یک عبارت» یا «بین دو تاریخ»—فیلتر کنیم. جنگو برای این منظور از سیستم عبارات جستجو (lookup expressions) پشتیبانی می‌کند که با استفاده از دو زیرخط (__) پس از نام فیلد اعمال می‌شوند.

 

⸺ مساوی


projectObj = Project.objects.filter(title="Fluent Speech")

رکوردهایی که فیلد title آن‌ها دقیقاً برابر با مقدار داده‌شده باشد. (معادل با استفاده از = در SQL)

💡 نیازی به __exact نیست؛ این رفتار پیش‌فرض filter() است.


⸺ شامل بودن (Contains)


آخرین رکورد از مجموعه نتایج را برمی‌گرداند. مانند first()، در صورت عدم وجود داده، None بازمی‌گرداند.

projectObjs = Project.objects.filter(title__contains="AI")

رکوردهایی که فیلد title آن‌ها عبارت "AI" را در هر جایی داشته باشند (حساس به بزرگ‌وکوچکی حروف).

💡 برای جستجوی غیرحساس به بزرگ‌وکوچکی حروف، از __icontains استفاده می‌شود.


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


projectObjs = Project.objects.filter(created__gte="2023-01-01") # gte = greater than or equal

رکوردهایی که تاریخ ایجاد (created) آن‌ها بزرگ‌تر یا مساوی تاریخ داده‌شده باشد.


⸺ کوچک‌تر یا مساوی


projectObj = Project.objects.filter(updated__lte='2023-01-01') # lte = less than or equal

رکوردهایی که تاریخ اخرین تغییرات آن‌ها (updated) کوچک‌تر یا مساوی تاریخ مشخص‌شده باشد.


⸺ بین دو مقدار (Range)


projectObj = Project.objects.filter(created__range=('2024-01-01', '2025-01-01'))

رکوردهایی که مقدار فیلد created آن‌ها بین دو تاریخ (شامل خود دو سر) باشد.

💡 معادل با  BETWEEN '2024-01-01' AND '2025-01-01' در SQL


⸺ داخل یک لیست (In)


projectObjs = Project.objects.filter(area__in=["Health", "Sport"])

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

💡 معادل با subject IN ('AI', 'Sports', 'Health') در SQL.


⸺ مقدار تهی (Null)


projectObjs = Project.objects.filter(content_isnull=False)

رکوردهایی که فیلد content آن‌ها مقدار NULL نباشد (یعنی داده داشته باشد).

💡 برای یافتن رکوردهایی که content آن‌ها خالی است: content__isnull=True.

💡 isnull فقط برای فیلدهایی که null=True دارند معنی دارد. برای فیلدهای CharField معمولاً بهتر است از __exact="" یا __isempty (در صورت نیاز) استفاده کرد.

متدهای تغییر و حذف داده‌ها


سیستم ORM جنگو فقط برای خواندن داده‌ها طراحی نشده است؛ بلکه امکان بروزرسانی و حذف داده‌ها را نیز به‌صورت ایمن و کارآمد فراهم می‌کند—بدون نیاز به نوشتن دستورات SQL دستی.

 

⸺ بروزرسانی رکوردها


متد update()، رکورد(های) مطابق با شرط را مستقیماً در پایگاه داده به‌روزرسانی می‌کند.

Project.objects.filter(id="4f0928dd-5d68-484c-bd19-aa7a18d27d66").update(title="Fluent Speech AI")
💡 بسیار سریع و کارآمد (یک کوئری SQL اجرا می‌شود).
⚠️ از سیگنال‌های save() و اعتبارسنجی مدل عبور نمی‌کند!

⸺ حذف رکوردها


همهٔ پروژه‌هایی که فیلد content آن‌ها خالی (NULL) باشد را به‌طور کامل از پایگاه داده حذف می‌کند.

Project.objects.filter(content__isnull=True).delete()

💡 حذف انبوه با یک دستور ساده.
⚠️ این عملیات غیرقابل بازگشت است و از سیگنال pre_delete/post_delete پشتیبانی می‌کند (برخلاف update).

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


یکی از مهم‌ترین توانایی‌های 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'))
)