في هذه المقالة ، أود أن ألخص مشاكل العمل مع قواعد البيانات التي تشغل golang. عند حل المشكلات البسيطة ، عادة ما تكون هذه المشكلات غير مرئية. كما ينمو المشروع ، وكذلك المشكلة. الأكثر موضعية منهم:
- انخفاض الاتصال من تطبيق قاعدة البيانات
- تسجيل الاستعلام وضع التصحيح
- العمل مع النسخ المتماثلة
تستند المقالة إلى الحزمة github.com/adverax/echo/database/sql. تعتبر دلالات استخدام هذه الحزمة أقرب ما يمكن إلى حزمة قاعدة البيانات / sql القياسية ، لذلك لا أعتقد أن أي شخص سيواجه مشاكل في استخدامه.
مجال
وكقاعدة عامة ، تحاول الأنظمة الكبيرة أن تكون مرتبطة بشكل ضعيف بمساحة واضحة من المسؤولية لكل مكون من مكونات النظام. لذلك ، تمارس أنماط تصميم الناشر / المشترك على نطاق واسع. فكر في مثال صغير لتسجيل مستخدم جديد في النظام.
package main import "database/sql" type User struct { Id int64 Name string Language string } type Manager struct { DB *sql.DB OnSignup func(db *sql.DB, user *User) error } func (m *Manager) Signup(user *User) (id int64, err error) { id, err = m.insert(user) if err != nil { return } user.Id = id err = m.OnSignup(m.DB, user) return } func (m *Manager) insert(user *User) (int64, error) { res, err := m.DB.Exec("INSERT ...") if err != nil { return 0, err } id, err := res.LastInsertId() if err != nil { return 0, err } return id, err } func main() { manager := &Manager{
في هذا المثال ، نحن مهتمون بشكل أساسي بحدث OnSignup. لتبسيط ، يتم تمثيل المعالج بواسطة وظيفة واحدة (في الحياة الحقيقية ، كل شيء أكثر تعقيدًا). في توقيع الحدث ، نصف بصرامة نوع المعلمة الأولى ، والتي عادة ما تكون لها عواقب بعيدة المدى.
لنفترض الآن أننا نريد توسيع وظيفة تطبيقنا وفي حالة نجاح تسجيل المستخدم ، أرسل رسالة إلى حسابه الشخصي. من الناحية المثالية ، يجب وضع الرسالة في نفس المعاملة مثل تسجيل المستخدم.
type Manager struct { DB *sql.DB OnSignup func(tx *sql.Tx, user *User) error } func (m *Manager) Signup(user *User) error { tx, err := m.DB.Begin() if err != nil { return err } defer tx.Rollback() id, err := m.insert(user) if err != nil { return err } user.Id = id err = m.OnSignup(tx, id) if err != nil { return err } return tx.Commit() } func main() { manager := &Manager{
كما ترون من المثال ، اضطررنا إلى تغيير توقيع الحدث. هذا الحل غير نظيف ويشير إلى أن المعالجين لديهم معرفة بسياق تنفيذ استعلام قاعدة البيانات. سيكون الحل الأكثر نظافة استخدام قاعدة بيانات عامة وواجهة نطاق المعاملة.
import "github.com/adverax/echo/database/sql" type Manager struct { DB sql.DB OnSignup func(scope sql.Scope, user *User) error } func (m *Manager) Signup(user *User) error { tx, err := m.DB.Begin() if err != nil { return err } defer tx.Rollback() id, err := m.insert(user) if err != nil { return err } err = m.OnSignup(tx, id) if err != nil { return err } return tx.Commit() } func main() { manager := &Manager{
لتنفيذ هذا النهج ، سنحتاج إلى دعم للمعاملات المتداخلة ، حيث يمكن للمعالج بدوره استخدام المعاملات. لحسن الحظ ، هذه ليست مشكلة ، لأن معظم نظم إدارة قواعد البيانات تدعم آلية SAVEPOINT.
قاعدة البيانات والسياق
في الممارسة العملية ، لا يتم تمرير الاتصال بقاعدة البيانات كمعلمة ، كما هو موضح أعلاه ، ويحتفظ كل مدير ارتباط إلى اتصال قاعدة البيانات. هذا يبسط تواقيع الطريقة ويحسن قراءة التعليمات البرمجية. في حالتنا ، من المستحيل تجنب ذلك ، لأنك تحتاج إلى نقل رابط للمعاملة.
أحد الحلول الأنيقة إلى حد ما هو وضع رابط المعاملة (النطاق) في السياق ، لأن السياق يتم وضعه كمعلمة نهاية إلى نهاية. ثم يمكننا تبسيط التعليمات البرمجية الخاصة بنا بشكل أكبر:
import ( "context" "github.com/adverax/echo/database/sql" ) type Manager struct { sql.Repository OnSignup func(ctx context.Context, user *User) error } func (m *Manager) Signup(ctx context.Context, user *User) error { return m.Transaction( ctx, func(ctx context.Context, scope sql.Scope) error { id, err := m.insert(user) if err != nil { return err } user.Id = id return m.OnSignup(ctx, user) }, ) } type Messenger struct { sql.Repository } func(messenger *Messenger) onSignupUser(ctx context.Context, user *User) error { _, err := messenger.Scope(ctx).Exec("INSERT ...") return err } func main() { db := ... messenger := &Messenger{ Repository: sql.NewRepository(db), } manager := &Manager{ Repository: sql.NewRepository(db), OnSignup: messenger.onSignup, } err := manager.Signup(&User{...}) if err != nil { panic(err) } }
يوضح هذا المثال أننا حافظنا على العزلة الكاملة للمديرين ، وزادنا من قراءة الكود ، وحققنا عملهم المشترك في نطاق واحد.
دعم النسخ المتماثل
تدعم المكتبة أيضًا استخدام النسخ المتماثل. يتم إرسال جميع طلبات النوع Exec إلى Master. يتم نقل طلبات من نوع الرقيق إلى الرقيق المحدد عشوائيا. لدعم النسخ المتماثل ، حدد فقط عدة مصادر للبيانات:
func work() { dsc := &sql.DSC{ Driver: "mysql", DSN: []*sql.DSN{ { Host: "127.0.0.1", Database: "echo", Username: "root", Password: "password", }, { Host: "192.168.44.01", Database: "echo", Username: "root", Password: "password", }, }, } db := dsc.Open(nil) defer db.Close() ... }
إذا كنت تستخدم مصدر بيانات واحد عند فتح قاعدة بيانات ، فسيتم فتحه في الوضع العادي دون حمل إضافي.
المقاييس
كما تعلمون ، المقاييس رخيصة ، والسجلات غالية الثمن. لذلك ، تقرر إضافة دعم للمقاييس الافتراضية.
الاستعلام التنميط وتسجيل
من الضروري جدًا تسجيل استعلامات قاعدة البيانات أثناء تصحيح الأخطاء. ومع ذلك ، لم أر آلية تسجيل ذات جودة عالية مع وجود صفر في الإنتاج. تتيح لك المكتبة حل هذه المشكلة بأناقة عن طريق التفاف قاعدة البيانات. لتعريف قاعدة البيانات ، ما عليك سوى تمرير المنشط المناسب إليها:
func openDatabase(dsc sql.DSC, debug bool) (sql.DB, error){ if debug { return dsc.Open(sql.OpenWithProfiler(nil, "", nil)) } return dsc.Open(nil) } func main() { dsc := ... db, err := openDatabase(dsc, true) if err != nil { panic(err) } defer db.Close() ... }
استنتاج
تسمح لك الحزمة المقترحة بتوسيع إمكانيات التفاعل مع قاعدة البيانات ، مع إخفاء التفاصيل غير الضرورية. يسمح لك هذا بتحسين جودة الكود ، وتركه مترابطًا وشفافًا ، على الرغم من التعقيد المتزايد للتطبيق.