साफ़ Go डेटा‑लेयर के लिए Go generics CRUD रिपॉजिटरी पैटर्न
Go में एक व्यावहारिक generics CRUD रिपॉजिटरी पैटर्न सीखें जो List/Get/Create/Update/Delete लॉजिक को दोबारा उपयोग करता है — पढ़ने में स्पष्ट constraints के साथ, बिना रिफ्लेक्शन के, और साफ़ कोड।

क्यों Go में CRUD रिपॉजिटरी गड़बड़ हो जाते हैं
CRUD रिपॉजिटरी शुरुआत में सरल होते हैं। आप GetUser लिखते हैं, फिर ListUsers, फिर वही Orders के लिए, फिर Invoices के लिए। कुछ एंटिटीज़ के बाद, डेटा लेयर लगभग-नकलों का ढेर बन जाता है जहाँ छोटे अंतर आसानी से छूट जाते हैं।
सबसे ज़्यादा जो दोहराता है वह असल SQL नहीं है। अक्सर यह आस-पास का फ्लो होता है: क्वेरी चलाना, पंक्तियों को स्कैन करना, “नहीं मिला” हैंडल करना, डेटाबेस त्रुटियों का मैप करना, पेजिनेशन डिफ़ॉल्ट लागू करना, और इनपुट्स को सही प्रकारों में बदलना।
आम hotspots परिचित हैं: डुप्लिकेट Scan कोड, context.Context और ट्रांज़ेक्शन पैटर्न की नकल, boilerplate LIMIT/OFFSET हैंडलिंग (कभी-कभी कुल गिनती के साथ), वही “0 rows means not found” चेक, और कॉपी-पेस्टेड INSERT ... RETURNING id वैरिएशन।
जब दोहराव काफी दर्द देता है, कई टीमें रिफ्लेक्शन की ओर झुकती हैं। यह "एक बार लिखो" CRUD का वादा करता है: किसी भी struct को लेकर रUNTIME पर कॉलम से भर दो। लागत बाद में दिखती है। रिफ्लेक्शन-भरा कोड पढ़ने में कठिन होता है, IDE सपोर्ट घटता है, और फेल्योर कंपाइल टाइम से रनटाइम पर चले जाते हैं। एक छोटा बदलाव—फील्ड का नाम बदलना या nullable कॉलम जोड़ना—ऐसे आश्चर्य बन जाते हैं जो केवल टेस्ट या प्रोडक्शन में दिखते हैं।
टाइप-सुरक्षित पुन:उपयोग का मतलब है CRUD फ्लो साझा करना बिना Go की रोज़मर्रा की सुविधाओं को छोड़े: स्पष्ट सिग्नेचर, कंपाइलर-चेक्ड प्रकार, और ऑटोकम्प्लीट जो सच में मदद करे। जनेरिक्स के साथ, आप Get[T] और List[T] जैसे ऑपरेशन्स री-यूज़ कर सकते हैं और फिर भी हर एंटिटी से उन हिस्सों की मांग कर सकते हैं जो अनुमानित नहीं हो सकते—जैसे किसी रो को T में स्कैन करने का तरीका।
यह पैटर्न जानबूझकर डेटा एक्सेस लेयर के बारे में है। यह SQL और मैपिंग को सुसंगत और उबाऊ रखता है। यह आपका डोमेन मॉडल करने, बिजनेस नियम लागू करने, या सर्विस-लेवल लॉजिक बदलने की कोशिश नहीं करता।
डिज़ाइन लक्ष्य (और जिन्हें यह हल नहीं करेगा)
एक अच्छा रिपॉजिटरी पैटर्न रोज़मर्रा के डेटाबेस एक्सेस को Predictable बनाता है। आपको रिपॉजिटरी पढ़कर जल्दी समझ आना चाहिए कि यह क्या करता है, कौन-सा SQL चलाता है, और कौन-सी त्रुटियाँ लौट सकता है।
लक्ष्य सरल हैं:
- एंड-टू-एंड टाइप सुरक्षा (IDs, entities, और results
anyन हों) - constraints जो इरादा स्पष्ट करें बिना टाइप जटिलताओं के
- महत्वपूर्ण व्यवहार छिपाए बिना कम boilerplate
- List/Get/Create/Update/Delete में सुसंगत व्यवहार
नॉन-लक्ष्य भी उतने ही महत्वपूर्ण हैं। यह ORM नहीं है। इसे फ़ील्ड मैपिंग अनुमानित नहीं करनी चाहिए, टेबल्स को ऑटो-जॉइन नहीं करना चाहिए, या क्वेरीज को चुपचाप बदलना नहीं चाहिए। “मैजिक मैपिंग” आपको फिर से रिफ्लेक्शन, टैग्स, और edge-cases में खींच ले जाएगी।
साधारण SQL वर्कफ़्लो मानें: स्पष्ट SQL (या पतला query builder), स्पष्ट ट्रांज़ेक्शन बॉउंडरीज़, और त्रुटियाँ जिनके बारे में आप सोच सकते हैं। जब कुछ फेल हो, एरर को बताना चाहिए "not found", "conflict/constraint violation", या "DB unavailable" — न कि vague "repository error"।
कुंजी निर्णय यह है कि क्या generic होगा और क्या per-entity रहेगा।
- Generic: फ्लो (क्वेरी चलाना, स्कैन, टाइप किए हुए मान लौटाना, सामान्य त्रुटियों का अनुवाद)।
- Per entity: अर्थ (table नाम, चुने गए कॉलम, joins, और SQL स्ट्रिंग्स)।
सभी एंटिटीज़ को एक यूनिवर्सल फिल्टर सिस्टम में ज़बरदस्ती डालना आम तौर पर तब कोड को पढ़ने में कठिन बना देता है जब कि दो स्पष्ट क्वेरीज लिखना आसान होता।
एंटिटी और ID constraints चुनना
अधिकांश CRUD कोड इसलिए दोहराता है क्योंकि हर टेबल के पास वही बुनियादी कदम होते हैं, लेकिन हर एंटिटी के अपने फील्ड्स होते हैं। जनेरिक्स के साथ, चाल यह है कि एक छोटा आकार साझा करें और बाकी सब खुला रखें।
शुरूआत में तय करें कि रिपॉजिटरी को वास्तव में एंटिटी के बारे में क्या जानना चाहिए। कई टीमों के लिए, एकमात्र सार्वभौमिक टुकड़ा ID ही होता है। टाइमस्टैम्प उपयोगी हो सकते हैं, लेकिन वे सार्वभौमिक नहीं हैं, और हर प्रकार में उन्हें ज़बरदस्ती शामिल करना मॉडल को नकली बना सकता है।
एक ID प्रकार चुनें जो आप स्वीकार कर सकें
आपका ID प्रकार डेटाबेस में पंक्तियों की पहचान के तरीके से मेल खाना चाहिए। कुछ प्रोजेक्ट int64 उपयोग करते हैं, कुछ UUID स्ट्रिंग्स। यदि आप एक ऐसा तरीका चाहते हैं जो सेवाओं में काम करे, तो ID को generic बनाएं। यदि आपके पूरे कोडबेस में एक ID प्रकार प्रयोग हो रहा है, तो उसे फिक्स रखकर सिग्नेचर्स छोटा हो सकता है।
एक अच्छा डिफ़ॉल्ट constraint comparable है, क्योंकि आप IDs की तुलना करेंगे, उन्हें map कीज़ के रूप में उपयोग करेंगे, और उन्हें पास करेंगे।
type ID interface {
comparable
}
type Entity[IDT ID] interface {
GetID() IDT
SetID(IDT)
}
एंटिटी constraints को न्यूनतम रखें
struct embedding या प्रकार-सेट ट्रिक्स जैसे ~struct{...} के ज़रिए फील्ड्स की मांग करने से बचें। ये शक्तिशाली दिखते हैं, पर वे आपके डोमेन प्रकारों को रिपॉजिटरी पैटर्न से जोड़ देते हैं।
इसके बजाय केवल वही मांगें जो साझा CRUD फ्लो को चाहिए:
- ID प्राप्त और सेट करना (ताकि Create ID लौटा सके, और Update/Delete उसे लक्षित कर सकें)
यदि आप बाद में soft deletes या optimistic locking जैसी सुविधाएँ जोड़ते हैं, तो छोटे opt-in इंटरफेसेस जोड़ें (उदाहरण के लिए GetVersion/SetVersion) और उन्हें केवल जहाँ जरूरी हो वहाँ उपयोग करें। छोटे इंटरफेस अक्सर अच्छे से उम्रदराज़ होते हैं।
एक generic रिपॉजिटरी इंटरफ़ेस जो पढ़ने में साफ़ रहे
रिपॉजिटरी इंटरफ़ेस को उस चीज़ का वर्णन करना चाहिए जो आपकी ऐप् को चाहिए, न कि जो डेटाबेस कर रहा है। यदि इंटरफ़ेस SQL जैसा लगे, तो वह विवरण फैल जाता है।
मेथड सेट को छोटा और Predictable रखें। context.Context को पहले रखें, फिर प्राथमिक इनपुट (ID या data), फिर वैकल्पिक नॉब्स को एक struct में बंडल करें।
type Repository[T any, ID comparable, CreateIn any, UpdateIn any, ListQ any] interface {
Get(ctx context.Context, id ID) (T, error)
List(ctx context.Context, q ListQ) ([]T, error)
Create(ctx context.Context, in CreateIn) (T, error)
Update(ctx context.Context, id ID, in UpdateIn) (T, error)
Delete(ctx context.Context, id ID) error
}
List के लिए एक यूनिवर्सल फिल्टर प्रकार थोपने से बचें। फिल्टर वहां सबसे ज़्यादा अलग होते हैं। एक व्यावहारिक तरीका है प्रति-एंटिटी क्वेरी प्रकार और एक छोटा साझा पेजिनेशन आकार जिसे आप embed कर सकें।
type Page struct {
Limit int
Offset int
}
एरर हैंडलिंग वहां होती है जहाँ रिपॉजिटरी अक्सर शोर करते हैं। पहले से तय करें कि कॉलर्स किन त्रुटियों पर शाखा बना सकते हैं। एक सरल सेट आमतौर पर काम करता है:
ErrNotFoundजब कोई ID मौजूद न होErrConflictयूनिक उल्लंघनों या version संघर्षों के लिएErrValidationजब इनपुट अमान्य हो (केवल अगर रिपॉजिटरी वैलिडेट करे)
बाकी सब कुछ low-level एरर (DB/network) के साथ wrap किया जा सकता है। इस कॉन्ट्रैक्ट के साथ, सर्विस कोड "not found" या "conflict" को हैंडल कर सकता है बिना यह जाने कि स्टोरेज आज PostgreSQL है या कुछ और बाद में।
रिफ्लेक्शन से बचते हुए फिर भी फ्लो को री-यूज़ करना
रिफ्लेक्शन आम तौर पर तब छिपता है जब आप चाहते हैं कि एक कोड का टुकड़ा "किसी भी struct को भर दे"। वह एरर को रनटाइम तक छिपा देता है और नियम अस्पष्ट कर देता है।
एक साफ़ तरीका यह है कि आप केवल उबाऊ हिस्सों को साझा करें: क्वेरीज चलाना, रोस पर लूप करना, प्रभावित पंक्तियों की गिनती चेक करना, और एरर को सुसंगत रूप से wrap करना। structs में मैपिंग को स्पष्ट रखें।
ज़िम्मेदारियों को विभाजित करें: SQL, मैपिंग, साझा फ्लो
एक व्यावहारिक विभाजन ऐसा दिखता है:
- प्रति एंटिटी: SQL स्ट्रिंग्स और पैरामीटर ऑर्डर एक जगह रखें
- प्रति एंटिटी: छोटे मैपिंग फ़ंक्शन लिखें जो रो को concrete struct में स्कैन करें
- Generic: साझा फ्लो प्रदान करें जो क्वेरी.execute करे और mapper को कॉल करे
इस तरह, जनेरिक्स repetition घटाते हैं बिना यह छिपाए कि डेटाबेस क्या कर रहा है।
यहाँ एक छोटा abstraction है जो आपको *sql.DB या *sql.Tx में से किसी को भी पास करने देता है बिना बाकी कोड को परवाह करने के:
type DBTX interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}
जनेरिक्स क्या करें (और क्या नहीं)
Generic लेयर को आपके struct "समझने" की कोशिश नहीं करनी चाहिए। इसके बजाय, यह स्पष्ट फ़ंक्शंस स्वीकार करे जो आप प्रदान करें, जैसे:
- एक binder जो इनपुट को क्वेरी आर्ग्स में बदलता है
- एक scanner जो कॉलम्स को एंटिटी में पढ़ता है
उदाहरण के लिए, एक Customer रिपॉजिटरी SQL को constants के रूप में रख सकती है (selectByID, insert, update) और scanCustomer(rows) एक बार लागू कर सकती है। एक generic List लूप, context, और एरर wrapping संभाल सकता है, जबकि scanCustomer मैपिंग को टाइप-सेफ़ और स्पष्ट रखता है।
यदि आप एक कॉलम जोड़ते हैं, SQL और scanner दोनों अपडेट करें। कंपाइलर आपको बताएगा कि क्या टूट गया।
कदम दर कदम: पैटर्न लागू करना
लक्ष्य है List/Get/Create/Update/Delete के लिए एक पुन:उपयोगी फ्लो while हर रिपॉजिटरी को उसके SQL और रो मैपिंग के प्रति ईमानदार रखना।
1) कोर प्रकार परिभाषित करें
सबसे कम constraints के साथ शुरू करें। अपने कोडबेस के लिए एक ID प्रकार चुनें और एक रिपॉजिटरी इंटरफ़ेस बनाएं जो Predictable रहे।
type ID interface{ ~int64 | ~string }
type Repo[E any, K ID] interface {
Get(ctx context.Context, id K) (E, error)
List(ctx context.Context, limit, offset int) ([]E, error)
Create(ctx context.Context, e *E) error
Update(ctx context.Context, e *E) error
Delete(ctx context.Context, id K) error
}
2) DB और ट्रांज़ेक्शंस के लिए एक executor जोड़ें
Generic कोड को सीधे *sql.DB या *sql.Tx से न बाँधें। छोटे executor इंटरफेस पर निर्भर रहें जो वही मेथड्स रखता है जिन्हें आप कॉल करते हैं (QueryContext, ExecContext, QueryRowContext)। फिर सर्विसेज़ DB या ट्रांज़ेक्शन पास कर सकती हैं बिना रिपॉजिटरी को बदले।
3) साझा फ्लो के साथ एक generic बेस बनाएं
एक baseRepo[E,K] बनाएं जो executor और कुछ फ़ंक्शन फ़ील्ड्स रखता है। बेस उबाऊ हिस्से संभालेगा: क्वेरी कॉल करना, “नहीं मिला” मैप करना, प्रभावित पंक्तियों की जाँच, और सुसंगत एरर लौटाना।
4) एंटिटी-विशिष्ट भाग लागू करें
प्रत्येक एंटिटी रिपॉजिटरी वही देती है जो generic नहीं हो सकती:
- list/get/create/update/delete के लिए SQL
- एक
scan(row)फ़ंक्शन जो रो कोEमें बदलता है - एक
bind(...)फ़ंक्शन जो क्वेरी आर्ग्स लौटाता है
5) कंक्रीट रिपॉजिटरीज़ को वायर करें और सर्विस से इस्तेमाल करें
NewCustomerRepo(exec Executor) *CustomerRepo बनाएं जो baseRepo को embed या wrap करे। आपकी सर्विस लेयर Repo[E,K] इंटरफ़ेस पर निर्भर करे और निर्णय ले कि ट्रांज़ेक्शन कब शुरू करना है; रिपॉजिटरी बस उसे दिया गया executor उपयोग करे।
List/Get/Create/Update/Delete को आश्चर्य के बिना हैंडल करना
Generic रिपॉजिटरी तभी मददगार होती है जब हर मेथड हर जगह एक जैसा व्यवहार करे। ज़्यादातर दर्द छोटे असंगतियों से आता है: एक रिपॉजिटरी created_at के हिसाब से order करती है, दूसरी id के हिसाब से; एक missing के लिए nil, nil लौटाती है और दूसरी error।
List: पेजिनेशन और ordering जो shift न करे
एक पेजिनेशन स्टाइल चुनें और इसे सुसंगत रूप से लागू करें। Offset पेजिनेशन (limit/offset) सरल है और admin स्क्रीन के लिए ठीक बैठता है। Cursor पेजिनेशन लगातार स्क्रॉलिंग के लिए बेहतर है, पर उसे स्थिर sort key चाहिए।
जो कुछ भी चुनें, ordering स्पष्ट और स्थिर रखें। यूनिक कॉलम (अक्सर primary key) पर order करने से नए रो आने पर आइटम्स पेजों के बीच कूदेंगे नहीं।
Get: एक स्पष्ट “नहीं मिला” सिग्नल
Get(ctx, id) को टाइप्ड एंटिटी और एक स्पष्ट missing-record सिग्नल लौटाना चाहिए, आमतौर पर ErrNotFound जैसा साझा sentinel। zero-value एंटिटी के साथ nil error वापस करने से बचें। कॉलर यह नहीं बता पाएंगे कि रिकॉर्ड गायब है या फ़ील्ड खाली हैं।
टाइप डेटा के लिए है, एरर स्थिति के लिए।
ऑपरेशन लागू करने से पहले कुछ निर्णय लें और उन्हें सुसंगत रखें:
Create: क्या आप इनपुट प्रकार स्वीकार करते हैं (बिना ID, बिना timestamps) या पूरा एंटिटी? कई टीमेंCreate(ctx, in CreateX)पसंद करती हैं ताकि कॉलर सर्वर-नियंत्रित फील्ड सेट न कर सकें।Update: क्या यह पूरा रिप्लेस है या पैच? अगर पैच है, तो plain structs का उपयोग न करें जहाँ zero values ambiguous हों। pointers, nullable types, या explicit field mask का उपयोग करें।Delete: hard delete या soft delete? अगर soft delete है, तो तय करें कि क्याGetडिलीटेड रो को डिफ़ॉल्ट रूप से छुपाता है।
लिखने वाले मेथड्स क्या लौटाएंगे यह भी तय करें। कम-आश्चर्य विकल्प हैं अपडेटेड एंटिटी लौटाना (DB defaults के बाद) या केवल ID लौटाना और ErrNotFound जब कुछ बदला ही नहीं।
जनरिक और एंटिटी-विशिष्ट हिस्सों के लिए टेस्टिंग रणनीति
यह दृष्टिकोण तभी काम करता है जब उसे भरोसा करना आसान हो। कोड की तरह ही टेस्ट्स को भी विभाजित करें: साझा helpers एक बार टेस्ट करें, फिर हर एंटिटी की SQL और scanning अलग से।
साझा हिस्सों को जहाँ संभव हो छोटे pure फ़ंक्शंस मानें, जैसे पेजिनेशन वैलिडेशन, sort keys को allowed कॉलम में मैप करना, या WHERE फ्रैगमेंट बनाना। इनको तेज़ यूनिट टेस्ट से कवर करें।
लिस्ट क्वेरीज के लिए table-driven टेस्ट काम करते हैं क्योंकि edge-cases पूरी समस्या होते हैं। खाली फिल्टर, unknown sort keys, limit 0, limit अधिकतम से अधिक, negative offset, और "next page" सीमाएँ जैसे केस कवर करें जहाँ आप एक अतिरिक्त रो पकड़ते हैं।
प्रति-एंटिटी टेस्ट वास्तव में एंटिटी-विशिष्ट चीज़ों पर फोकस रखें: आप किस SQL की उम्मीद करते हैं और पंक्तियाँ एंटिटी टाइप में कैसे स्कैन होती हैं। SQL mock या हल्का टेस्ट डेटाबेस उपयोग करें और सुनिश्चित करें कि scan लॉजिक nulls, optional कॉलम, और टाइप कनवर्ज़न संभाले।
यदि आपका पैटर्न ट्रांज़ेक्शंस सपोर्ट करता है, तो कमिट/रोलबैक व्यवहार एक छोटे fake executor के साथ टेस्ट करें जो कॉल्स रिकॉर्ड करे और त्रुटियाँ सिम्युलेट करे:
- Begin एक tx-scoped executor लौटाता है
- त्रुटि पर, rollback ठीक एक बार कॉल होता है
- सफलता पर, commit ठीक एक बार कॉल होता है
- अगर commit फेल होता है, त्रुटि अपरिवर्तित लौटती है
आप छोटे "contract tests" भी जोड़ सकते हैं जो हर रिपॉजिटरी पास करे: create फिर get वही डाटा लौटाए, update इच्छित फील्ड बदल दे, delete के बाद get ErrNotFound लौटाए, और list एक ही इनपुट के तहत स्थिर ordering लौटाए।
सामान्य गलतियाँ और जाल
जनेरिक्स से यह प्रलोभन आता है कि आप एक ही रिपॉजिटरी सबके लिए बना दें। डेटा एक्सेस छोटे अंतर से भरा होता है, और वे अंतर मायने रखते हैं।
कुछ ट्रैप्स अक्सर दिखते हैं:
- ओवर-जनरलाइज़ करना जब हर मेथड giant विकल्प बैग लेता है (joins, search, permissions, soft deletes, caching)। तब आप एक दूसरा ORM बना लेते हैं।
- बहुत चालाक constraints। अगर पाठकों को यह समझने के लिए type sets decode करनी पड़ती है कि एंटिटी को क्या implement करना चाहिए, तो abstraction की लागत फायदा से ज़्यादा होती है।
- इनपुट प्रकारों को DB मॉडल मानना। जब Create और Update वही struct लेते हैं जिसे आप पंक्तियों से स्कैन करते हैं, DB विवरण हैंडलर्स और टेस्ट्स में लीक हो जाते हैं, और स्कीमा बदलते ही ऐप में सभी जगह असर दिखता है।
Listमें चुपचाप व्यवहार: अस्थिर sorting, inconsistent defaults, या पेजिंग नियम जो एंटिटी के अनुसार बदलते हैं।- Not-found हैंडलिंग जो कॉलर्स को error strings पार्स करने पर मजबूर करे बजाय
errors.Isका प्रयोग करने के।
एक ठोस उदाहरण: ListCustomers हर बार अलग क्रम में लौटाता है क्योंकि रिपॉजिटरी ने ORDER BY सेट नहीं किया। पेजिनेशन तब रिकॉर्ड्स को डुप्लीकेट या छोड़ देता है। ordering स्पष्ट रखें (यहां तक कि यदि यह सिर्फ primary key द्वारा हो) और डिफ़ॉल्ट्स सुसंगत रखें।
अपनाने से पहले त्वरित चेकलिस्ट
Generic रिपॉजिटरी को हर पैकेज में रोल करने से पहले सुनिश्चित करें कि यह दोहराव कम करे बिना महत्वपूर्ण डेटाबेस व्यवहार छिपाए।
संगति से शुरू करें। अगर एक रिपॉजिटरी context.Context लेती है और दूसरी नहीं, या एक (T, error) लौटाती है जबकि दूसरी (*T, error), तो दर्द सर्विसेज़, टेस्ट्स, और मॉक में दिखेगा।
प्रत्येक एंटिटी के लिए अभी भी उसके SQL के लिए एक स्पष्ट घर होना चाहिए। Generics फ्लो (scan, validate, map errors) को री-यूज़ करें, न कि queries को टुकड़ों में बिखेर दें।
कुछ चेक जो अधिकांश आश्चर्य रोकते हैं:
- List/Get/Create/Update/Delete के लिए एक सिग्नेचर कन्वेंशन
- हर रिपॉजिटरी द्वारा उपयोग किया जाने वाला एक predictable not-found नियम
- दस्तावेज़ीकृत और टेस्ट की गई स्थिर list ordering
*sql.DBऔर*sql.Txपर एक जैसा कोड चलाने का साफ़ तरीका (executor इंटरफेस के माध्यम से)- generic कोड और एंटिटी नियमों के बीच स्पष्ट सीमा (वैधता और बिजनेस चेक generic लेयर के बाहर रहें)
यदि आप AppMaster में जल्दी internal tools बना रहे हैं और बाद में जेनरेटेड Go कोड को export या extend कर रहे हैं, तो ये चेक डेटा लेयर को Predictable और टेस्ट करने में आसान रखने में मदद करते हैं।
एक यथार्थवादी उदाहरण: Customer रिपॉजिटरी बनाना
यहाँ एक छोटा Customer रिपॉजिटरी आकार है जो टाइप-सेफ़ रहता है बिना बहुत चालाक हुए।
स्टोर किया गया मॉडल से शुरू करें। ID को strongly typed रखें ताकि आप गलती से अन्य IDs के साथ न मिला सकें:
type CustomerID int64
type Customer struct {
ID CustomerID
Name string
Status string // "active", "blocked", "trial"...
}
अब "API क्या स्वीकार करता है" और "आप क्या स्टोर करते हैं" को अलग रखें। यही जगह है जहाँ Create और Update अलग होने चाहिए।
type CreateCustomerInput struct {
Name string
Status string
}
type UpdateCustomerInput struct {
Name *string
Status *string
}
आपका generic बेस साझा फ्लो संभाल सकता है (SQL चलाना, स्कैन करना, एरर मैप करना), जबकि Customer repo Customer-विशिष्ट SQL और मैपिंग रखेगा। सर्विस लेयर के दृष्टिकोण से, इंटरफ़ेस साफ़ रहता है:
type CustomerRepo interface {
Create(ctx context.Context, in CreateCustomerInput) (Customer, error)
Update(ctx context.Context, id CustomerID, in UpdateCustomerInput) (Customer, error)
Get(ctx context.Context, id CustomerID) (Customer, error)
Delete(ctx context.Context, id CustomerID) error
List(ctx context.Context, q CustomerListQuery) ([]Customer, int, error)
}
List के लिए, फ़िल्टर और पेजिनेशन को प्रथम-श्रेणी अनुरोध ऑब्जेक्ट के रूप में ट्रीट करें। इससे कॉल साइट्स पठनीय रहते हैं और limits भूलना मुश्किल होता है।
type CustomerListQuery struct {
Status *string // filter
Search *string // name contains
Limit int
Offset int
}
यहीं से पैटर्न अच्छी तरह स्केल करता है: अगली एंटिटी के लिए संरचना कॉपी करें, इनपुट्स को स्टोर किए गए मॉडलों से अलग रखें, और स्कैनिंग को स्पष्ट रखें ताकि बदलाव स्पष्ट और कंपाइलर-अनुकूल रहें।
सामान्य प्रश्न
जनेरिक्स का उपयोग केवल फ्लो को दोबारा उपयोग करने के लिए करें (क्वेरी, रो-लूप, "नहीं मिला" हैंडलिंग, पेजिनेशन डिफ़ॉल्ट, एरर मैपिंग), लेकिन SQL और रो मैपिंग को प्रत्येक एंटिटी के लिए स्पष्ट रखें। इससे आप रिपीट होने वाली कोडिंग कम करेंगे बिना डेटा लेयर को रनटाइम 'मैजिक' में बदल दिए।
रिफ्लेक्शन मैपिंग के नियम छिपा देता है और फेलियर रनटाइम पर ले जाता है। आप कंपाइल-टाइम चेक्स खो देते हैं, IDE सहायता कमजोर हो जाती है, और छोटी स्कीमा बदलती हुई चीज़ें आश्चर्य बन जाती हैं। जनेरिक्स के साथ स्पष्ट scanner फ़ंक्शन रखकर आप प्रकार-सुरक्षा बनाए रख सकते हैं और फिर भी बार-बार आने वाले हिस्सों को साझा कर सकते हैं।
एक अच्छा डिफ़ॉल्ट comparable है, क्योंकि ID की तुलना की जाती है, उन्हें map की चाबियों के रूप में उपयोग किया जाता है, और वे हर जगह पास होते हैं। यदि आपके सिस्टम में कई ID शैलियाँ हैं (जैसे int64 और UUID स्ट्रिंग), तो ID को generic बनाना सभी रिपॉजिटरीज़ पर एक चुनाव थोपने से बचाता है।
इसे न्यूनतम रखें: आम तौर पर केवल वही जो साझा CRUD फ्लो को चाहिए, जैसे GetID() और SetID(). embedding या जटिल type-sets के ज़रिए फील्ड्स को ज़बरदस्ती शामिल न करें, क्योंकि इससे आपके डोमेन प्रकार रिपॉजिटरी पैटर्न के साथ जुड़ जाते हैं और refactor मुश्किल हो जाता है।
छोटा DBTX जैसा executor इंटरफेस बनाएं जिसमें केवल वे ही मेथड हों जो आप कॉल करते हैं, जैसे QueryContext, QueryRowContext, और ExecContext. फिर आपका रिपॉजिटरी कोड *sql.DB या *sql.Tx दोनों पर बिना डुप्लिकेट किए चल सकता है।
Get(ctx, id) एक टाइप्ड एंटिटी और स्पष्ट "नहीं मिला" संकेत लौटाए। सामान्य तौर पर एक साझा sentinel error जैसे ErrNotFound का उपयोग करें। न कि zero-value एंटिटी और nil error — इससे कॉलर यह नहीं बता पाएगा कि रिकॉर्ड गायब है या सिर्फ़ फील्ड खाली हैं।
इनपुट को स्टोर्ड मॉडल से अलग रखें। Create(ctx, CreateInput) और Update(ctx, id, UpdateInput) पसंद करें ताकि कॉलर सर्वर-नियंत्रित फील्ड जैसे ID या timestamps सेट न कर सकें। पैच अपडेट के लिए pointers या nullable प्रकार का उपयोग करें ताकि "अनसेट" और "ज़ीरो पर सेट" में फर्क रहे।
हर बार एक स्थिर, स्पष्ट ORDER BY सेट करें, आदत डाल लें कि यह यूनिक कॉलम (अक्सर primary key) पर हो। बिना इसके, पेजिनेशन नए रो आने या स्कैन order बदलने पर रिकॉर्ड्स को छोड़ या डुप्लिकेट कर सकती है।
सर्विस को ब्रांच करने के लिए कॉलर्स को एक छोटा सा एरर सेट दें, जैसे ErrNotFound और ErrConflict, और बाकी सब कुछ underlying DB error के संदर्भ के साथ wrap कर दें। कॉलर्स को स्ट्रिंग पार्स करने पर मजबूर न करें; errors.Is जैसी जाँचें प्रयोग करें और लॉग्स के लिए मददगार संदेश रखें।
शेयर किए गए helpers को एक बार टेस्ट करें (पेजिनेशन normalization, not-found मैपिंग, affected-row चेक), फिर प्रत्येक एंटिटी की SQL और scanning अलग से टेस्ट करें। छोटे "contract tests" जोड़ें जैसे create-then-get मैच होना, update अपेक्षित फील्ड बदलना, delete के बाद get ErrNotFound लौटाना, और list ordering स्थिर होना।


