পাঠ ৪৩ · ৫৮-এর মধ্যে · মডিউল ১০
Home / Courses / Full-Stack Web Frameworks / GraphQL ফান্ডামেন্টাল

GraphQL ফান্ডামেন্টাল

GraphQL Fundamentals
১১ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

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

  • GraphQL কী এবং এটি কেন তৈরি হয়েছিল — single endpoint, client-specified response shape
  • schema, resolver, ও query-শেপ — এই তিনটি ধারণা কীভাবে একসাথে কাজ করে
  • স্কেলার ফিল্ড, নেস্টেড অবজেক্ট ফিল্ড, ও লিস্ট ফিল্ডের রেজলিউশন ভিন্নভাবে কীভাবে হ্যান্ডল করতে হয়
  • Python দিয়ে একটি সম্পূর্ণ, রিকার্সিভ, সত্যিকারের কার্যকর কোয়েরি-এক্সিকিউশন ইঞ্জিন তৈরি ও চালানো

১ · GraphQL কী ও কেন

GraphQLGraphQLএকটি কোয়েরি ভাষা ও এক্সিকিউশন ইঞ্জিন, যেখানে ক্লায়েন্ট নিজেই ঠিক করে দেয় রেসপন্সে কোন কোন ফিল্ড থাকবে। একটি single endpoint-ভিত্তিক API স্টাইল, যেখানে ক্লায়েন্ট প্রতিটি রিকোয়েস্টে নিজেই বলে দেয় ঠিক কোন কোন ফিল্ড দরকার — সার্ভার সেই অনুযায়ী রেসপন্স শেপ তৈরি করে। এটি REST-এর বিপরীত, যেখানে প্রতিটি endpoint-এর রেসপন্স শেপ আগে থেকেই সার্ভার-সাইডে ফিক্সড থাকে (../dbms-sql/ কোর্সে SQL-এর SELECT স্টেটমেন্টেও এই একই ধারণা আছে — শুধু প্রয়োজনীয় কলাম বেছে নেওয়া)।

ক্লায়েন্ট কোয়েরি + প্রয়োজনীয় ফিল্ড /graphql (একটাই endpoint) execute() Query.user resolver User.posts resolver রেসপন্স শুধু চাওয়া ফিল্ড
ক্লায়েন্টের কোয়েরিতে কোন ফিল্ড চাওয়া হয়েছে তার উপর ভিত্তি করে execute() শুধু প্রয়োজনীয় resolver-গুলোই কল করে — বাকিগুলো একেবারেই কল হয় না।

২ · Schema, Resolver, ও Query-শেপ

GraphQL-এ তিনটি অংশ মিলে একটি রিকোয়েস্ট প্রসেস হয় — একটি schema (কোন টাইপে কোন ফিল্ড আছে ও প্রতিটির জন্য কোন ফাংশন ডেটা আনবে), ক্লায়েন্টের একটি query (কোন ফিল্ড আসলে দরকার), এবং একটি execute ইঞ্জিন (কোয়েরি অনুযায়ী schema-র সঠিক resolver-গুলো কল করে)।

Schema
প্রতিটি টাইপ (যেমন User, Post) ও তার প্রতিটি ফিল্ডের জন্য একটি resolver ফাংশনের নেস্টেড ম্যাপিং।
Resolver
একটি নির্দিষ্ট ফিল্ডের জন্য প্রকৃত ডেটা এনে দেয় এমন ফাংশন — একটি প্যারেন্ট অবজেক্ট ও আর্গুমেন্ট নেয়, একটি মান রিটার্ন করে।
Query-শেপ
ক্লায়েন্ট কোন কোন ফিল্ড চায় তার একটি nested বর্ণনা — এখানে আমরা এটিকে সরাসরি একটি Python dict দিয়ে উপস্থাপন করছি।

৩ · Python দিয়ে একটি সত্যিকারের কোয়েরি-এক্সিকিউশন ইঞ্জিন

নিচের কোড সেলে SCHEMA নামে একটি নেস্টেড dict তৈরি করা হয়েছে যেখানে প্রতিটি ফিল্ডের সাথে একটি প্রকৃত resolver ফাংশন ও তার রিটার্ন-টাইপ ("scalar", একটি টাইপের নাম, বা লিস্টের জন্য "...​_list") যুক্ত করা আছে। execute() ফাংশনটি একটি query-শেপ dict ওয়াক করে — স্কেলার ফিল্ডের জন্য সরাসরি resolver কল করে, আর নেস্টেড/লিস্ট ফিল্ডের জন্য নিজেকেই আবার কল করে (রিকার্সন) সাব-কোয়েরি প্রসেস করতে।

Python
import json

