04 مايو 2025·5 دقيقة قراءة

نمط مستودع CRUD عام في Go لطبقة بيانات نظيفة

تعلّم نمط مستودع CRUD عملي في Go باستخدام generics لإعادة استخدام منطق list/get/create/update/delete مع قيود قابلة للقراءة، دون reflection، وشيفرة واضحة.

نمط مستودع CRUD عام في Go لطبقة بيانات نظيفة

لماذا تصبح مستودعات CRUD فوضوية في Go

تبدأ مستودعات CRUD بسيطة. تكتب GetUser، ثم ListUsers، ثم نفس الشيء لـ Orders ثم Invoices. بعد بضع كيانات، تتحول طبقة البيانات إلى كومة من النسخ المتشابهة حيث تصبح الاختلافات الصغيرة سهلة الإغفال.

ما يتكرر في الغالب ليس SQL نفسه، بل التدفق المحيط: تشغيل الاستعلام، مسح الصفوف، التعامل مع "غير موجود"، تحويل أخطاء قاعدة البيانات، تطبيق إعدادات التجزئة الافتراضية، وتحويل المدخلات إلى الأنواع الصحيحة.

النقاط الساخنة المعتادة مألوفة: كود Scan المكرر، تكرار context.Context ونمط المعاملات، إعدادات LIMIT/OFFSET الروتينية (أحيانًا مع العد الإجمالي)، نفس فحص "0 صفوف يعني غير موجود"، وتكرار متنوع لـ INSERT ... RETURNING id.

عندما يؤذي التكرار بما فيه الكفاية، يلجأ العديد من الفرق إلى reflection. يوعد ذلك بـ "اكتبها مرة واحدة": تأخذ أي هيكل وتملأه من الأعمدة وقت التشغيل. التكلفة تظهر لاحقًا. الكود المعتمد على reflection أصعب في القراءة، دعم IDE يصبح أضعف، والأخطاء تتحول من وقت الترجمة إلى وقت التشغيل. تغييرات صغيرة، مثل إعادة تسمية حقل أو إضافة عمود قابل لأن يكون فارغًا، تصبح مفاجآت تظهر فقط في الاختبارات أو الإنتاج.

إعادة الاستخدام الآمن نوعيًا تعني مشاركة تدفق CRUD دون التخلي عن راحة Go اليومية: تواقيع واضحة، أنواع مفحوصة بالمترجم، وإكمال تلقائي يفيد فعليًا. مع generics، يمكنك إعادة استخدام عمليات مثل Get[T] وList[T] مع ما يزال طلب كل كيان أن يوفّر الأجزاء التي لا يمكن تخمينها، مثل كيفية مسح صف إلى T.

هذا النمط يركز عمدًا على طبقة الوصول إلى البيانات. يحافظ على SQL والربط متسقين ومملين. لا يحاول نمذجة نطاقك، أو فرض قواعد العمل، أو استبدال منطق مستوى الخدمة.

أهداف التصميم (وما لن نحاول حله)

نمط المستودع الجيد يجعل الوصول إلى قاعدة البيانات اليومي قابلًا للتوقع. يجب أن تكون قادرًا على قراءة مستودع ورؤية ما يفعله سريعًا، أي SQL يشغّل، وما الأخطاء التي قد يرجعها.

الأهداف بسيطة:

  • أمان أنواع عبر السلسلة (المعرفات، الكيانات، والنتائج ليست any)
  • قيود تشرح النية دون ألعاب نوعية معقدة
  • تقليل البيروقراطية دون إخفاء السلوك المهم
  • سلوك متسق عبر List/Get/Create/Update/Delete

الأهداف غير المقصودة مهمة بنفس القدر. هذا ليس ORM. لا ينبغي أن يخمن خرائط الحقول، أو ينضم للجداول تلقائيًا، أو يغيّر الاستعلامات بصمت. "الربط السحري" يعيدك إلى reflection، والوسوم، وحالات الحافة.

