পাঠ ২৮ · ৫৮-এর মধ্যে · মডিউল ৭
Home / Courses / Full-Stack Web Frameworks / ORM প্যাটার্ন

ORM প্যাটার্ন — অবজেক্টকে টেবিলে ম্যাপ করা

The ORM pattern -- mapping objects to database tables
১১ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • ORM প্যাটার্ন কী সমস্যা সমাধান করে এবং কেন প্রায় সব ব্যাক-এন্ড ফ্রেমওয়ার্কে এটি থাকে
  • একটি Model বেস ক্লাস কীভাবে সাবক্লাসিং-এর মাধ্যমে প্রতিটি "মডেল"-এর নিজস্ব ইন-মেমরি টেবিল দেয় (__init_subclass__)
  • .save() মেথড কীভাবে insert বনাম update সিদ্ধান্ত নেয়
  • .objects.filter(**kwargs)-স্টাইল কোয়েরি এবং রাউন্ড-ট্রিপ সঠিকতা (সেভ করা ডেটা ও ফেরত পাওয়া ডেটা হুবহু মেলা) হাতে-কলমে যাচাই করা

১ · ORM কী ও কেন দরকার

ORMObject-Relational Mappingডেটাবেস টেবিলের রো-কে পাইথন/জাভা/রুবি অবজেক্টে, এবং অবজেক্টকে আবার রো-তে — দুই দিকেই — ম্যাপ করার একটি প্যাটার্ন। এমন একটি প্যাটার্ন যা ডেটাবেসের রিলেশনাল ডেটা (টেবিল, রো, কলাম) এবং প্রোগ্রামিং ভাষার অবজেক্ট-ওরিয়েন্টেড ডেটা (ক্লাস, ইনস্ট্যান্স, অ্যাট্রিবিউট) — এই দুই জগতের মধ্যে একটি সেতু তৈরি করে। ORM ছাড়া প্রতিটি ডেটাবেস অপারেশনের জন্য হাতে SQL স্ট্রিং লিখতে হয়; ORM দিয়ে একই কাজ সাধারণ মেথড কল দিয়ে করা যায়।

ORM ছাড়া (raw SQL)
INSERT INTO users (name, email) VALUES (...) — হাতে লেখা SQL স্ট্রিং, ডেটাবেস-নির্দিষ্ট সিনট্যাক্স।
ORM দিয়ে
User(name=..., email=...).save() — সাধারণ পাইথন কোড, ORM ভেতরে SQL তৈরি করে (বাস্তব ORM-এ)।
DBMS & SQL কোর্সের সাথে সম্পর্ক

এই পাঠ ধরে নেয় টেবিল, রো, কলাম, প্রাইমারি কী-এর মতো SQL-এর মৌলিক ধারণাগুলো ইতিমধ্যে পরিচিত — Database Management Systems কোর্সে সেই ভিত্তি বিস্তারিত শেখানো হয়েছে। এখানে আমরা শুধু দেখব কীভাবে একটি ব্যাক-এন্ড ফ্রেমওয়ার্কের ORM স্তর সেই টেবিল-ধারণাগুলোকে অবজেক্টে মোড়ায়। Pyodide স্যান্ডবক্সে সত্যিকারের ডেটাবেস কানেকশন সম্ভব না, তাই নিচের "টেবিল" আসলে পাইথনের একটি লিস্ট — তবে তার উপর চলা লজিক (insert, filter) সত্যিকারের ও সম্পূর্ণ কার্যকর।

২ · একটি সত্যিকারের মিনি-ORM — Model বেস ক্লাস

নিচের Model ক্লাসে __init_subclass__ ব্যবহার করা হয়েছে — যখনই Model-এর কোনো সাবক্লাস (যেমন User) সংজ্ঞায়িত হয়, পাইথন স্বয়ংক্রিয়ভাবে __init_subclass__ কল করে, এবং সেখানে সেই সাবক্লাসের নিজস্ব খালি _table লিস্ট ও _next_id কাউন্টার তৈরি হয় — ফলে User ও ভবিষ্যতের অন্য যেকোনো মডেল একে অপরের টেবিল থেকে সম্পূর্ণ আলাদা থাকে।

User(...) পাইথন অবজেক্ট -- .name, .email .save() .filter() Model / QueryManager ORM স্তর User._table [{"id":1,"name":...}, ...] ইন-মেমরি "টেবিল" (dict-এর লিস্ট)
অবজেক্ট থেকে টেবিলে যেতে .save(), টেবিল থেকে অবজেক্ট-শেপড ডেটা ফেরত পেতে .objects.filter() — এই দুই দিকের ম্যাপিংই ORM-এর মূল কাজ।
Python
class QueryManager:
    """প্রতিটি Model সাবক্লাসের নিজস্ব QueryManager থাকে -- .objects.filter(...) স্টাইল দেয়"""
    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())
        ]

    def all(self):
        return list(self.model_cls._table)