# ---------- আসল ডেটা (এখানে একটি in-memory "ডেটাবেস") ----------
users_db = {
    1: {"id": 1, "name": "Farhana Akter", "email": "farhana@example.com", "posts": [101, 102]},
    2: {"id": 2, "name": "Kamal Hossain", "email": "kamal@example.com", "posts": [103]},
}
posts_db = {
    101: {"id": 101, "title": "GraphQL শেখা শুরু", "body": "প্রথম পোস্ট...", "author_id": 1},
    102: {"id": 102, "title": "REST বনাম GraphQL", "body": "দ্বিতীয় পোস্ট...", "author_id": 1},
    103: {"id": 103, "title": "WebSocket পরিচিতি", "body": "তৃতীয় পোস্ট...", "author_id": 2},
}

# ---------- SCHEMA -- প্রতিটি ফিল্ডের জন্য একটি resolver + তার রিটার্ন-টাইপ ----------
SCHEMA = {
    "Query": {
        "user": {"resolve": lambda root, args: users_db.get(args.get("id")), "type": "User"},
    },
    "User": {
        "id":    {"resolve": lambda obj, args: obj["id"], "type": "scalar"},
        "name":  {"resolve": lambda obj, args: obj["name"], "type": "scalar"},
        "email": {"resolve": lambda obj, args: obj["email"], "type": "scalar"},
        "posts": {"resolve": lambda obj, args: [posts_db[pid] for pid in obj["posts"]], "type": "Post_list"},
    },
    "Post": {
        "id":        {"resolve": lambda obj, args: obj["id"], "type": "scalar"},
        "title":     {"resolve": lambda obj, args: obj["title"], "type": "scalar"},
        "body":      {"resolve": lambda obj, args: obj["body"], "type": "scalar"},
        "author_id": {"resolve": lambda obj, args: obj["author_id"], "type": "scalar"},
    },
}


# ---------- EXECUTE -- একটি query-শেপ রিকার্সিভভাবে ওয়াক করে সঠিক resolver কল করে ----------
def execute(query_shape, root, type_name):
    result = {}
    for field_name, sub in query_shape.items():
        field_def = SCHEMA[type_name][field_name]
        args = sub.get("_args", {})
        value = field_def["resolve"](root, args)
        field_type = field_def["type"]

        if field_type == "scalar":
            result[field_name] = value

        elif field_type.endswith("_list"):
            child_type = field_type[:-len("_list")]
            select = sub.get("_select", [])
            items = []
            for item in value:
                item_result = {f: SCHEMA[child_type][f]["resolve"](item, {}) for f in select}
                nested_keys = [k for k in sub if k not in ("_args", "_select") and k not in select]
                for nk in nested_keys:
                    item_result[nk] = execute({nk: sub[nk]}, item, child_type)[nk]
                items.append(item_result)
            result[field_name] = items

        else:
            child_type = field_type
            select = sub.get("_select", [])
            obj_result = {f: SCHEMA[child_type][f]["resolve"](value, {}) for f in select}
            nested_keys = [k for k in sub if k not in ("_args", "_select") and k not in select]
            for nk in nested_keys:
                obj_result[nk] = execute({nk: sub[nk]}, value, child_type)[nk]
            result[field_name] = obj_result

    return result


# ---------- একটি concrete কোয়েরি -- শুধু name, email, ও posts.title চাওয়া হচ্ছে ----------
query = {
    "user": {
        "_args": {"id": 1},
        "_select": ["name", "email"],
        "posts": {"_select": ["title"]},
    }
}

result = execute(query, root=None, type_name="Query")

print("কোয়েরি-শেপ (ক্লায়েন্ট যা চেয়েছে):")
print(json.dumps(query, ensure_ascii=False, indent=2))

print("\nচূড়ান্ত ফলাফল (resolver-রা প্রকৃতভাবে হিসাব করে দিয়েছে):")
print(json.dumps(result, ensure_ascii=False, indent=2))

    
লক্ষ্য করুন result["user"]-এ id নেই, এবং result["user"]["posts"]-এর প্রতিটি আইটেমে শুধু title আছে — body বা author_id নেই। এর কারণ SCHEMA["User"]["id"]-এর resolver কখনো কল-ই হয়নি, কারণ query["user"]["_select"] লিস্টে "id" ছিল না। একইভাবে posts-এর জন্য শুধু SCHEMA["Post"]["title"] resolver কল হয়েছে, বাকি তিনটি Post-resolver কল-ই হয়নি — এটাই GraphQL-এর মূল প্রতিশ্রুতি: যা চাওয়া হয়নি, তা কম্পিউটও করা হয় না।
মূল কথা · Key takeaway

GraphQL একটি single endpoint-এ resolver ফাংশনের একটি schema সাজিয়ে রাখে, আর ক্লায়েন্টের কোয়েরি ঠিক করে দেয় কোন resolver আসলে কল হবে। execute() ফাংশনটি রিকার্সিভভাবে কোয়েরি-শেপ ওয়াক করে — স্কেলার ফিল্ডের জন্য সরাসরি resolver কল করে, নেস্টেড অবজেক্ট বা লিস্টের জন্য নিজেকেই আবার কল করে। পরের পাঠে (L44) আমরা এই একই প্যাটার্নকে একটি ফিক্সড-শেপ REST endpoint-এর পাশাপাশি চালিয়ে দেখব ঠিক কতটা কম ডেটা GraphQL ফেরত পাঠায়।

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

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

