পরিষ্কার Go ডেটা লেয়ারের জন্য Go জেনেরিকস CRUD রিপোজিটরি প্যাটার্ন
পাঠক্তিকভাবে ব্যবহারযোগ্য Go জেনেরিক CRUD রিপোজিটরি প্যাটার্ন শিখুন: পুনরায় ব্যবহারযোগ্য List/Get/Create/Update/Delete ফ্লো, স্পষ্ট কনস্ট্রেইন্ট, reflection ছাড়া এবং পড়তে সহজ কোড।

কেন Go-তে CRUD রিপোজিটরি ঝামেলায় পড়ে\n\nCRUD রিপোজিটরি শুরুতে সাধারণ হয়। আপনি লিখেন GetUser, পরে ListUsers, তারপর Orders, তারপর Invoices। কয়েকটি এন্টিটি পরে ডেটা লেয়ার নকল কপির গুচ্ছ হয়ে যায় যেখানে ছোট পরিবর্তনগুলো হারিয়ে যায়।\n\nসবচেয়ে বেশি যেটা বারবার হয় তা SQL নয়। সেটা হচ্ছে চারপাশের প্রবাহ: কোয়েরি চালানো, রো স্ক্যান করা, “not found” হ্যান্ডল করা, ডাটাবেস এররগুলো ম্যাপ করা, pagination ডিফল্ট প্রয়োগ করা, এবং ইনপুটগুলো সঠিক টাইপে কনভার্ট করা।\n\nপরিচিত হটস্পটগুলো: কপিকৃত Scan কোড, context.Context এবং ট্রানজেকশন প্যাটার্নের পুনরাবৃত্তি, boilerplate LIMIT/OFFSET হ্যান্ডলিং (কখনো কখনো total count-সহ), “0 rows means not found” চেক, এবং কপি-পেস্ট করা INSERT ... RETURNING id ভ্যারিয়েশন।\n\nযখন পুনরাবৃত্তি ক্ষতিতকর হয়ে ওঠে, অনেক টিম reflection-এ হাত বাড়ায়। এটি “একবার লিখুন” CRUD-এর প্রতিশ্রুতি দেয়: যে কোনো struct নিন এবং কলামগুলো রuntime-এ ভরিয়ে দিন। খরচ পরে দেখা যায়। reflection-ভিত্তিক কোড পড়তে কঠিন, IDE সমর্থন খারাপ হয়, এবং ব্যর্থতা compile time থেকে runtime-এ চলে যায়। ফিল্ডের নাম বদলানো বা nullable কলাম যোগ করা ছোট বদলগুলো কেবল টেস্ট বা প্রোডাকশনে আশ্চর্যতা তৈরি করে।\n\nটাইপ-সেফ পুনঃব্যবহার মানে হলো CRUD ফ্লো শেয়ার করা কিন্তু Go-এর প্রতিদিনের সুবিধাগুলো হারানো নয়: পরিষ্কার সিগনেচার, কম্পাইলার-চেকড টাইপ, এবং autocomplete যা সত্যিই সহায়ক। জেনেরিকস দিয়ে আপনি Get[T] ও List[T] মতো অপারেশন reuse করতে পারেন, একই সময়ে প্রত্যেক এন্টিটি থেকে যে অংশগুলো অনুমান করা যায় না (যেমন কিভাবে একটি রো T-তে স্ক্যান করতে হয়) সেগুলো স্পষ্টভাবে দাবি করতে পারেন।\n\nএই প্যাটার্নটি ইচ্ছাকৃতভাবে ডেটা অ্যাক্সেস লেয়ারের সম্পর্কে। এটি SQL ও ম্যাপিং কনসিস্টেন্ট ও নিস্তেজ রাখে। এটি আপনার ডোমেইন মডেল করার বা বিজনেস রুল আরোপ করার চেষ্টা করে না, কিংবা সার্ভিস-লেভেল লজিক প্রতিস্থাপন করে না।\n\n## ডিজাইন লক্ষ্য (ও কি বিষয়সমূহ এখানে সমাধান হবে না)\n\nএকটি ভালো রিপোজিটরি প্যাটার্ন প্রতিদিনের ডাটাবেস অ্যাক্সেসকে পূর্বানুমানযোগ্য করে তোলে। আপনি একটি রিপোজিটরি পড়লেও দ্রুত বুঝে ফেলতে পারেন এটা কী করছে, কোন SQL চালায়, এবং কোন এররগুলো ফেরত দিতে পারে।\n\nলক্ষ্যগুলো সহজ:\n\n- টার্মিনাল পর্যন্ত টাইপ সেফটি (IDs, entities, ও results any নয়)\n- সীমাবদ্ধতাগুলো উদ্দেশ্য ব্যাখ্যা করবে টাইট টাইপ-জিমন্যাস্টিকস ছাড়া\n- গুরুত্বপূর্ণ আচরণ লুকোয় না এমনভাবে বুয়াইলারপ্লেট কমানো\n- List/Get/Create/Update/Delete জুড়ে ধারাবাহিক আচরণ\n\nনন-গোলগুলোও সমানভাবে গুরুত্বপূর্ণ। এটা ORM নয়। এটি ফিল্ড ম্যাপিং অনুমান করা, টেবিল স্বয়ংক্রিয়ভাবে join করা, বা নীরবভাবে কোয়েরি পরিবর্তন করা উচিত নয়। “ম্যাজিক ম্যাপিং” আপনাকে পুনরায় reflection, ট্যাগস, ও এজ কেসে ফিরে নিয়ে যাবে।\n\nসাধারণ SQL ওয়ার্কফ্লো ধরে নিন: স্পষ্ট SQL (বা পাতলা query builder), পরিষ্কার ট্রানজেকশন সীমা, এবং এমন এররগুলো যেগুলো আপনি বিবেচনা করে সমস্যা বুঝতে পারবেন। যখন কিছু ব্যর্থ হয়, এররটা বলবে “not found”, “conflict/constraint violation”, বা “DB unavailable” — না যে অস্পষ্ট “repository error”।\n\nকী সিদ্ধান্ত নিতে হবে তা হলো কোনটা জেনেরিক হবে এবং কোনটা প্রতিটি এন্টিটির জন্য রেখে দেবেন।\n\n- জেনেরিক: প্রবাহ (কোয়েরি চালানো, স্ক্যান, টাইপেড ভ্যালু ফেরত, সাধারণ এরর অনুবাদ)।\n- প্রতিটি এন্টিটি: অর্থ (টেবিল নাম, সিলেক্ট করা কলাম, joins, এবং SQL স্ট্রিং)।\n\nসব এন্টিটিকে একক ফিল্টার সিস্টেমে জোর করলে কোড পড়তে কঠিন হয়ে পড়ে — অনেকক্ষেত্রে two clear queries লেখা সহজ।\n\n## এন্টিটি এবং ID সীমাবদ্ধতা নির্বাচন করা\n\nঅধিকাংশ CRUD কোড বারবার হয় কারণ প্রতিটি টেবিলে একই মৌলিক কাজগুলো আছে, কিন্তু প্রতিটি এন্টিটির নিজস্ব ফিল্ড। জেনেরিকস নিয়ে কৌশল হল একটি ছোট শেপ শেয়ার করা এবং বাকিটা মুক্ত রাখা।\n\nশুরুতে সিদ্ধান্ত নিন রিপোজিটরিটি বাস্তবে এন্টিটি সম্পর্কে কী জানতে হবে। অনেক দলের জন্য একমাত্র সার্বজনীন অংশ হল ID। টাইমস্ট্যাম্প দরকার হতে পারে, কিন্তু সব জায়গায় বাধ্য করলে মডেল ভানচি মনে হতে পারে।\n\n### এমন একটি ID টাইপ বেছে নিন যাতে আপনি থাকতে পারেন\n\nআপনার ID টাইপটি ডাটাবেসে কিভাবে সারি শনাক্ত হয় তার মিল রাখা উচিত। কিছু প্রজেক্ট int64 ব্যবহার করে, অন্যরা UUID string। যদি একটি অভিন্ন পদ্ধতি সার্ভিসজুড়ে কাজ করে, তাহলে ID জেনেরিক করুন। যদি পুরো কোডবেস একটি ID টাইপ ব্যবহার করে, সেটি ফিক্স করে রাখা সিগনেচার ছোট রাখতে পারে।\n\nএকটি ভাল ডিফল্ট কনস্ট্রেইন্ট হলো comparable, কারণ আপনি ID তুলনা করবেন, map কী হিসেবে ব্যবহার করবেন, এবং এগুলো পাস করবেন।\n\ngo\ntype ID interface {\n\tcomparable\n}\n\ntype Entity[IDT ID] interface {\n\tGetID() IDT\n\tSetID(IDT)\n}\n\n\n### এন্টিটি কনস্ট্রেইন্ট যতটা সম্ভব ছোট রাখুন\n\nstruct embedding বা ~struct{...} মতো টাইপ-সেট ট্রিকস ব্যবহার করে ফিল্ড বাধ্য করা এড়িয়ে চলুন। এগুলো শক্তিশালী দেখায়, তবে আপনার ডোমেইন টাইপগুলোকে রিপোজিটরি প্যাটার্নের সঙ্গে জোড়ে দেবে।\n\nবরং, কেবল সেইগুলো দাবি করুন যেগুলো শেয়ার্ড CRUD প্রবাহকে লাগে:\n\n- ID পেতে ও সেট করতে পারা (তাতে Create ID ফেরত দিতে পারে, এবং Update/Delete লক্ষ্য করতে পারে)\n\nপরে যদি soft deletes বা optimistic locking যোগ করতে চান, ছোট opt-in ইন্টারফেস যোগ করুন (উদাহরণ: GetVersion/SetVersion) এবং কেবল যেখানে দরকার সেখানেই ব্যবহার করুন। ছোট ইন্টারফেসগুলো সাধারনত ভালোভাবে বয়স বাড়ায়।\n\n## একটি জেনেরিক রিপোজিটরি ইন্টারফেস যা পড়তে সহজ থাকবে\n\nরিপোজিটরি ইন্টারফেসে আপনি যা চান তা বর্ণনা করুন, না যে ডাটাবেস কীভাবে কাজ করে তা। যদি ইন্টারফেস SQL-র মতো হয়, তা সর্বত্র ডিটেইল ফাঁস করে।\n\nমেথড সেট ছোট ও প্রত্যাশিত রাখুন। context.Context প্রথম দিন, তারপর প্রধান ইনপুট (ID বা ডেটা), তারপর বিকল্প নকসগুলো একটি struct-এ বান্ডেল করুন।\n\ngo\ntype Repository[T any, ID comparable, CreateIn any, UpdateIn any, ListQ any] interface {\n\tGet(ctx context.Context, id ID) (T, error)\n\tList(ctx context.Context, q ListQ) ([]T, error)\n\tCreate(ctx context.Context, in CreateIn) (T, error)\n\tUpdate(ctx context.Context, id ID, in UpdateIn) (T, error)\n\tDelete(ctx context.Context, id ID) error\n}\n\n\nList-এর জন্য একটি সার্বজনীন ফিল্টার টাইপ জোর না করা ভাল। ফিল্টারগুলোই এন্টিটিগুলোর মধ্যে সবচেয়ে আলাদা। একটি ব্যবহারিক পদ্ধতি হলো প্রতি-এন্টিটি কুয়েরি টাইপ এবং একটি ছোট শেয়ার্ড pagination shape আপনি embed করতে পারেন।\n\ngo\ntype Page struct {\n\tLimit int\n\tOffset int\n}\n\n\nএরর হ্যান্ডলিং এমন জায়গা যেখানে রিপোজিটরি প্রায়শই গোলমাল করে। আগেই সিদ্ধান্ত নিন কোন এররগুলোর উপর কলাররা শাখা করবে। সাধারণ সেটটি কাজ করে:\n\n- ErrNotFound যখন কোনো ID নেই\n- ErrConflict ইউনিক ব্যর্থতা বা ভার্সন সংঘর্ষের জন্য\n- ErrValidation ইনপুট অবৈধ হলে (শুধুমাত্র যদি রিপো ভ্যালিডেট করে)\n\nবাকি সবকিছু low-level এরর হিসেবে wrap করুন (DB/network)। এই কনট্রাক্ট থাকলে সার্ভিস কোড “not found” বা “conflict” হ্যান্ডল করতে পারবে তার পরের স্টোরেজ কি PostgreSQL নাকি অন্য কিছু সেটা নিয়ে চিন্তা না করে।\n\n## কিভাবে reflection ছাড়াই ফ্লো reuse করবেন\n\nReflection সাধারণত তখন প্রবেশ করে যখন আপনি একটি কোড চান যা “কোনও struct-ই পূরণ করবে”। এটি ত্রুটিগুলো runtime-এ লুকায় এবং নিয়মগুলো অস্পষ্ট করে।\n\nএকটি পরিষ্কার পদ্ধতি হলো শুধুমাত্র নিস্তেজ অংশগুলো reuse করা: কোয়েরি চালানো, রো লুপ, প্রভাবিত রো গণনা চেক, এবং এররগুলি ধারাবাহিকভাবে wrap করা। struct-এ ম্যাপিং স্পষ্ট রাখুন।\n\n### দায়িত্ব ভাগ করুন: SQL, ম্যাপিং, শেয়ার্ড ফ্লো\n\nপ্রায়োগিক ভাগদারি দেখতে এমন হতে পারে:\n\n- প্রতিটি এন্টিটি: SQL স্ট্রিং ও প্যারামিটার অর্ডার এক স্থানে রাখুন\n- প্রতিটি এন্টিটি: ছোট ম্যাপিং ফাংশন লিখুন যা রোকে কনক্রিট struct-এ স্ক্যান করে\n- জেনেরিক: শেয়ার্ড ফ্লো প্রদান করবে যা কোয়েরি চালায় এবং ম্যাপার কল করে\n\nএভাবে জেনেরিকস পুনরাবৃত্তি কমায় কিন্তু ডাটাবেস কি করছে সেটা লুকায় না।\n\nনিচে একটি ছোট অ্যাবস্ট্রাকশন আছে যা *sql.DB বা *sql.Tx দুটোকেই পাস করতে দেয়: \n\ngo\ntype DBTX interface {\n\tExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)\n\tQueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)\n\tQueryRowContext(ctx context.Context, query string, args ...any) *sql.Row\n}\n\n\n### জেনেরিকস কী করা উচিত (ও কী না)\n\nজেনেরিক স্তরটি আপনার struct “বোঝার” চেষ্টা করবে না। বরং এটি স্পষ্ট ফাংশন নেবে, যেমন:\n\n- একটি binder যা ইনপুটকে কোয়েরি আর্গস-এ পরিণত করে\n- একটি scanner যা কলামগুলো entity-তে পড়ে\n\nউদাহরণস্বরূপ, একটি Customer রিপোজিটরি SQL কনস্ট্যান্ট (selectByID, insert, update) হিসাবে রাখে এবং একবার scanCustomer(rows) ইমপ্লিমেন্ট করে। একটি জেনেরিক List লুপ, context, ও এরর wrapping হ্যান্ডল করবে, আর scanCustomer টাইপ-সেফটি ও স্পষ্ট ম্যাপিং রাখবে।\n\nনতুন কলাম যোগ করলে SQL ও scanner আপডেট করুন — কম্পাইলারই আপনাকে বলবে কী ভাঙেছে।\n\n## ধাপে ধাপে: প্যাটার্ন ইমপ্লিমেন্ট করা\n\nলক্ষ্য হলো List/Get/Create/Update/Delete-এর জন্য এক reusable ফ্লো যেটা প্রতিটি রিপোজিটরিকে তার SQL ও রো ম্যাপিং সম্পর্কে সৎ রাখে।\n\n### 1) কোর টাইপগুলো সংজ্ঞায়িত করুন\n\nকমপক্ষে সীমাবদ্ধতা দিয়ে শুরু করুন। আপনার কোডবেসের জন্য একটি ID টাইপ বেছে নিন এবং একটি প্রত্যাশিত রিপোজিটরি ইন্টারফেস রাখুন।\n\ngo\ntype ID interface{ ~int64 | ~string }\n\ntype Repo[E any, K ID] interface {\n\tGet(ctx context.Context, id K) (E, error)\n\tList(ctx context.Context, limit, offset int) ([]E, error)\n\tCreate(ctx context.Context, e *E) error\n\tUpdate(ctx context.Context, e *E) error\n\tDelete(ctx context.Context, id K) error\n}\n\n\n### 2) DB ও ট্রানজেকশনের জন্য একটি executor যোগ করুন\n\nজেনেরিক কোড সরাসরি *sql.DB বা *sql.Tx-এর সঙ্গে বাঁধবেন না। একটি ছোট executor ইন্টারফেসের উপর নির্ভর করুন যা আপনি ডাকে (QueryContext, ExecContext, QueryRowContext)। সেবা থেকে DB বা ট্রানজেকশন পাস করলে রিপোজিটরির কোড বদলাতে হবে না।\n\n### 3) শেয়ার্ড ফ্লো সহ একটি জেনেরিক বেস তৈরি করুন\n\nbaseRepo[E,K] তৈরি করুন যা executor এবং কিছু ফাংশন ফিল্ড সংরক্ষণ করে। বেসটি বিরক্তিকর কাজগুলো হ্যান্ডল করবে: কোয়েরি কল করা, “not found” ম্যাপ করা, প্রভাবিত রো চেক করা, এবং কনসিস্টেন্ট এরর ফেরত দেয়া।\n\n### 4) এন্টিটি-নির্দিষ্ট অংশগুলো ইমপ্লিমেন্ট করুন\n\nপ্রতিটি এন্টিটি রিপো যা জেনেরিক করা যায় না সেটাই সরবরাহ করবে:\n\n- list/get/create/update/delete-এর SQL\n- scan(row) ফাংশন যা রোকে E-তে রূপান্তর করে\n- bind(...) ফাংশন যা কোয়েরি আর্গস রিটার্ন করে\n\n### 5) কনক্রিট রিপোওয়াগুলো ও সার্ভিসে ওগুলো ব্যবহার করুন\n\nNewCustomerRepo(exec Executor) *CustomerRepo তৈরি করুন যা baseRepo embed বা wrap করে। সার্ভিস লেয়ার Repo[E,K] ইন্টারফেসে নির্ভর করে এবং সিদ্ধান্ত নেয় কখন ট্রানজেকশন শুরু করতে হবে; রিপোজিটরি কেবল যে executor দেওয়া হয়েছে সেটা ব্যবহার করে।\n\n## List/Get/Create/Update/Delete হ্যান্ডল করা যাতে ঝামেলা না আসে\n\nজেনেরিক রিপোজিটরি তখনই সাহায্য করে যখন প্রতিটি মেথড সব জায়গায় একইভাবে আচরণ করে। বেশিরভাগ সমস্যা ছোট অনিয়ম থেকে আসে: এক রিপো created_at দিয়ে order করে, অন্যটি id দিয়ে; একটি nil, nil ফেরত দেয় missing rows-এ, অন্যটি এরর দেয়।\n\n### List: pagination ও ordering যাতে আইটেম সরে না যায়\n\nএকটি pagination স্টাইল বেছে নিন এবং তা ধারাবাহিকভাবে প্রয়োগ করুন। Offset pagination (limit/offset) সিম্পল এবং অ্যাডমিন স্ক্রিনের জন্য ভাল। Cursor pagination অনন্ত স্ক্রলিংয়ের জন্য ভালো, কিন্তু এটি একটি স্থিতিশীল sort key চাই।\n\nযা কিছু বেছে নিন, ordering স্পষ্ট ও স্থিতিশীল রাখুন। ইউনিক কলাম (প্রাইমারি কী) দিয়ে order করলে নতুন রেকর্ড এলে আইটেম পেজ বদলে যাবে না।\n\n### Get: স্পষ্ট “not found” সিগন্যাল\n\nGet(ctx, id) একটি টাইপেড এন্টিটি ও স্পষ্ট মিসিং-রেকর্ড সংকেত ফেরত দিন — সাধারণত ErrNotFound মতো শেয়ার্ড sentinel। শূন্য-মূল্য এন্টিটি ও nil এরর ফেরত দিলে কলাররা বুঝতে পারবে না “মিসিং” না “খালি ফিল্ড”।\n\nটাইপ ডেটার জন্য, এরর স্টেটের জন্য।\n\nমেথড ইমপ্লিমেন্ট করার আগে কয়েকটি সিদ্ধান্ত নিন এবং সেগুলো কনসিস্টেন্ট রাখুন:\n\n- Create: আপনি কি ইনপুট টাইপ নেবেন (কোন ID, কোন timestamps নয়) নাকি পূর্ণ entity? অনেক দল Create(ctx, in CreateX) পছন্দ করে যাতে কলাররা সার্ভার-নিয়ন্ত্রিত ফিল্ড সেট করতে না পারে।\n- Update: এটি পূর্ণ রিপ্লেস নাকি প্যাচ? প্যাচ হলে plain struct ব্যবহার করবেন না যেখানে শূন্য মান বিভ্রান্তিকর। পয়েন্টার, nullable টাইপ, বা explicit field mask ব্যবহার করুন।\n- Delete: হার্ড ডিলেট নাকি সফট ডিলেট? সফট ডিলেট হলে সিদ্ধান্ত নিন Get ডিফল্টভাবে ডিলেটেড সারি লুকাবে কি না।\n\nলিখিত মেথডগুলো কী রিটার্ন করবে তাও সিদ্ধান্ত নিন। কম-আশ্চর্যের অপশন হলো আপডেট হওয়া entity রিটার্ন করা (DB ডিফল্টগুলো সহ) অথবা শুধুই ID ও ErrNotFound ফেরত করা যখন কিছু বদলায়নি।\n\n## জেনেরিক ও এন্টিটি-নির্দিষ্ট অংশের টেস্টিং কৌশল\n\nএই পদ্ধতি তখনই মূল্য দেয় যখন বিশ্বাস করা সহজ। কোড যেমন ভাগ করেছেন, টেস্টও সেই লাইনেই ভাগ করুন: শেয়ার্ড হেল্পারগুলো একবার টেস্ট করুন, তারপর প্রতিটি এন্টিটির SQL ও scanning আলাদা করে টেস্ট করুন।\n\nশেয়ার্ড অংশগুলোকে যতটা সম্ভব ছোট pure ফাংশনে রাখুন — pagination validation, sort key-কে অনুমোদিত কলামে ম্যাপ করা, WHERE ফ্রাগমেন্ট বিল্ডিং ইত্যাদি। এগুলো দ্রুত ইউনিট টেস্ট দিয়ে কভার করা যায়।\n\nList কুয়েরিদের জন্য table-driven টেস্ট ভালো কাজ করে কারণ edge case-ই সমস্যা। খালি ফিল্টার, অজানা sort key, limit 0, অতিরিক্ত বেশি limit, negative offset, এবং “next page” সীমান্ত যেখানে আপনি এক্সট্রা রো ফেচ করেন — এসব কভার করুন।\n\nপ্রতি-এন্টিটির টেস্টগুলো এন্টিটি-নির্দিষ্ট কী তা ফোকাস রাখুক: আপনি কোন SQL আশা করেন এবং কিভাবে রো entity-তে স্ক্যান হয়। SQL mock বা হালকা টেস্ট ডাটাবেস ব্যবহার করে নিশ্চিত করুন scanner nulls, optional কলাম, ও টাইপ কনভারশন সামলায়।\n\nআপনার প্যাটার্ন যদি ট্রানজেকশন সাপোর্ট করে, commit/rollback আচরণ ছোট fake executor দিয়ে টেস্ট করুন যে কলগুলো রেকর্ড করে এবং এরর সিমুলেট করে: \n\n- Begin একটি tx-scoped executor রিটার্ন করে\n- ত্রুটিতে rollback ঠিক একবার কল হয়\n- সফল হলে commit ঠিক একবার কল হয়\n- commit ব্যর্থ হলে এরর অপরিবর্তিত ফেরত দেয়\n\nএছাড়াও ছোট “contract tests” যোগ করতে পারেন যা প্রতিটি রিপোজিটরিকে পাশ করতে হবে: create তারপর get একই ডেটা, update প্রত্যাশিত ফিল্ড বদলায়, delete করলে get ErrNotFound দেয়, এবং list একই ইনপুটে স্থিতিশীল ordering দেয়।\n\n## সাধারণ ভুল ও ফাঁদ\n\nজেনেরিকস দেখে অনেকেই একটা রিপোজিটরি বানিয়ে সবকিছুকে কন্ট্রোল করতে চাইবে। ডাটা অ্যাক্সেস ছোট ছোট ভিন্নতায় পরিপূর্ণ, এবং সেই ভিন্নতাগুলো গুরুত্বপূর্ণ।\n\nকিছু ফাঁদ প্রায়ই দেখা যায়:\n\n- অতিরিক্ত সাধারণকরণ যেখানে প্রতিটি মেথড বড় অপশন ব্যাগ নেয় (joins, search, permissions, soft deletes, caching)। তখন আপনি আরেকটি ORM বানিয়ে ফেলেছেন।\n- খুব বুদ্ধিমান কনস্ট্রেইন্ট। যদি পাঠকদের টাইপ সেট ডিকোড করতে হয় বুঝতে যে একটি এন্টিটি কী ইমপ্লিমেন্ট করতে হবে, তখন abstraction-টি খরচের বেশি হয়ে যায়।\n- ইনপুট টাইপকে DB মডেল হিসেবে মানা। যখন Create ও Update একই struct নেয় যা আপনি রো থেকে স্ক্যান করেন, DB ডিজাইন হ্যান্ডলার ও টেস্টে লিক করে এবং schema পরিবর্তন অ্যাপে ছড়িয়ে পড়ে।\n- List-এ নীরবে আচরণ: অস্থিতিশীল sorting, inconsistent defaults, বা paging নিয়ম যা এন্টিটি অনুযায়ী বদলে যায়।\n\nএকটি বাস্তব উদাহরণ: ListCustomers প্রতিবার ভিন্ন অর্ডারে রিটার্ন করে যদি রিপোজিটরিটি ORDER BY না দেয়। তখন pagination রেকর্ড ডুপ্লিকেট বা স্কিপ করে। ordering স্পষ্ট করুন (যদি প্রয়োজন হয় শুধুই প্রাইমারি কী দিয়ে) এবং ডিফল্টগুলো কনসিস্টেন্ট রাখুন।\n\n## গ্রহণ করার আগে দ্রুত চেকলিস্ট\n\nজেনেরিক রিপোজিটরি প্রতিটি প্যাকেজে রোল আউট করার আগে নিশ্চিত করুন এটা পুনরাবৃত্তি কমাবে কিন্তু গুরুত্বপূর্ণ ডাটাবেস আচরণ লুকাবে না।\n\nকনসিস্টেন্সি দিয়ে শুরু করুন। একটি রিপো context.Context নেয় আর অন্যটি না যদি এমন হয়, অথবা একটি (T, error) রিটার্ন করে আর অন্যটি (*T, error) — এসব অসামঞ্জস্য সার্ভিস, টেস্ট, ও মক-এ সমস্যা তৈরি করে।\n\nপ্রতিটি এন্টিটির জন্য গল্পে একটি স্পষ্ট বাড়ি রাখুন যেখানে SQL থাকে। জেনেরিক কোড ফ্লো reuse করুক (scan, validate, map errors), কিন্তু কোয়েরি স্ট্রিং টুকরো ছড়িয়ে না পড়ুক।\n\nএকটি দ্রুত চেকের সেট যা বেশিরভাগ বিস্ময় প্রতিরোধ করে:\n\n- List/Get/Create/Update/Delete-এর জন্য একটি সিগনেচার কনভেনশন\n- প্রতিটি রিপোতে ব্যবহৃত এক প্রত্যাশিত not-found নিয়ম\n- স্থিতিশীল list ordering যা ডকুমেন্টেড ও টেস্ট করা আছে\n- *sql.DB ও *sql.Tx একই কোডে চালানোর পরিষ্কার উপায় (executor ইন্টারফেস)\n- জেনেরিক কোড ও এন্টিটি নিয়মের মধ্যে পরিষ্কার সীমানা (ভ্যালিডেশন ও বিজনেস চেক জেনেরিক স্তরের বাইরে রাখুন)\n\nআপনি যদি AppMaster-এ দ্রুতই অভ্যন্তরীণ টুল বানাচ্ছেন এবং পরে জেনারেটেড Go কোড এক্সপোর্ট বা বৃদ্ধি করছেন, এই চেকগুলো ডেটা লেয়ারকে পূর্বানুমানযোগ্য ও টেস্ট করা সহজ রাখতে সাহায্য করে।\n\n## বাস্তব উদাহরণ: একটি Customer রিপোজিটরি তৈরি করা\n\nনিচে একটি ছোট Customer রিপোজিটরি রূপ যা টাইপ-সেফ রেখে বেশ সরল।\n\nস্টোরড মডেল দিয়ে শুরু করুন। ID শক্ত টাইপ করা রাখুন যাতে ভুল ID মিশে না যায়:\n\ngo\ntype CustomerID int64\n\ntype Customer struct {\n\tID CustomerID\n\tName string\n\tStatus string // "active", "blocked", "trial"...\n}\n\n\nএখন “API কি নেয়” এবং “আপনি কি স্টোর করেন” আলাদা করুন। Create ও Update এখানে আলাদা হওয়া উচিত।\n\ngo\ntype CreateCustomerInput struct {\n\tName string\n\tStatus string\n}\n\ntype UpdateCustomerInput struct {\n\tName *string\n\tStatus *string\n}\n\n\nআপনার জেনেরিক বেস শেয়ার্ড ফ্লো (কোয়েরি চালানো, স্ক্যান, এরর ম্যাপিং) হ্যান্ডল করবে, আর Customer repo Customer-নির্দিষ্ট SQL ও ম্যাপিং দেখবে। সার্ভিস লেয়ারের দৃষ্টিতে ইন্টারফেস পরিষ্কার থাকবে:\n\ngo\ntype CustomerRepo interface {\n\tCreate(ctx context.Context, in CreateCustomerInput) (Customer, error)\n\tUpdate(ctx context.Context, id CustomerID, in UpdateCustomerInput) (Customer, error)\n\tGet(ctx context.Context, id CustomerID) (Customer, error)\n\tDelete(ctx context.Context, id CustomerID) error\n\tList(ctx context.Context, q CustomerListQuery) ([]Customer, int, error)\n}\n\n\nList-এর জন্য, ফিল্টার ও pagination কে ফার্স্ট-ক্লাস অনুরোধ অবজেক্ট হিসেবে বিবেচনা করুন। এটি কল সাইটগুলোকে পড়তে সহজ রাখে এবং লিমিট ভুলে যাওয়া কঠিন করে দেয়।\n\ngo\ntype CustomerListQuery struct {\n\tStatus *string // filter\n\tSearch *string // name contains\n\tLimit int\n\tOffset int\n}\n\n\nএখান থেকে প্যাটার্নটি ভালোভাবে স্কেল করে: পরের এন্টিটির জন্য একই স্ট্রাকচার অনুলিপি করুন, ইনপুট স্টোরড মডেল থেকে আলাদা রাখুন, এবং স্ক্যানিং স্পষ্ট রাখুন যাতে পরিবর্তনগুলো স্বচ্ছ ও কম্পাইলার-সহায়িত থাকে।
প্রশ্নোত্তর
জেনেরিকস ব্যবহার করে আপনি কেবল প্রবাহটিই পুনঃব্যবহার করবেন (কোয়েরি চালানো, রো লুপ, not-found হ্যান্ডলিং, pagination ডিফল্ট, এরর ম্যাপিং) — কিন্তু প্রতিটি এন্টিটির SQL ও রো ম্যাপিং স্পষ্টভাবে আলাদা রাখবেন। এতে পুনরাবৃত্তি কমবে কিন্তু ডেটা লেয়ার runtime-এ অদৃশ্য “ম্যাজিক” হয়ে যাবে না।
রিফ্লেকশন ম্যাপিং নিয়মগুলো লুকিয়ে দেয় এবং ব্যর্থতা runtime-এ চলে যায়। কম্পাইলার চেক চলে যায়, IDE সহায়তা কমে যায়, আর ছোট schema পরিবর্তনগুলো আচমকা সমস্যার কারণ হয়ে ওঠে। জেনেরিকসের সঙ্গে স্পষ্ট scanner ফাংশন রাখলে টাইপ সেফটি বজায় থাকে এবং একই সময়ে 반복 অংশগুলো শেয়ার করা যায়।
সাধারণত comparable ভাল ডিফল্ট কারণ ID-গুলো তুলনা করা হয়, map-এর কী হিসেবে ব্যবহার হয় এবং বারবার পাস করা হয়। যদি আপনার সিস্টেমে বিভিন্ন ID স্টাইল থাকে (যেমন int64 ও UUID string), তাহলে ID টাইপ জেনেরিক রাখা ভালো যাতে একটি সিদ্ধান্ত সব রেপোতে জোর না করা হয়।
রিপোজিটরি কেবল সেই জিনিসগুলোই দাবি করবে যা শেয়ার্ড CRUD ফ্লো-কে দরকার, সাধারণত GetID() ও SetID()। struct embedding বা জটিল type-set ট্রিকস ব্যবহার করে ফিল্ড বাধ্য করা এড়িয়ে চলুন — তা আপনার ডোমেইন টাইপগুলোকে রিপোজিটরি প্যাটার্নে বেঁধে দেবে।
একটি ছোট executor ইন্টারফেস (যেমন DBTX) ব্যবহার করুন যাতে শুধুমাত্র আপনার ডাকা মেথডগুলো থাকে, যেমন QueryContext, QueryRowContext, ExecContext। তখন *sql.DB বা *sql.Tx যে কোনোটি executor হিসেবে পাস করা যাবে এবং রিপোজিটরি কোডে শাখা-বিভাগ যোগ করতে হবে না।
Get(ctx, id) টাইপেড এন্টিটি এবং স্পষ্ট মিসিং-রেকর্ড সংকেত ফেরত দেয় — সাধারণত শেয়ার্ড sentinel এরর যেমন ErrNotFound। শূন্য-মূল্য এন্টিটি ও nil এরর ফেরত দিলে কলাররা বুঝতে পারে না যে রেকর্ডটি নেই নাকি শুধু ফিল্ডগুলো খালি।
ইনপুটগুলোকে স্টোর করা মডেল থেকে আলাদা রাখুন। প্রায়শই Create(ctx, CreateInput) ও Update(ctx, id, UpdateInput) বেশি নিরাপদ — এতে কলাররা সার্ভার-নিয়ন্ত্রিত ফিল্ড (ID, timestamps) নিজে সেট করতে পারে না। প্যাচ-স্টাইল আপডেট হলে পয়েন্টার বা nullable টাইপ ব্যবহার করুন যাতে “অসেট” ও “শূন্যতে সেট” আলাদা করা যায়।
প্রতিবার স্পষ্ট, স্থিতিশীল ORDER BY নির্ধারণ করুন, সম্ভব হলে ইউনিক কলাম (প্রাইমারি কী) দিয়ে। না হলে নতুন রেকর্ড এলে বা প্ল্যানার বদলালে pagination-এ আইটেমগুলো লাফিয়ে পড়বে বা কপি/স্কিপ হবে।
রিপোজিটরি থেকে সার্ভিসগুলোকে যে এররগুলোতে শাখা করা উচিত তা সীমিত রাখুন, যেমন ErrNotFound ও ErrConflict। বাকি সবকিছু low-level এরর হিসেবে context-সহ wrap করুন। কলাররা string parse করে সিদ্ধান্ত নেবে না — errors.Is ব্যবহার করে শাখা করা যাবে।
শেয়ার্ড হেল্পারগুলো একবার ইউনিট টেস্ট করুন (pagination normalization, not-found mapping, affected-row checks) এবং প্রতিটি এন্টিটির SQL ও scanning আলাদা করে টেস্ট করুন। ছোট contract tests রাখুন: create→get মিলছে, update প্রত্যাশিত ফিল্ড বদলায়, delete পরে ErrNotFound আসে, list ordering স্থিতিশীল ইত্যাদি।