class Model:
    """একটি ছোট্ট, সত্যিকারের কার্যকর ORM বেস ক্লাস।
    সাবক্লাস করলেই সেই সাবক্লাস নিজস্ব ইন-মেমরি টেবিল ও .objects ম্যানেজার পায়।"""

    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:                       # id নেই -- এটি একটি নতুন রো, INSERT
            self.id = cls._next_id
            cls._next_id += 1
            cls._table.append(self._to_dict())
        else:                                      # id আছে -- বিদ্যমান রো, UPDATE
            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


# তিনটি User ইনস্ট্যান্স তৈরি ও সেভ করা
u1 = User(name="Aporna", email="aporna@example.com").save()
u2 = User(name="Rafi", email="rafi@example.com").save()
u3 = User(name="Aporna", email="aporna.k@example.com").save()

print("তৈরি হওয়া id গুলো:", u1.id, u2.id, u3.id)
print("in-memory টেবিলে মোট সারি সংখ্যা:", len(User._table))
print("টেবিলের কাঁচা কন্টেন্ট:")
for row in User._table:
    print(" ", row)

# ১. id দিয়ে ফিল্টার -- একটিই ফলাফল আসা উচিত, রাউন্ড-ট্রিপ সঠিকতা যাচাই
found = User.objects.filter(id=u2.id)
print("\nid দিয়ে খোঁজা ফলাফল:", found)
round_trip_ok = (
    len(found) == 1
    and found[0]["name"] == u2.name
    and found[0]["email"] == u2.email
)
print("রাউন্ড-ট্রিপ সঠিক (সেভ করা ডেটা == ফেরত পাওয়া ডেটা):", round_trip_ok)
assert round_trip_ok

# ২. name দিয়ে ফিল্টার -- দুইটি ফলাফল আসা উচিত (u1 ও u3 দুজনেরই নাম "Aporna")
aporna_rows = User.objects.filter(name="Aporna")
print("\nname='Aporna' ফিল্টার ফলাফল:", aporna_rows)
assert len(aporna_rows) == 2

# ৩. একটি Update -- u1-এর ইমেইল বদলে আবার সেভ করা, তারপর যাচাই
u1.email = "aporna.updated@example.com"
u1.save()
after_update = User.objects.filter(id=u1.id)[0]
print("\nu1 আপডেটের পর টেবিলে email:", after_update["email"])
assert after_update["email"] == "aporna.updated@example.com"
assert len(User._table) == 3   # আপডেট নতুন রো যোগ করেনি, বিদ্যমান রো-ই বদলেছে

    
লক্ষ্য করুন save() মেথড কীভাবে self.id চেক করে সিদ্ধান্ত নেয় — id None হলে এটি একটি নতুন রো (INSERT-এর সমতুল্য), আর id ইতিমধ্যে সেট থাকলে এটি একটি বিদ্যমান রো (UPDATE-এর সমতুল্য)। শেষ ধাপে u1.save() দ্বিতীয়বার কল করার পরেও User._table-এর দৈর্ঘ্য ৩-ই থাকে (৪ হয় না) — কারণ এটি নতুন রো যোগ করেনি, বিদ্যমান রো-টিই আপডেট করেছে। বাস্তব ORM-গুলোতে (Django ORM, SQLAlchemy) এই একই "id আছে কি না" যুক্তিতে insert বনাম update সিদ্ধান্ত নেওয়া হয়, শুধু ভেতরে সত্যিকারের SQL INSERT/UPDATE স্টেটমেন্ট তৈরি হয়।
মূল কথা · Key takeaway

ORM অবজেক্ট ও ডেটাবেস টেবিলের মধ্যে একটি দ্বিমুখী ম্যাপিং তৈরি করে — .save() অবজেক্ট থেকে টেবিলে যায়, .objects.filter(**kwargs) টেবিল থেকে অবজেক্ট-শেপড ডেটা ফিরিয়ে আনে। উপরের Model ক্লাসটি এই কোর্সের পরের চারটি পাঠে (মাইগ্রেশন, রিলেশনশিপ, কোয়েরি বিল্ডার তুলনা, N+1 সমস্যা) বিস্তৃত হবে, তাই এর গঠন মনে রাখা জরুরি — বিশেষ করে _table, save(), ও objects.filter()।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ _table = [] সরাসরি Model ক্লাসে না লিখে __init_subclass__-এর ভেতর প্রতিটি সাবক্লাসের জন্য আলাদাভাবে তৈরি করা হলো কেন?