প্র ০১ কেন execute() ফাংশনটি রিকার্সিভ (নিজেই নিজেকে কল করে) হতে হয়েছে?

কারণ একটি কোয়েরিতে নেস্টেড অবজেক্ট থাকতে পারে — উপরের উদাহরণে user-এর ভেতরে posts, আর তত্ত্বগতভাবে posts-এর প্রতিটি আইটেমের ভেতরেও আরেকটি নেস্টেড ফিল্ড থাকতে পারত (যেমন প্রতিটি পোস্টের কমেন্ট লিস্ট)। প্রতিটি নেস্টিং লেভেলের নিজস্ব টাইপ ও নিজস্ব _select আছে, তাই execute() নিজেকেই আবার কল করে সেই সাব-কোয়েরিটি প্রসেস করে — এভাবে যেকোনো গভীরতার নেস্টিং একই লজিক দিয়ে হ্যান্ডল হয়ে যায়, আলাদা কোড লেখা লাগে না।

প্র ০২ SCHEMA["User"]["posts"]-এর টাইপ "Post_list" রাখা হয়েছে, কিন্তু SCHEMA["Query"]["user"]-এর টাইপ শুধু "User" — এই পার্থক্য কেন দরকার?

execute()-কে জানতে হয় resolver-এর ফেরত-দেওয়া মান একটি একক অবজেক্ট নাকি অবজেক্টের একটি লিস্ট, কারণ দুটোর প্রসেসিং আলাদা — একক অবজেক্টের জন্য সরাসরি একটি dict তৈরি হয়, লিস্টের জন্য প্রতিটি আইটেমের উপর আলাদা করে লুপ চালিয়ে dict-দের একটি লিস্ট তৈরি হয়। "_list" সাফিক্স চেক করেই কোড বুঝে নেয় কোন শাখায় (branch) যেতে হবে।

প্র ০৩ যদি query["user"]["_select"]-এ "email" না থাকত, তাহলে SCHEMA["User"]["email"]["resolve"] ফাংশনটি কি আদৌ কল হতো?

না। execute()-এর else-শাখায় obj_result শুধু select লিস্টে থাকা ফিল্ডগুলোর জন্যই dict comprehension দিয়ে resolver কল করে ({f: SCHEMA[child_type][f]["resolve"](value, {}) for f in select})। "email" সেই লিস্টে না থাকলে সেই resolver-এর জন্য কোনো এন্ট্রিই তৈরি হয় না — অর্থাৎ শুধু রেসপন্সেই ফিল্ডটি বাদ পড়ে না, resolver ফাংশনটাই কল হয় না। এটাই দেখায় GraphQL শুধু আউটপুট ফিল্টার করে না, অপ্রয়োজনীয় কাজও এড়িয়ে যায়।

অনুশীলন

  1. চিন্তা করুন: যদি query["user"]["posts"]["_select"]-এ "title"-এর পাশাপাশি "id"-ও যোগ করা হয়, তাহলে result["user"]["posts"]-এর প্রতিটি dict-এ কী কী কী (key) থাকবে বলে মনে হয়?

    প্রতিটি পোস্ট dict-এ তখন "id" ও "title" — এই দুটি key থাকবে, যেমন {"id": 101, "title": "GraphQL শেখা শুরু"}। কারণ execute()-এর লিস্ট-শাখায় item_result তৈরি হয় select লিস্টের প্রতিটি নামের জন্য একটি করে key দিয়ে — select-এ দুটো নাম থাকলে dict-এও দুটো key আসবে।

  2. পরীক্ষা করুন: উপরের কোড সেলে query["user"]["posts"]["_select"]-এ "title"-এর সাথে "body"-ও যোগ করুন (অর্থাৎ "_select": ["title", "body"]), তারপর Run চেপে দেখুন প্রতিটি পোস্টের রেজাল্টে body ফিল্ডও এসেছে কি না।

    হ্যাঁ — result["user"]["posts"]-এর প্রতিটি dict-এ এখন title-এর পাশাপাশি body-ও থাকবে, কারণ SCHEMA["Post"]["body"]["resolve"] এখন select লিস্টে থাকায় কল হয়েছে। এটাই দেখায় ক্লায়েন্ট নিজের কোয়েরি বদলে সার্ভারের কোনো কোড না ছুঁয়েই রেসপন্সের শেপ নিয়ন্ত্রণ করতে পারে।

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

আগের পাঠ
হাইড্রেশন — সার্ভার ও ক্লায়েন্টের সংযোগ