পাঠ ৩১ · ৫৮-এর মধ্যে · মডিউল ৭
Home / Courses / Full-Stack Web Frameworks / কোয়েরি বিল্ডার বনাম SQL বনাম ORM

কোয়েরি বিল্ডার বনাম রॉ SQL বনাম ORM

Query builders vs raw SQL vs ORM
১১ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • তিনটি কোয়েরি-প্রকাশ পদ্ধতির মধ্যে বিমূর্ততা ও নিয়ন্ত্রণের ট্রেড-অফ
  • একটি ফ্লুয়েন্ট, চেইনযোগ্য QueryBuilder কীভাবে .where()/.order_by() দিয়ে ধাপে ধাপে একটি কোয়েরি-স্পেক তৈরি করে, তারপর .execute() দিয়ে প্রয়োগ করে
  • কেন একটি লিটারাল SQL স্ট্রিং শুধু "দেখানো" যায়, এই কোর্সে এক্সিকিউট করা যায় না
  • দুইটি ভিন্ন পদ্ধতির (কোয়েরি বিল্ডার, ORM) ফলাফল একই কি না সেট-তুলনা দিয়ে যাচাই করা

১ · তিনটি পদ্ধতি, একই লক্ষ্য

প্রায় প্রতিটি ব্যাক-এন্ড ফ্রেমওয়ার্কে ডেটাবেস কোয়েরি করার জন্য একাধিক স্তরের বিমূর্ততা পাওয়া যায়। নিচের চিপ-গ্রিডে তিনটি পদ্ধতির মূল বৈশিষ্ট্য তুলনা করা হলো — একই লক্ষ্য অর্জনের তিনটি ভিন্ন উপায়।

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

২ · একই কোয়েরি তিনভাবে — কোড ও যাচাই

লক্ষ্য: "১৮ বছরের বেশি বয়সী সব ইউজার, নাম অনুসারে (A→Z) সাজানো"। নিচের কোড সেলে এই একই কোয়েরি তিনভাবে লেখা হয়েছে, এবং কোয়েরি বিল্ডার ও ORM ভার্সনের ফলাফল একই কি না সেট-তুলনা দিয়ে যাচাই করা হয়েছে।

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 save(self):
        cls = type(self)
        self.id = cls._next_id
        cls._next_id += 1
        cls._table.append(dict(self.__dict__))
        return self


class User(Model):
    pass


people = [("Zafar", 25), ("Aporna", 30), ("Karim", 15), ("Bithi", 22), ("Nadia", 17), ("Rafi", 40)]
for name, age in people:
    User(name=name, age=age).save()


# ---------- ১. ফ্লুয়েন্ট কোয়েরি বিল্ডার ----------
class QueryBuilder:
    def __init__(self, table):
        self.table = table
        self._filters = {}
        self._order_key = None

    def where(self, **criteria):
        self._filters.update(criteria)
        return self          # নিজেকেই রিটার্ন করে -- চেইনিং সম্ভব করে তোলে

    def order_by(self, key):
        self._order_key = key
        return self

    def execute(self):
        result = []
        for row in self.table:
            ok = True
            for key, value in self._filters.items():
                if key.endswith("__gt"):
                    field = key[: -len("__gt")]
                    if not (row.get(field) is not None and row[field] > value):
                        ok = False
                        break
                elif row.get(key) != value:
                    ok = False
                    break
            if ok:
                result.append(row)
        if self._order_key:
            result = sorted(result, key=lambda r: r[self._order_key])
        return result


qb_result = QueryBuilder(User._table).where(age__gt=18).order_by("name").execute()
print("১. কোয়েরি বিল্ডার ফলাফল:")
for row in qb_result:
    print(" ", row["name"], row["age"])


# ---------- ২. লিটারাল SQL -- শুধু একটি স্ট্রিং, এক্সিকিউট করা হয় না ----------
raw_sql = "SELECT * FROM users WHERE age > 18 ORDER BY name ASC;"
print("\n২. লিটারাল SQL (শুধু দেখানো হলো, এক্সিকিউট করা হয়নি):")
print(" ", raw_sql)