افترض سير عمل SQL عادي: SQL صريح (أو مُنشئ استعلام رقيق)، حدود معاملات واضحة، وأخطاء يمكنك التفكير بها. عندما يفشل شيء، يجب أن يخبرك الخطأ "غير موجود" أو "تعارض/انتهاك قيد" أو "قاعدة بيانات غير متاحة"، وليس "خطأ في المستودع" غامضًا.

القرار الأساسي هو ما يصبح عامًا مقابل ما يبقى خاصًا بكل كيان.

  • عام: التدفق (تشغيل الاستعلام، المسح، إرجاع القيم المطبّعة بالأنواع، ترجمة الأخطاء الشائعة).
  • لكل كيان: المعنى (أسماء الجداول، الأعمدة المحددة، الانضمامات، وسلاسل SQL).

محاولة إجبار كل الكيانات في نظام فلترة عالمي عادةً ما تجعل الكود أصعب للقراءة من كتابة استعلامين واضحين.

اختيار قيود الكيان والمعرف

معظم كود CRUD يتكرر لأن كل جدول يقوم بنفس الحركات الأساسية، لكن كل كيان له حقوله الخاصة. مع generics، الحيلة هي مشاركة شكل صغير وترك كل شيء آخر حرًا.

ابدأ بتحديد ما يجب أن يعرفه المستودع حقًا عن كيان. بالنسبة للعديد من الفرق، الجزء الوحيد العالمي هو المعرف (ID). الطوابع الزمنية قد تكون مفيدة، لكنها ليست عالمية، وإجباريتها في كل نوع تجعل النموذج يبدو مزيفًا.

اختر نوع معرف يمكنك التعايش معه

يجب أن يطابق نوع المعرف كيف تحدد الصفوف في قاعدة البيانات. تستخدم بعض المشاريع int64، وأخرى تستخدم سلاسل UUID. إذا أردت نهجًا واحدًا يعمل عبر الخدمات، اجعل المعرف عامًا. إذا كانت قاعدة الشيفرة كلها تستخدم نوع معرف واحد، إبقاؤه ثابتًا يمكن أن يقصر التواقيع.

قيد افتراضي جيد للمعرفات هو comparable، لأنك ستقارن المعرفات، وتستخدمها كمفاتيح في الخرائط، وتنقلها.

type ID interface {
	comparable
}

type Entity[IDT ID] interface {
	GetID() IDT
	SetID(IDT)
}

اجعل قيود الكيان قليلة قدر الإمكان

تجنب طلب الحقول عبر تضمين struct أو حيل مجموعات الأنواع مثل ~struct{...}. قد تبدو قوية، لكنها تقترن أنواع نطاقك بنمط المستودع.

بدلًا من ذلك، اطلب فقط ما يحتاجه تدفق CRUD المشترك:

  • الحصول على المعرف وتعيينه (لكي يعيد Create المعرف، ولتستهدف Update/Delete به)

إذا أضفت لاحقًا ميزات مثل الحذف الناعم أو التحقق من الإصدارات، أضف واجهات اختيارية صغيرة (مثال: GetVersion/SetVersion) واستخدمها فقط حيث يلزم. الواجهات الصغيرة تميل لأن تدوم جيدًا.

واجهة مستودع عامة تبقى قابلة للقراءة

يجب أن تصف واجهة المستودع ما يحتاجه تطبيقك، وليس ما تفعله قاعدة البيانات بالصدفة. إذا شعرت الواجهة كأنها SQL، فإنها ستسرّب التفاصيل في كل مكان.

اجعل مجموعة الطرق صغيرة ومتوقعة. ضع context.Context أولًا، ثم المدخل الرئيسي (المعرف أو البيانات)، ثم خيارات اختيارية مجمّعة في هيكل.

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، تجنب إجبار نوع فلترة عالمي. الفلاتر هي حيث تختلف الكيانات أكثر. نهج عملي هو أنواع استعلام لكل كيان بالإضافة إلى شكل ترقيم صغير مشترك يمكنك تضمينه.