যদি _table = [] সরাসরি Model ক্লাসে লেখা হতো, তাহলে সেই একটি লিস্টই Model-এর সব সাবক্লাসের মধ্যে শেয়ার হতো (পাইথনে ক্লাস অ্যাট্রিবিউট উত্তরাধিকারসূত্রে পাওয়া যায়, কপি হয় না) — ফলে User ও ভবিষ্যতের কোনো Product মডেলের সব রো একই লিস্টে মিশে যেত। __init_subclass__ প্রতিটি সাবক্লাসের জন্য একটি করে নতুন, খালি _table তৈরি করে দেয়, তাই প্রতিটি মডেলের নিজস্ব সম্পূর্ণ আলাদা টেবিল থাকে।

প্র ০২ _to_dict() মেথডে dict(self.__dict__) লেখা হয়েছে, সরাসরি self.__dict__ না লিখে — এই পার্থক্যটা গুরুত্বপূর্ণ কেন?

self.__dict__ সরাসরি ব্যবহার করলে টেবিলের রো ও অবজেক্টের অ্যাট্রিবিউট ডিকশনারি একই অবজেক্টের রেফারেন্স হতো — পরে যদি কেউ u1.name = "নতুন নাম" লিখে save() না-ও করে, তাহলেও টেবিলের রো নিঃশব্দে বদলে যেত। dict(self.__dict__) একটি নতুন, স্বাধীন কপি তৈরি করে, তাই টেবিলের রো শুধুই save() কল করলে আপডেট হয় — ঠিক যেমন বাস্তব ডেটাবেসে না-কমিট-করা পরিবর্তন টেবিলে প্রতিফলিত হয় না।

প্র ০৩ User.objects.filter(name="Aporna") দুইটি ফলাফল ফেরত দেয় কেন, যদিও দুজনের ইমেইল আলাদা?

কারণ filter() শুধু যেসব কীওয়ার্ড আর্গুমেন্ট দেওয়া হয়েছে (এখানে শুধু name) তার সাথে মিলিয়ে দেখে — all(row.get(key) == value for key, value in criteria.items()) শর্তে শুধু name চেক হয়, email নয়। যেহেতু u1 ও u3 দুজনেরই name "Aporna", দুটো রো-ই শর্ত পূরণ করে এবং দুটোই ফেরত আসে।

অনুশীলন

  1. চিন্তা করুন: যদি save()-এর ভেতরের if self.id is None চেকটি সরিয়ে সবসময় cls._table.append(...) করা হতো, তাহলে একই ইউজারকে দুইবার save() কল করলে কী সমস্যা হতো?

    প্রতিবার save() কল করলেই নতুন একটি রো যোগ হয়ে যেত, এমনকি একই অবজেক্টের জন্যও — টেবিলে একই ইউজারের একাধিক ডুপ্লিকেট রো তৈরি হতো, প্রতিটির id আলাদা। এটাই দেখায় insert বনাম update পার্থক্য করাটা কতটা জরুরি — বাস্তব ডেটাবেসেও এই ভুল হলে প্রাইমারি-কী কনস্ট্রেইন্ট ভাঙে বা ডেটা ডুপ্লিকেট হয়ে যায়।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি নতুন User ইনস্ট্যান্স তৈরি করুন (email="rafi@example.com" দিয়ে, u2-এর মতোই ইমেইল), সেভ করুন, তারপর User.objects.filter(email="rafi@example.com") চালিয়ে দেখুন এখন কয়টি রো ফেরত আসে।

    এখন দুইটি ভিন্ন id-র রো ফেরত আসবে — একটি মূল u2 (যার email="rafi@example.com") এবং নতুন তৈরি করা ইনস্ট্যান্স, কারণ এই সহজ মিনি-ORM-এ কোনো ইউনিক কনস্ট্রেইন্ট নেই যা ডুপ্লিকেট ইমেইল আটকায় — বাস্তব ডেটাবেসে এটি একটি ইউনিক-কী কনস্ট্রেইন্ট (../dbms-sql/) দিয়ে আটকানো হয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

  • পরবর্তী পাঠ L29 মাইগ্রেশন ও স্কিমা ইভোলিউশন — স্কিমা কীভাবে সময়ের সাথে নিরাপদে বদলানো হয় দেখুন।
  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
  • Database Management Systems কোর্স সহোদর কোর্স উপরের ORM-এর পেছনের 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 — সব এক জায়গায়।
আগের পাঠ
HATEOAS ও API ডকুমেন্টেশন (OpenAPI/Swagger)