GraphQL ফান্ডামেন্টাল
এই পাঠে যা শিখবেন
- GraphQL কী এবং এটি কেন তৈরি হয়েছিল — single endpoint, client-specified response shape
- schema, resolver, ও query-শেপ — এই তিনটি ধারণা কীভাবে একসাথে কাজ করে
- স্কেলার ফিল্ড, নেস্টেড অবজেক্ট ফিল্ড, ও লিস্ট ফিল্ডের রেজলিউশন ভিন্নভাবে কীভাবে হ্যান্ডল করতে হয়
- Python দিয়ে একটি সম্পূর্ণ, রিকার্সিভ, সত্যিকারের কার্যকর কোয়েরি-এক্সিকিউশন ইঞ্জিন তৈরি ও চালানো
১ · GraphQL কী ও কেন
GraphQLGraphQLএকটি কোয়েরি ভাষা ও এক্সিকিউশন ইঞ্জিন, যেখানে ক্লায়েন্ট নিজেই ঠিক করে দেয় রেসপন্সে কোন কোন ফিল্ড থাকবে।
একটি single endpoint-ভিত্তিক API স্টাইল, যেখানে ক্লায়েন্ট প্রতিটি রিকোয়েস্টে নিজেই বলে দেয় ঠিক কোন কোন ফিল্ড
দরকার — সার্ভার সেই অনুযায়ী রেসপন্স শেপ তৈরি করে। এটি REST-এর বিপরীত, যেখানে প্রতিটি endpoint-এর রেসপন্স শেপ
আগে থেকেই সার্ভার-সাইডে ফিক্সড থাকে (../dbms-sql/ কোর্সে SQL-এর SELECT স্টেটমেন্টেও
এই একই ধারণা আছে — শুধু প্রয়োজনীয় কলাম বেছে নেওয়া)।
execute() শুধু প্রয়োজনীয় resolver-গুলোই কল করে — বাকিগুলো একেবারেই কল হয় না।২ · Schema, Resolver, ও Query-শেপ
GraphQL-এ তিনটি অংশ মিলে একটি রিকোয়েস্ট প্রসেস হয় — একটি schema (কোন টাইপে কোন ফিল্ড আছে ও প্রতিটির জন্য কোন ফাংশন ডেটা আনবে), ক্লায়েন্টের একটি query (কোন ফিল্ড আসলে দরকার), এবং একটি execute ইঞ্জিন (কোয়েরি অনুযায়ী schema-র সঠিক resolver-গুলো কল করে)।
প্রতিটি টাইপ (যেমন
User, Post) ও তার প্রতিটি ফিল্ডের জন্য একটি resolver ফাংশনের নেস্টেড ম্যাপিং।একটি নির্দিষ্ট ফিল্ডের জন্য প্রকৃত ডেটা এনে দেয় এমন ফাংশন — একটি প্যারেন্ট অবজেক্ট ও আর্গুমেন্ট নেয়, একটি মান রিটার্ন করে।
ক্লায়েন্ট কোন কোন ফিল্ড চায় তার একটি nested বর্ণনা — এখানে আমরা এটিকে সরাসরি একটি Python dict দিয়ে উপস্থাপন করছি।
৩ · Python দিয়ে একটি সত্যিকারের কোয়েরি-এক্সিকিউশন ইঞ্জিন
নিচের কোড সেলে SCHEMA নামে একটি নেস্টেড dict তৈরি করা হয়েছে যেখানে প্রতিটি ফিল্ডের সাথে একটি
প্রকৃত resolver ফাংশন ও তার রিটার্ন-টাইপ ("scalar", একটি টাইপের নাম, বা লিস্টের জন্য
"..._list") যুক্ত করা আছে। execute() ফাংশনটি একটি query-শেপ dict ওয়াক করে —
স্কেলার ফিল্ডের জন্য সরাসরি resolver কল করে, আর নেস্টেড/লিস্ট ফিল্ডের জন্য নিজেকেই আবার কল করে (রিকার্সন)
সাব-কোয়েরি প্রসেস করতে।
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-এর মূল প্রতিশ্রুতি: যা চাওয়া হয়নি,
তা কম্পিউটও করা হয় না।
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 শুধু আউটপুট ফিল্টার করে না,
অপ্রয়োজনীয় কাজও এড়িয়ে যায়।
অনুশীলন
-
চিন্তা করুন: যদি
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 আসবে। -
পরীক্ষা করুন: উপরের কোড সেলে
query["user"]["posts"]["_select"]-এ"title"-এর সাথে"body"-ও যোগ করুন (অর্থাৎ"_select": ["title", "body"]), তারপর Run চেপে দেখুন প্রতিটি পোস্টের রেজাল্টেbodyফিল্ডও এসেছে কি না।হ্যাঁ —
result["user"]["posts"]-এর প্রতিটি dict-এ এখনtitle-এর পাশাপাশিbody-ও থাকবে, কারণSCHEMA["Post"]["body"]["resolve"]এখনselectলিস্টে থাকায় কল হয়েছে। এটাই দেখায় ক্লায়েন্ট নিজের কোয়েরি বদলে সার্ভারের কোনো কোড না ছুঁয়েই রেসপন্সের শেপ নিয়ন্ত্রণ করতে পারে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ: GraphQL বনাম REST — কখন কোনটি ব্যবহার করবেন L44 একই ডেটার উপর REST-স্টাইল ফিক্সড-শেপ endpoint বনাম আজকের resolver/execute() ইঞ্জিন চালিয়ে over-fetching-এর প্রকৃত ফিল্ড-সংখ্যা গোনা হবে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
- Python Programming কোর্স সহোদর কোর্স উপরের কোড সেলে ব্যবহৃত dict comprehension, লুপ, ও lambda ফাংশনের ভাষাগত ভিত্তি সেই কোর্সে শেখানো হয়েছে।
- সব 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 — সব এক জায়গায়।