type Page struct {
	Limit  int
	Offset int
}

التعامل مع الأخطاء هو المكان الذي غالبًا ما يصبح فيه المستودعات صاخبة. قرر مسبقًا أي الأخطاء يسمح للمستدعين بالتفرّع بناءً عليها. مجموعة بسيطة عادةً تعمل:

  • ErrNotFound عندما لا يوجد معرف
  • ErrConflict لانتهاكات الفريد أو تضارب الإصدارات
  • ErrValidation عندما تكون المدخلات غير صالحة (فقط إذا كان المستودع يتحقق)

كل شيء آخر يمكن أن يكون خطأ منخفض المستوى ملفوفًا. مع هذا العقد، يمكن لكود الخدمة التعامل مع "غير موجود" أو "تعارض" دون الاهتمام بما إذا كانت التخزين PostgreSQL اليوم أو شيء آخر لاحقًا.

كيف تتجنّب reflection بينما لا تزال تعيد استخدام التدفق

Keep inputs consistent
Separate create and update inputs cleanly so handlers stay predictable as schemas evolve.
Try Now

عادةً ما يتسلل reflection عندما تريد قطعة واحدة من الكود أن "تملأ أي هيكل". هذا يخفي الأخطاء حتى وقت التشغيل ويجعل القواعد غير واضحة.

النهج الأنظف هو إعادة استخدام الأجزاء المملة فقط: تنفيذ الاستعلامات، تكرار الصفوف، فحص عدد الصفوف المتأثرة، ولف الأخطاء بشكل متسق. اجعل الربط من وإلى structs صريحًا.

قسّم المسؤوليات: SQL، الربط، والتدفق المشترك

تقسيم عملي يبدو هكذا:

  • لكل كيان: احتفظ بسلاسل SQL وترتيب المعاملات في مكان واحد
  • لكل كيان: اكتب دوال ربط صغيرة تمسح الصفوف إلى الهيكل المحدد
  • عام: قدّم التدفق المشترك الذي ينفّذ الاستعلام وينادي على الماسح

بهذه الطريقة، تقلل generics التكرار دون إخفاء ما تفعله قاعدة البيانات.

إليك تجريدًا صغيرًا يسمح بتمرير إما *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
}

ما الذي يجب أن تفعله (وما الذي لا يجب) generics

الطبقة العامة لا ينبغي أن تحاول "فهم" struct الخاص بك. بدلًا من ذلك، يجب أن تقبل دوال صريحة تزوّدها بها، مثل:

  • رابط يحول المدخلات إلى معاملات الاستعلام
  • ماسح يقرأ الأعمدة إلى كيان

على سبيل المثال، يمكن لمستودع Customer تخزين SQL كثوابت (selectByID, insert, update) وتنفيذ scanCustomer(rows) مرة واحدة. يمكن لـ List العام التعامل مع الحلقة، وcontext، ولف الأخطاء، بينما يبقي scanCustomer الربط آمنًا نوعيًا وواضحًا.

إذا أضفت عمودًا، تحدّث SQL والماسح. المترجم يساعدك في العثور على ما تعطل.

خطوة بخطوة: تنفيذ النمط

الهدف هو تدفق قابل لإعادة الاستخدام واحد لـ List/Get/Create/Update/Delete مع إبقاء كل مستودع صادقًا فيما يتعلق بـ SQL وربط الصف.

1) عرّف الأنواع الأساسية

ابدأ بأقل القيود الممكنة. اختر نوع معرف يعمل لقاعدة شيفرتك وواجهة مستودع تبقى متوقعة.

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 والمعاملات

لا تربط الكود العام مباشرة بـ *sql.DB أو *sql.Tx. اعتمد على واجهة منفّذ صغيرة تتطابق مع ما تستدعوه (QueryContext, ExecContext, QueryRowContext). ثم يمكن للخدمات تمرير DB أو معاملة دون تغيير كود المستودع.

