ORM প্যাটার্ন — অবজেক্টকে টেবিলে ম্যাপ করা
এই পাঠে যা শিখবেন
- ORM প্যাটার্ন কী সমস্যা সমাধান করে এবং কেন প্রায় সব ব্যাক-এন্ড ফ্রেমওয়ার্কে এটি থাকে
- একটি
Modelবেস ক্লাস কীভাবে সাবক্লাসিং-এর মাধ্যমে প্রতিটি "মডেল"-এর নিজস্ব ইন-মেমরি টেবিল দেয় (__init_subclass__) .save()মেথড কীভাবে insert বনাম update সিদ্ধান্ত নেয়.objects.filter(**kwargs)-স্টাইল কোয়েরি এবং রাউন্ড-ট্রিপ সঠিকতা (সেভ করা ডেটা ও ফেরত পাওয়া ডেটা হুবহু মেলা) হাতে-কলমে যাচাই করা
১ · ORM কী ও কেন দরকার
ORMObject-Relational Mappingডেটাবেস টেবিলের রো-কে পাইথন/জাভা/রুবি অবজেক্টে, এবং অবজেক্টকে আবার রো-তে — দুই দিকেই — ম্যাপ করার একটি প্যাটার্ন। এমন একটি প্যাটার্ন যা ডেটাবেসের রিলেশনাল ডেটা (টেবিল, রো, কলাম) এবং প্রোগ্রামিং ভাষার অবজেক্ট-ওরিয়েন্টেড ডেটা (ক্লাস, ইনস্ট্যান্স, অ্যাট্রিবিউট) — এই দুই জগতের মধ্যে একটি সেতু তৈরি করে। ORM ছাড়া প্রতিটি ডেটাবেস অপারেশনের জন্য হাতে SQL স্ট্রিং লিখতে হয়; ORM দিয়ে একই কাজ সাধারণ মেথড কল দিয়ে করা যায়।
INSERT INTO users (name, email) VALUES (...) — হাতে লেখা SQL স্ট্রিং, ডেটাবেস-নির্দিষ্ট সিনট্যাক্স।User(name=..., email=...).save() — সাধারণ পাইথন কোড, ORM ভেতরে SQL তৈরি করে (বাস্তব ORM-এ)।এই পাঠ ধরে নেয় টেবিল, রো, কলাম, প্রাইমারি কী-এর মতো SQL-এর মৌলিক ধারণাগুলো ইতিমধ্যে পরিচিত — Database Management Systems কোর্সে সেই ভিত্তি বিস্তারিত শেখানো হয়েছে। এখানে আমরা শুধু দেখব কীভাবে একটি ব্যাক-এন্ড ফ্রেমওয়ার্কের ORM স্তর সেই টেবিল-ধারণাগুলোকে অবজেক্টে মোড়ায়। Pyodide স্যান্ডবক্সে সত্যিকারের ডেটাবেস কানেকশন সম্ভব না, তাই নিচের "টেবিল" আসলে পাইথনের একটি লিস্ট — তবে তার উপর চলা লজিক (insert, filter) সত্যিকারের ও সম্পূর্ণ কার্যকর।
২ · একটি সত্যিকারের মিনি-ORM — Model বেস ক্লাস
নিচের Model ক্লাসে __init_subclass__ ব্যবহার করা হয়েছে — যখনই Model-এর
কোনো সাবক্লাস (যেমন User) সংজ্ঞায়িত হয়, পাইথন স্বয়ংক্রিয়ভাবে __init_subclass__
কল করে, এবং সেখানে সেই সাবক্লাসের নিজস্ব খালি _table লিস্ট ও _next_id কাউন্টার
তৈরি হয় — ফলে User ও ভবিষ্যতের অন্য যেকোনো মডেল একে অপরের টেবিল থেকে সম্পূর্ণ আলাদা থাকে।
.save(), টেবিল থেকে অবজেক্ট-শেপড ডেটা ফেরত পেতে .objects.filter() — এই দুই দিকের ম্যাপিংই ORM-এর মূল কাজ।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 স্টেটমেন্ট তৈরি হয়।
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", দুটো রো-ই শর্ত পূরণ করে এবং দুটোই ফেরত আসে।
অনুশীলন
-
চিন্তা করুন: যদি
save()-এর ভেতরেরif self.id is Noneচেকটি সরিয়ে সবসময়cls._table.append(...)করা হতো, তাহলে একই ইউজারকে দুইবারsave()কল করলে কী সমস্যা হতো?প্রতিবার
save()কল করলেই নতুন একটি রো যোগ হয়ে যেত, এমনকি একই অবজেক্টের জন্যও — টেবিলে একই ইউজারের একাধিক ডুপ্লিকেট রো তৈরি হতো, প্রতিটিরidআলাদা। এটাই দেখায় insert বনাম update পার্থক্য করাটা কতটা জরুরি — বাস্তব ডেটাবেসেও এই ভুল হলে প্রাইমারি-কী কনস্ট্রেইন্ট ভাঙে বা ডেটা ডুপ্লিকেট হয়ে যায়। -
পরীক্ষা করুন: উপরের কোড সেলে একটি নতুন
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 — সব এক জায়গায়।