রিলেশনশিপ মডেলিং — ওয়ান-টু-মেনি ও মেনি-টু-মেনি
এই পাঠে যা শিখবেন
- ওয়ান-টু-মেনি সম্পর্ক কীভাবে একটি ফরেন-কী কলাম দিয়ে প্রকাশ করা হয়
- একটি
.author-স্টাইল অ্যাক্সেসর কীভাবে সেই ফরেন-কী দিয়ে সম্পর্কিত রো খুঁজে বের করে - মেনি-টু-মেনি সম্পর্ক কেন একটি ফরেন-কী কলাম দিয়ে সম্ভব না — এবং join table কীভাবে সমাধান দেয়
- রিলেশনশিপ রেজলিউশনের ফলাফল হাতে-তৈরি প্রত্যাশিত ফলাফলের সাথে যাচাই করা
১ · ওয়ান-টু-মেনি — ফরেন কী দিয়ে সম্পর্ক
একজন User একাধিক Post লিখতে পারেন, কিন্তু প্রতিটি Post-এর লেখক ঠিক
একজনই — এটিই ওয়ান-টু-মেনিOne-to-Manyএকটি রো (User) একাধিক রো (Post)-এর সাথে সম্পর্কিত হতে পারে, কিন্তু প্রতিটি Post ঠিক একটি User-এরই।
সম্পর্ক। এটি প্রকাশ করা হয় Post টেবিলে একটি ফরেন-কী কলাম
(author_id) রেখে, যা সংশ্লিষ্ট User-এর id-কে নির্দেশ করে।
২ · মেনি-টু-মেনি — join table দিয়ে সম্পর্ক
একটি Post-এর একাধিক Tag থাকতে পারে, আবার একটি Tag
একাধিক Post-এ ব্যবহৃত হতে পারে — এখানে কোনো একদিকের টেবিলে ফরেন-কী কলাম রাখা সম্ভব না (একটি
একক কলামে একাধিক মান রাখা যায় না)। সমাধান একটি আলাদা join table — এখানে প্রতিটি রো একটি
post_id ও একটি tag_id-এর জোড়া রাখে, প্রতিটি জোড়া একটি নির্দিষ্ট
Post-Tag সংযোগ প্রতিনিধিত্ব করে।
# ---------- L28-এর মিনি-ORM (পুনরায় সংজ্ঞায়িত) ----------
class QueryManager:
def __init__(self, model_cls):
self.model_cls = model_cls
def filter(self, **criteria):
return [
row for row in self.model_cls._table
if all(row.get(key) == value for key, value in criteria.items())
]
class Model:
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls._table = []
cls._next_id = 1
cls.objects = QueryManager(cls)
def __init__(self, **fields):
self.id = fields.pop("id", None)
for key, value in fields.items():
setattr(self, key, value)
def _to_dict(self):
return dict(self.__dict__)
def save(self):
cls = type(self)
if self.id is None:
self.id = cls._next_id
cls._next_id += 1
cls._table.append(self._to_dict())
else:
for i, row in enumerate(cls._table):
if row["id"] == self.id:
cls._table[i] = self._to_dict()
break
return self
class User(Model):
pass
class Post(Model):
@property
def author(self):
"""ওয়ান-টু-মেনি: author_id ফরেন কী দিয়ে সংশ্লিষ্ট User রো খুঁজে বের করে"""
matches = User.objects.filter(id=self.author_id)
return matches[0] if matches else None
class Tag(Model):
pass
# ---------- মেনি-টু-মেনি join table ----------
post_tags = [] # [{"post_id": ..., "tag_id": ...}, ...]
def add_tag_to_post(post, tag):
post_tags.append({"post_id": post.id, "tag_id": tag.id})
def get_tags_for_post(post_id):
"""post_tags স্ক্যান করে একটি পোস্টের সব ট্যাগ রিজলভ করা"""
tag_ids = [row["tag_id"] for row in post_tags if row["post_id"] == post_id]
return [tag for tid in tag_ids for tag in Tag.objects.filter(id=tid)]
# ---------- ডেটা তৈরি ----------
u1 = User(name="Aporna", email="aporna@example.com").save()
u2 = User(name="Rafi", email="rafi@example.com").save()
p1 = Post(title="প্রথম পোস্ট", author_id=u1.id).save()
p2 = Post(title="দ্বিতীয় পোস্ট", author_id=u2.id).save()
t_python = Tag(name="Python").save()
t_web = Tag(name="Web").save()
t_orm = Tag(name="ORM").save()
add_tag_to_post(p1, t_python)
add_tag_to_post(p1, t_web)
add_tag_to_post(p2, t_orm)
# ---------- ওয়ান-টু-মেনি (.author) যাচাই ----------
print("p1.author:", p1.author)
print("p2.author:", p2.author)
assert p1.author["name"] == "Aporna"
assert p2.author["name"] == "Rafi"
# ---------- মেনি-টু-মেনি (ট্যাগ রেজলিউশন) যাচাই ----------
p1_tags = sorted(tag["name"] for tag in get_tags_for_post(p1.id))
p2_tags = sorted(tag["name"] for tag in get_tags_for_post(p2.id))
print("\np1-এর ট্যাগসমূহ:", p1_tags)
print("p2-এর ট্যাগসমূহ:", p2_tags)
# হাতে-তৈরি প্রত্যাশিত ফলাফলের সাথে মিলিয়ে দেখা
assert p1_tags == ["Python", "Web"] # p1-এর ঠিক ২টি ট্যাগ, বেশিও না কমও না
assert p2_tags == ["ORM"] # p2-এর ঠিক ১টি ট্যাগ
print("\nসব রিলেশনশিপ (author FK ও tags M2M) হাতে-তৈরি প্রত্যাশিত ফলাফলের সাথে হুবহু মিলেছে।")
Post.author একটি @property — এটি প্রতিবার অ্যাক্সেস করার সময়
User.objects.filter(id=self.author_id) সত্যিকারভাবে চালিয়ে সবশেষ ডেটা নিয়ে আসে, কোনো ক্যাশ করা
মান ফেরত দেয় না। একইভাবে get_tags_for_post() প্রতিবার post_tags সম্পূর্ণ স্ক্যান
করে — এটিই আসলে পরের পাঠ (L32)-এ দেখা "N+1 কোয়েরি সমস্যা"-র মূল কারণ, যদি এই লুকআপ একাধিক পোস্টের জন্য
একটি লুপের ভেতর বারবার আলাদা করে চালানো হয়।
ওয়ান-টু-মেনি সম্পর্ক একটি ফরেন-কী কলাম দিয়ে ("many" পাশের টেবিলে), মেনি-টু-মেনি সম্পর্ক একটি আলাদা
join table দিয়ে (দুই দিকের id-এর জোড়া রেখে) প্রকাশ করা হয়। উভয় ক্ষেত্রেই সম্পর্ক
"রিজলভ" করতে হয় একটি লুকআপ ফাংশন দিয়ে — এখানে .author ও get_tags_for_post()।
এই দুই প্যাটার্নই প্রায় প্রতিটি বাস্তব ORM-এ (Django ORM-এর ForeignKey ও
ManyToManyField) একই ধারণায় কাজ করে, শুধু ভেতরে সত্যিকারের SQL JOIN চলে।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
কেন Tag-এর মধ্যে সরাসরি একটি post_ids লিস্ট কলাম রেখে মেনি-টু-মেনি সম্পর্ক
তৈরি করা হলো না, বরং আলাদা post_tags টেবিল ব্যবহার করা হলো?
রিলেশনাল ডেটাবেসের একটি কলামে একাধিক মান (যেমন একটি লিস্ট) রাখা রিলেশনাল মডেলের মৌলিক নিয়ম ভাঙে (প্রথম
নরমাল ফর্ম — ../dbms-sql/-এ বিস্তারিত), এবং সেটিতে দক্ষভাবে ফিল্টার/জয়েন করা কঠিন। একটি
আলাদা join table প্রতিটি সংযোগকে একটি স্বতন্ত্র রো হিসেবে রাখে, তাই "এই ট্যাগযুক্ত সব পোস্ট" বা "এই
পোস্টের সব ট্যাগ" — দুই দিকেই সহজে ও নির্ভরযোগ্যভাবে খোঁজা যায়।
প্র ০২
get_tags_for_post()-এর রিটার্ন লাইনে
[tag for tid in tag_ids for tag in Tag.objects.filter(id=tid)] — এই নেস্টেড লিস্ট
কমপ্রিহেনশনটি ঠিক কী করছে?
এটি প্রতিটি tid (post_tags থেকে পাওয়া tag_id) নিয়ে Tag.objects.filter(id=tid)
কল করে (যা সাধারণত একটি-উপাদানের লিস্ট ফেরত দেয়, কারণ id ইউনিক), তারপর সেই ছোট লিস্টের
ভেতরের প্রতিটি tag-কে চূড়ান্ত ফলাফলে ফ্ল্যাটেন করে যোগ করে। ফলে একাধিক tid-এর
জন্য একাধিক filter() কল হয় এবং তাদের ফলাফল একটি একক ফ্ল্যাট লিস্টে মিশে যায়।
প্র ০৩
p2.author কেন Rafi ফেরত দেয়, যদিও কোডে কোথাও সরাসরি
p2.author = u2 লেখা হয়নি?
কারণ p2 = Post(title="দ্বিতীয় পোস্ট", author_id=u2.id).save() লেখার সময়
author_id-এ সরাসরি u2.id (যা 2) বসানো হয়েছিল। author
প্রপার্টি এক্সেস করার সময় User.objects.filter(id=self.author_id) চলে, অর্থাৎ
User.objects.filter(id=2) — যা u2-এর রো ফেরত দেয়, যার
name "Rafi"।
অনুশীলন
-
চিন্তা করুন: যদি
get_tags_for_post()-এ ভুলবশতrow["post_id"] == post_id-এর বদলেrow["tag_id"] == post_idলেখা হতো, তাহলেp1_tags == ["Python", "Web"]assertion-টির কী হতো?ভুল শর্তটি সম্পূর্ণ ভুল রো ম্যাচ করত (post_id-এর বদলে tag_id মেলাচ্ছে), তাই ভুল
tag_id-গুলো বের হতো এবং আসলp1-এর ট্যাগের সাথে মিলত না — ফলেassert p1_tags == ["Python", "Web"]AssertionErrorছুড়ে ফলাফল ভুল ধরিয়ে দিত। এটাই দেখায় কেন এই ধরনের হাতে-তৈরি প্রত্যাশিত ফলাফলের সাথে যাচাই করা গুরুত্বপূর্ণ — নীরব ভুল রিলেশনশিপ লজিক সহজেই লক্ষ্য এড়িয়ে যেতে পারে। -
পরীক্ষা করুন: উপরের কোড সেলে একটি তৃতীয়
Postতৈরি করুন (author_id=u1.idদিয়ে), তাকেt_pythonওt_orm— দুইটি ট্যাগ যোগ করুন, তারপরget_tags_for_post()চালিয়ে দেখুন এবংp1.authorও নতুন পোস্টের.authorএকইUserরো ফেরত দেয় কি না যাচাই করুন।যেহেতু নতুন পোস্টের
author_idওp1.author_idএকই (দুটোইu1.id),.authorপ্রপার্টি দুটোর জন্যই একই ডেটা ({"id": 1, "name": "Aporna", ...}) ফেরত দেবে — একজন ইউজারের একাধিক পোস্ট থাকতে পারা ঠিক ওয়ান-টু-মেনি সম্পর্কের সংজ্ঞা। নতুন পোস্টেরget_tags_for_post()ফলাফলে["ORM", "Python"](sorted) আসবে, যা দেখায় একইTagএকাধিক পোস্টে (এখানেt_pythonp1 ও নতুন পোস্ট দুটোতেই) ব্যবহৃত হতে পারে — মেনি-টু-মেনি-র মূল বৈশিষ্ট্য।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ L31 কোয়েরি বিল্ডার বনাম রॉ SQL বনাম ORM — একই কোয়েরি তিনভাবে প্রকাশ করা দেখুন।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
- Database Management Systems কোর্স সহোদর কোর্স ফরেন কী, জয়েন, ও নরমালাইজেশনের SQL ভিত্তি সেই কোর্সে বিস্তারিত শেখানো হয়েছে।
- সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Programming Languages & Compiler Design, Software Engineering & Git, Theory of Computation, Engineering Economics ও Full-Stack Web Frameworks — সব এক জায়গায়।