3) ابنِ أساسًا عامًا بتدفق مشترك

انشئ baseRepo[E,K] يخزن المنفّذ وبعض حقول الدوال. الأساس يتعامل مع الأجزاء المملة: استدعاء الاستعلام، تحويل "غير موجود"، فحص الصفوف المتأثرة، وإرجاع أخطاء متسقة.

4) نفّذ الأجزاء الخاصة بكل كيان

كل مستودع كيان يوفّر ما لا يمكن أن يكون عامًا:

  • SQL لـ list/get/create/update/delete
  • دالة scan(row) التي تحول صفًا إلى E
  • دالة bind(...) التي تعيد معاملات الاستعلام

5) اربط المستودعات الملموسة واستخدمها من الخدمات

ابنِ NewCustomerRepo(exec Executor) *CustomerRepo التي تضم أو تغلف baseRepo. تعتمد طبقة الخدمة على واجهة Repo[E,K] وتقرر متى تبدأ معاملة؛ المستودع فقط يستخدم المنفّذ المزوّد به.

التعامل مع List/Get/Create/Update/Delete دون مفاجآت

From schema to full app
Design PostgreSQL data, then generate web and mobile apps on top of it.
Start Building

مستودع عام يساعد فقط إذا تصرف كل أسلوب بنفس الطريقة في كل مكان. يأتي معظم الألم من التباينات الصغيرة: واحد يرتب بـ created_at، وآخر بـ id; واحد يرجع nil, nil للصفوف المفقودة، وآخر يرجع خطأ.

List: ترقيم الصفحات والترتيب الذي لا يتغير

اختر أسلوب ترقيم واحد وطبّقه باستمرار. ترقيم الإزاحة (limit/offset) بسيط ويناسب لوحات الإدارة. ترقيم المؤشر أفضل للتمرير المستمر، لكنه يحتاج مفتاح ترتيب ثابت.

مهما اخترت، اجعل الترتيب صريحًا ومستقرًا. الترتيب على عمود فريد (غالبًا المفتاح الأساسي) يمنع العناصر من القفز بين الصفحات عند ظهور صفوف جديدة.

Get: إشارة "غير موجود" واضحة

Get(ctx, id) يجب أن يرجع كيانًا مطبوع النوع وإشارة واضحة إلى أن السجل مفقود، عادةً خطأ مرجعي مثل ErrNotFound. تجنب إرجاع قيمة صفرية مع خطأ nil للصفوف المفقودة. المستدعون لا يمكنهم التمييز بين "مفقود" و"حقول فارغة".

اجعل هذه العادة مبكرة: النوع للبيانات، والخطأ للحالة.

قبل تنفيذ الطرق، اتخذ بعض القرارات واحتفظ بها متسقة:

  • Create: هل تقبل نوع إدخال (بدون معرف، بدون طوابع زمنية) أم كيانًا كاملاً؟ كثير من الفرق تفضّل Create(ctx, in CreateX) لمنع المستدعين من تعيين حقول يملكها الخادم.
  • Update: هل هو استبدال كامل أم تصحيح جزئي (patch)؟ إذا كان تصحيحًا جزئيًا، لا تستخدم structs عادية حيث تكون القيم الصفرية غامضة. استخدم مؤشرات، أو أنواع قابلة لأن تكون فارغة، أو قناع حقول صريح.
  • Delete: حذف دائم أم حذف ناعم؟ إذا كان حذفًا ناعمًا، قرر ما إذا كان Get يخفي الصفوف المحذوفة افتراضيًا.

قرر أيضًا ماذا تُرجع طرق الكتابة. خيارات قليلة المفاجآت هي إرجاع الكيان المحدث (بعد قيم القاعدة الافتراضية) أو إرجاع المعرف مع ErrNotFound عندما لم يتغير شيء.

استراتيجية الاختبار للأجزاء العامة والخاصة بالكيان

