পাঠ ৩০ · ৫৮-এর মধ্যে · মডিউল ৭
Home / Courses / Full-Stack Web Frameworks / রিলেশনশিপ মডেলিং

রিলেশনশিপ মডেলিং — ওয়ান-টু-মেনি ও মেনি-টু-মেনি

Modeling relationships -- one-to-many and many-to-many
১২ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • ওয়ান-টু-মেনি সম্পর্ক কীভাবে একটি ফরেন-কী কলাম দিয়ে প্রকাশ করা হয়
  • একটি .author-স্টাইল অ্যাক্সেসর কীভাবে সেই ফরেন-কী দিয়ে সম্পর্কিত রো খুঁজে বের করে
  • মেনি-টু-মেনি সম্পর্ক কেন একটি ফরেন-কী কলাম দিয়ে সম্ভব না — এবং join table কীভাবে সমাধান দেয়
  • রিলেশনশিপ রেজলিউশনের ফলাফল হাতে-তৈরি প্রত্যাশিত ফলাফলের সাথে যাচাই করা

১ · ওয়ান-টু-মেনি — ফরেন কী দিয়ে সম্পর্ক

একজন User একাধিক Post লিখতে পারেন, কিন্তু প্রতিটি Post-এর লেখক ঠিক একজনই — এটিই ওয়ান-টু-মেনিOne-to-Manyএকটি রো (User) একাধিক রো (Post)-এর সাথে সম্পর্কিত হতে পারে, কিন্তু প্রতিটি Post ঠিক একটি User-এরই। সম্পর্ক। এটি প্রকাশ করা হয় Post টেবিলে একটি ফরেন-কী কলাম (author_id) রেখে, যা সংশ্লিষ্ট User-এর id-কে নির্দেশ করে।

User (id=1) Aporna author_id = 1 (FK) Post (id=1) author_id: 1 post_tags join table Tag: Python Tag: Web post_tags-এ একটি post_id একাধিক tag_id-র সাথে সারি হিসেবে আছে
ওয়ান-টু-মেনি সম্পর্ক একটি ফরেন-কী কলাম দিয়ে (Post.author_id), মেনি-টু-মেনি সম্পর্ক একটি আলাদা join table দিয়ে (post_tags) প্রকাশ করা হয়।

২ · মেনি-টু-মেনি — join table দিয়ে সম্পর্ক

একটি Post-এর একাধিক Tag থাকতে পারে, আবার একটি Tag একাধিক Post-এ ব্যবহৃত হতে পারে — এখানে কোনো একদিকের টেবিলে ফরেন-কী কলাম রাখা সম্ভব না (একটি একক কলামে একাধিক মান রাখা যায় না)। সমাধান একটি আলাদা join table — এখানে প্রতিটি রো একটি post_id ও একটি tag_id-এর জোড়া রাখে, প্রতিটি জোড়া একটি নির্দিষ্ট Post-Tag সংযোগ প্রতিনিধিত্ব করে।

Python
# ---------- 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 কোয়েরি সমস্যা"-র মূল কারণ, যদি এই লুকআপ একাধিক পোস্টের জন্য একটি লুপের ভেতর বারবার আলাদা করে চালানো হয়।
মূল কথা · Key takeaway

ওয়ান-টু-মেনি সম্পর্ক একটি ফরেন-কী কলাম দিয়ে ("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"।

অনুশীলন

  1. চিন্তা করুন: যদি 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 ছুড়ে ফলাফল ভুল ধরিয়ে দিত। এটাই দেখায় কেন এই ধরনের হাতে-তৈরি প্রত্যাশিত ফলাফলের সাথে যাচাই করা গুরুত্বপূর্ণ — নীরব ভুল রিলেশনশিপ লজিক সহজেই লক্ষ্য এড়িয়ে যেতে পারে।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি তৃতীয় 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_python p1 ও নতুন পোস্ট দুটোতেই) ব্যবহৃত হতে পারে — মেনি-টু-মেনি-র মূল বৈশিষ্ট্য।

আরও পড়ুন · 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 — সব এক জায়গায়।
আগের পাঠ
মাইগ্রেশন ও স্কিমা ইভোলিউশন