# ---------- ৩. মিনি-ORM স্টাইল ----------
# এই সহজ filter() শুধু ইকুয়ালিটি সাপোর্ট করে (L28), তাই বয়সের ">" শর্ত সাধারণ পাইথন দিয়ে প্রয়োগ করা হলো --
# একটি বাস্তব ORM-এ (Django-এর age__gt=18) এই একই কাজ ভেতরে সত্যিকারের SQL দিয়ে হতো
all_users = User.objects.filter()
orm_result = sorted((row for row in all_users if row["age"] > 18), key=lambda r: r["name"])
print("\n৩. মিনি-ORM ফলাফল:")
for row in orm_result:
    print(" ", row["name"], row["age"])


# ---------- অভিন্নতা যাচাই ----------
qb_set = {(r["name"], r["age"]) for r in qb_result}
orm_set = {(r["name"], r["age"]) for r in orm_result}
identical = qb_set == orm_set
print(f"\nকোয়েরি বিল্ডার ও ORM ফলাফল অভিন্ন: {identical}")
assert identical

    
লক্ষ্য করুন QueryBuilder.where() ও .order_by() উভয়েই self রিটার্ন করে — এটিই চেইনিং সম্ভব করে তোলে (.where(...).order_by(...).execute() একটি লাইনে লেখা যায়)। মিনি-ORM-এর filter() শুধু ইকুয়ালিটি চেক করে, তাই age > 18-এর মতো একটি "greater than" শর্তের জন্য এখানে সাধারণ পাইথন জেনারেটর এক্সপ্রেশন ব্যবহার করা হয়েছে — একটি বাস্তব ORM (যেমন Django) এই একই কাজ age__gt=18-এর মতো একটি কী-ওয়ার্ড কনভেনশন দিয়ে ভেতরে সামলায়। শেষে qb_set == orm_set সমতা প্রমাণ করে দুটি সম্পূর্ণ ভিন্ন কোড-পথ একই সঠিক ফলাফলে পৌঁছেছে।
মূল কথা · Key takeaway

রॉ SQL, কোয়েরি বিল্ডার, ও ORM — তিনটিই একই অন্তর্নিহিত প্রশ্নের ("কোন রো চাই, কোন ক্রমে") ভিন্ন উপস্থাপনা। কোয়েরি বিল্ডার SQL-এর নিয়ন্ত্রণ ও প্রোগ্রামিং ভাষার নিরাপত্তা/কম্পোজেবিলিটির মাঝামাঝি একটি ভারসাম্য দেয়, আর ORM সবচেয়ে বেশি বিমূর্ততা দেয় দাম হিসেবে কিছুটা নিয়ন্ত্রণ ছেড়ে। বাস্তব প্রজেক্টে তিনটিই একসাথে ব্যবহৃত হয় — সাধারণ কোয়েরিতে ORM, জটিল/পারফরম্যান্স-ক্রিটিক্যাল কোয়েরিতে রॉ SQL বা কোয়েরি বিল্ডার।

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

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

প্র ০১ raw_sql ভেরিয়েবলটি কেন শুধু একটি স্ট্রিং, এবং কোড সেলে সেটি চালিয়ে সত্যিকারের ফলাফল কেন দেখানো হয়নি?

কারণ এই কোর্সের Pyodide স্যান্ডবক্সে কোনো সত্যিকারের ডেটাবেস কানেকশন নেই (কোর্সের CLAUDE.md-এ বর্ণিত একটি সুস্পষ্ট সীমাবদ্ধতা) — শুধু ইন-মেমরি পাইথন ডেটা স্ট্রাকচার নিয়ে কাজ করা সম্ভব। SQL স্ট্রিংটি এখানে শুধু দেখানোর জন্য যে এটি দেখতে কেমন হতো; একটি বাস্তব ডেটাবেসের বিপরীতে এই একই SQL সত্যিকারভাবে এক্সিকিউট করার প্র্যাকটিস ../dbms-sql/ কোর্সে কভার করা হয়েছে।

প্র ০২ মিনি-ORM ভার্সনে কেন User.objects.filter() কোনো আর্গুমেন্ট ছাড়া কল করে তারপর ম্যানুয়ালি বয়স ফিল্টার করা হলো, সরাসরি filter(age=18)-এর মতো কিছু কেন লেখা হলো না?