Standardize CRUD behavior
Create consistent list and get endpoints with stable behavior across your entities.
Generate API

هذا النهج يدفع فقط إذا كان من السهل الوثوق به. قسّم الاختبارات مثلما قسّمت الشيفرة: اختبر المساعدات المشتركة مرة واحدة، ثم اختبر SQL ومسح كل كيان منفصلًا.

عامل القطع المشتركة كدوال نقية صغيرة متى أمكن، مثل تطبيع الترقيم، تحويل مفاتيح الفرز إلى أعمدة مسموح بها، أو بناء أجزاء WHERE. يمكن تغطيتها باختبارات وحدة سريعة.

للاستعلامات القائمة، تعمل اختبارات الجدول الموجه جيدًا لأن حالات الحافة هي جوهر المشكلة. غطِ أمورًا مثل الفلاتر الفارغة، مفاتيح فرز غير معروفة، limit صفر، حد أقصى يتجاوز الحد، إزاحة سالبة، وحدود "الصفحة التالية" حيث تستجلب صفًا إضافيًا.

اجعل اختبارات كل كيان تركز على ما هو خاص بالفعل بالكيان: SQL المتوقع تشغيله وكيفية مسح الصفوف إلى نوع الكيان. استخدم قوالب SQL مزيفة أو قاعدة بيانات اختبار خفيفة وتأكد أن منطق المسح يتعامل مع القيم الفارغة، الأعمدة الاختيارية، وتحويل الأنواع.

إذا كان نمطك يدعم المعاملات، اختبر سلوك commit/rollback مع منفّذ مزيف صغير يسجّل الاستدعاءات ويُحاكي الأخطاء:

  • Begin يرجع منفّذ نطاق المعاملة
  • عند الخطأ، يُستدعى rollback مرة واحدة بالضبط
  • عند النجاح، يُستدعى commit مرة واحدة بالضبط
  • إذا فشل commit، يُرجع الخطأ دون تغيّر

يمكنك أيضًا إضافة "اختبارات عقدية" صغيرة يجب أن يمرّ بها كل مستودع: create ثم get يعيد نفس البيانات، update يغيّر الحقول المتوقعة، delete يجعل get يعيد ErrNotFound، وlist يعيد ترتيبًا ثابتًا تحت نفس المدخلات.

أخطاء ومزالق شائعة

تجعل generics من المغري بناء مستودع واحد يحكم الجميع. الوصول إلى البيانات مليء بالفروق الصغيرة، وتلك الفروق مهمة.

بعض المزالق تظهر غالبًا:

  • التعميم المفرط حتى تصبح كل طريقة تأخذ كيسًا ضخمًا من الخيارات (انضمامات، بحث، أذونات، حذف ناعم، تخزين مؤقت). عندها تكون قد بنيت ORM ثاني.
  • قيود معقدة للغاية. إذا احتاج القارئ لفك مجموعات الأنواع لفهم ما يجب أن يوفّره الكيان، فإن التجريد يكلف أكثر مما يوفر.
  • معاملة أنواع الإدخال كنموذج قاعدة البيانات. عندما تأخذ Create وUpdate نفس struct الذي تمسحه من الصفوف، تتسرب تفاصيل القاعدة إلى المعالجات والاختبارات، وتتكرر تغييرات المخطط عبر التطبيق.
  • سلوك صامت في List: فرز غير ثابت، افتراضات غير متسقة، أو قواعد ترقيم تختلف حسب الكيان.
  • معالجة غير موجود تجبر المستدعين على تحليل سلاسل الخطأ بدلًا من استخدام errors.Is.

مثال ملموس: ListCustomers يرجع العملاء بترتيب مختلف في كل مرة لأن المستودع لا يحدد ORDER BY. عندها الترقيم يكرر أو يتخطى سجلات بين الطلبات. اجعل الفرز صريحًا (حتى لو كان المفتاح الأساسي) واحتفظ بالإعدادات الافتراضية متسقة.

قائمة فحص سريعة قبل الاعتماد على هذا

Reduce boilerplate safely
Build typed backends and UI screens while keeping the codebase easy to extend.
Get Started

قبل أن تعمم مستودعًا عامًا لكل حزمة، تأكد أنه سيزيل التكرار دون إخفاء سلوك قاعدة البيانات المهم.

ابدأ بالاتساق. إذا كان مستودع واحد يأخذ context.Context وآخر لا يفعل، أو أحدهما يرجع (T, error) وآخر يرجع (*T, error), سيظهر الألم في كل مكان: الخدمات، الاختبارات، والقوالب.

تأكد أن لكل كيان لا يزال له موضع واضح واحد لـ SQL. يجب أن تعيد generics التدفق (المسح، التحقق، لف الأخطاء)، لا تشتت الاستعلامات عبر شظايا سلاسل.

قائمة فحوصات سريعة تمنع معظم المفاجآت:

  • قاعدة توقيع واحدة لـ List/Get/Create/Update/Delete
  • قاعدة واحدة متوقعة لـ not-found يستخدمها كل مستودع
  • ترتيب قائمة ثابت موثق ومختبر
  • طريقة نظيفة لتشغيل نفس الشيفرة على *sql.DB و*sql.Tx (عبر واجهة منفّذ)
  • حد واضح بين الشيفرة العامة وقواعد الكيان (التحقق وقواعد العمل تبقى خارج الطبقة العامة)

إذا تبني داخل أدوات داخلية بسرعة في AppMaster ثم صدّرت أو وسّعت الشيفرة الناتجة لاحقًا، تساعد هذه الفحوصات في إبقاء طبقة البيانات متوقعة وسهلة الاختبار.

مثال واقعي: بناء مستودع Customer

إليك شكل مستودع Customer صغير يبقى آمنًا نوعيًا دون أن يصبح ذكيًا كثيرًا.

ابدأ بنموذج مخزن. اجعل المعرف قوي النوع حتى لا تخلط بينه وبين معرفات أخرى عن طريق الخطأ:

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
}

الأساس العام يمكنه التعامل مع التدفق المشترك (تنفيذ SQL، المسح، لف الأخطاء)، بينما يمتلك مستودع 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، عامل الفلاتر والترقيم ككائن طلب من الدرجة الأولى. هذا يبقي مواقع الاستدعاء مقروءة ويجعل نسيان الحدود أصعب.

type CustomerListQuery struct {
	Status *string // filter
	Search *string // name contains
	Limit  int
	Offset int
}

من هنا، يتوسع النمط جيدًا: انسخ البنية للكيان التالي، حافظ على فصل المدخلات عن نماذج التخزين، واجعل المسح صريحًا حتى تبقى التغييرات واضحة ومساعدة المترجم مفيدة.

الأسئلة الشائعة

ما المشكلة التي تحلها مستودعات CRUD العامة في Go فعليًا؟

استخدم generics لإعادة استخدام التدفق (تشغيل الاستعلام، حلقة المسح، التعامل مع "غير موجود"، إعدادات التجزئة الافتراضية، تحويل الأخطاء)، لكن احتفظ بـ SQL وربط الصفوف صريحًا لكل كيان. هذا يمنحك تكرارًا أقل دون تحويل طبقة البيانات إلى “سحر” وقت التشغيل الذي يكسر بصمت.

لماذا تجنّب أدوات CRUD المعتمدة على reflection التي تمسح أي هيكل؟

الانعكاس (reflection) يخفي قواعد الربط وينقل الأخطاء إلى وقت التشغيل. تفقد فحوصات المترجم (compiler)، ويدعم IDE أقل، وتصبح تغييرات المخطط الصغيرة مفاجآت. مع generics ودوال الماسح (scanner) الصريحة، تحافظ على أمان النوع بينما لا تزال تشارك الأجزاء المتكررة.

ما القيود المعقولة لنوع المعرف (ID)؟