কারণ এই লেসনের filter() শুধু ইকুয়ালিটি (==) চেক করে — এটি "১৮-এর বেশি" বোঝার কোনো উপায় জানে না। filter(age=18) লিখলে শুধু ঠিক ১৮ বছর বয়সী ইউজার পাওয়া যেত, যা প্রশ্নের উত্তর না। তাই filter()-কে আর্গুমেন্ট ছাড়া কল করে (যা পুরো টেবিল ফেরত দেয়) তারপর সাধারণ পাইথন দিয়ে age > 18 শর্ত প্রয়োগ করা হয়েছে — এটি স্পষ্ট করে যে এই সহজ ORM-এর একটি বাস্তব সীমাবদ্ধতা আছে, যা বাস্তব ORM-গুলো age__gt-এর মতো বিশেষ কী-ওয়ার্ড কনভেনশন দিয়ে সমাধান করে।

প্র ০৩ অভিন্নতা যাচাইয়ে সরাসরি লিস্ট তুলনা (qb_result == orm_result) না করে সেট তুলনা (qb_set == orm_set) করা হলো কেন?

লিস্ট তুলনা উপাদানের ক্রমও মিলতে হয়, কিন্তু "একই ফলাফল সেট" বলতে বোঝানো হয়েছে একই সারিগুলো পাওয়া গেছে কি না, ক্রম নয় (যদিও এখানে দুটোই order_by("name")-এর কারণে একই ক্রমে আছে)। এছাড়া dict সরাসরি তুলনাযোগ্য হলেও set-এ রাখতে হ্যাশযোগ্য উপাদান লাগে, তাই প্রতিটি রো থেকে (name, age) টাপল বের করে সেট বানানো হয়েছে — এটি একটি নিরাপদ, ক্রম-নিরপেক্ষ তুলনা পদ্ধতি।

অনুশীলন

  1. চিন্তা করুন: যদি QueryBuilder.order_by()-এ self রিটার্ন না করে None রিটার্ন করা হতো, তাহলে QueryBuilder(User._table).where(age__gt=18).order_by("name").execute() লাইনটি চালালে কী হতো?

    .order_by("name") কল None রিটার্ন করত, এবং পরের .execute() কলটি None-এর উপর কল হতো — যা AttributeError: 'NoneType' object has no attribute 'execute' ছুড়ে ফেলত। এটাই দেখায় ফ্লুয়েন্ট চেইনিং কাজ করার জন্য প্রতিটি মেথডকে অবশ্যই self (বা অন্তত একটি সামঞ্জস্যপূর্ণ অবজেক্ট) রিটার্ন করতে হবে।

  2. পরীক্ষা করুন: উপরের কোড সেলে QueryBuilder-এ একটি নতুন মেথড limit(n) যোগ করুন যা self._limit = n সেট করে self রিটার্ন করে, তারপর execute()-এর শেষে if self._limit-চেক দিয়ে ফলাফলকে result[: self._limit]-এ কেটে ফেলুন, এবং .limit(2) চেইন করে দেখুন শুধু প্রথম দুইজন ইউজারই ফেরত আসে কি না।

    __init__-এ self._limit = None যোগ করে, limit(n) মেথডে self._limit = n; return self লিখে, এবং execute()-এর return-এর ঠিক আগে if self._limit: result = result[: self._limit] যোগ করলে .where(age__gt=18).order_by("name").limit(2).execute() এখন সাজানো ফলাফলের প্রথম দুইটি রো (Aporna, Bithi) ফেরত দেবে — এটি দেখায় কোয়েরি বিল্ডার প্যাটার্নটি সহজেই নতুন ক্ষমতা (limit, offset, ইত্যাদি) দিয়ে বিস্তৃত করা যায়, প্রতিটি নতুন মেথডই শুধু চেইনে আরেকটি ধাপ যোগ করে।

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

  • পরবর্তী পাঠ L32 N+1 কোয়েরি সমস্যা ও Eager Loading — কোয়েরি সংখ্যা গুনে পারফরম্যান্স সমস্যা বাস্তবে দেখুন।
  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
  • Database Management Systems কোর্স সহোদর কোর্স বাস্তব SQL SELECT/WHERE/ORDER BY এক্সিকিউশন সেই কোর্সে বিস্তারিত শেখানো হয়েছে।
  • সব 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 — সব এক জায়গায়।
আগের পাঠ
রিলেশনশিপ মডেলিং — ওয়ান-টু-মেনি ও মেনি-টু-মেনি