إعداد مفيد هو comparable لأن المعرفات تُقارَن، وتُستخدم كمفاتيح في الخرائط، وتُنقل في كل مكان. إذا كان النظام لديك يستخدم أنماط معرفات متعددة (مثل int64 وسلاسل UUID)، فإن جعل نوع المعرف عامًا يمنع إجبار اختيار واحد عبر كل المستودعات.

ماذا يجب أن يتضمن قيد الكيان (entity constraint) وما الذي لا يجب تضمينه؟

اجعلها بسيطة: عادةً ما يكفي ما يحتاجه التدفق المشترك، مثل GetID() وSetID(). تجنب إجبار الحقول المشتركة عبر embedding أو مجموعات نوعية معقّدة، لأن ذلك يقيد أنواع النطاق (domain) بنمط المستودع ويصعّب إعادة الهيكلة.

كيف أدعم كلًا من *sql.DB و *sql.Tx بشكل نظيف؟

استخدم واجهة منفّذة صغيرة (مثل DBTX) تحتوي فقط على الأساليب التي تستدعيها، مثل QueryContext وQueryRowContext وExecContext. حينها يمكن لشيفرة المستودع العمل على *sql.DB أو *sql.Tx دون تفرعين أو تكرار.

ما أفضل طريقة للإشارة إلى "غير موجود" من Get؟

إرجاع قيمة صفرية مع خطأ nil لـ “غير موجود” يجبر المستدعين على التخمين هل السجل مفقود أم أن الحقول فارغة. استخدام مؤشر مشترك مثل ErrNotFound يحفظ الحالة في قناة الأخطاء، فيمكن للكود أن يتفرع بثقة باستخدام errors.Is.

هل يجب أن تأخذ Create/Update هيكل الكيان الكامل؟

فصّل المدخلات عن نموذج التخزين. الأفضلية لـ Create(ctx, CreateInput) وUpdate(ctx, id, UpdateInput) حتى لا يستطيع المستدعون تعيين حقول يملكها الخادم مثل المعرفات أو الطوابع الزمنية. للتحديث الجزئي، استخدم مؤشرات (أو أنواع قابلة لأن تكون فارغة) لتستطيع التمييز بين "لم يُحدد" و"تم تعيينه إلى الصفر".

كيف أحافظ على عدم تناقض نتائج ترقيم الصفحات في List؟

حدد ORDER BY مستقرًا وصريحًا في كل مرة، ويفضل أن يكون على عمود فريد مثل المفتاح الأساسي. بدونه، قد تفقد أو تكرر العناصر بين الطلبات عندما تظهر صفوف جديدة أو يغيّر المخطط ترتيب المسح.

ما عقدة الأخطاء التي يجب أن توفّرها المستودعات للخدمات؟

كشف مجموعة صغيرة من الأخطاء التي يمكن للمستدعين التفريع بناءً عليها، مثل ErrNotFound وErrConflict، ولفّ كل شيء آخر مع سياق من خطأ قاعدة البيانات الأساسي. لا تجبر المستدعين على تحليل سلاسل؛ استهدف فحوصات errors.Is مع رسالة مساعدة للسجلات.

كيف أختبر نمط مستودع عام دون الإفراط في الاختبار؟

اختبر المساعدة المشتركة مرة واحدة (تطبيع الترقيح، تحويل "غير موجود"، فحوصات عدد الصفوف المتأثرة)، ثم اختبر SQL والمسح لكل كيان بشكل منفصل. أضف "اختبارات عقدية" صغيرة لكل مستودع: إنشاء ثم استدعاء Get يعيد نفس البيانات، التحديث يغيّر الحقول المتوقعة، الحذف يجعل Get يعيد ErrNotFound، وList يضمن ترتيبًا ثابتًا.

من السهل أن تبدأ
أنشئ شيئًا رائعًا

تجربة مع AppMaster مع خطة مجانية.
عندما تكون جاهزًا ، يمكنك اختيار الاشتراك المناسب.

البدء