In diesem Artikel möchte ich die Probleme bei der Arbeit mit Datenbanken zusammenfassen, auf denen golang ausgeführt wird. Bei der Lösung einfacher Probleme sind diese Probleme normalerweise nicht sichtbar. Wenn das Projekt wächst, wächst auch das Problem. Das aktuellste von ihnen:
- Verringern der Konnektivität einer Datenbankanwendung
- Abfrageprotokollierung im Debug-Modus
- Arbeiten Sie mit Replikaten
Der Artikel basiert auf dem Paket github.com/adverax/echo/database/sql. Die Semantik der Verwendung dieses Pakets kommt dem Standard-Datenbank- / SQL-Paket so nahe wie möglich, sodass ich nicht glaube, dass irgendjemand Probleme haben wird, es zu verwenden.
Geltungsbereich
In der Regel versuchen große Systeme, eine lose Verbindung mit einem klaren Verantwortungsbereich für jede Komponente des Systems herzustellen. Daher sind Entwurfsmuster für Verlage / Abonnenten weit verbreitet. Betrachten Sie ein kleines Beispiel für die Registrierung eines neuen Benutzers im System.
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{
In diesem Beispiel interessieren wir uns hauptsächlich für das OnSignup-Ereignis. Zur Vereinfachung wird der Handler durch eine einzige Funktion dargestellt (im wirklichen Leben ist alles komplizierter). In der Ereignissignatur schreiben wir den Typ des ersten Parameters streng vor, was normalerweise weitreichende Konsequenzen hat.
Angenommen, wir möchten jetzt die Funktionalität unserer Anwendung erweitern und im Falle einer erfolgreichen Benutzerregistrierung eine Nachricht an sein persönliches Konto senden. Idealerweise sollte die Nachricht in derselben Transaktion wie die Benutzerregistrierung platziert werden.
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{
Wie Sie dem Beispiel entnehmen können, mussten wir die Signatur des Ereignisses ändern. Diese Lösung ist nicht sauber und impliziert, dass die Handler den Kontext der Ausführung der Datenbankabfrage kennen. Eine viel sauberere Lösung wäre die Verwendung einer generischen Datenbank und einer Transaktionsschnittstelle - Umfang.
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{
Um diesen Ansatz zu implementieren, benötigen wir Unterstützung für verschachtelte Transaktionen, da der Handler wiederum Transaktionen verwenden kann. Glücklicherweise ist dies kein Problem, da die meisten DBMS den SAVEPOINT-Mechanismus unterstützen.
Datenbank und Kontext
In der Regel wird die Verbindung zur Datenbank nicht wie oben gezeigt als Parameter übergeben, und jeder Manager behält eine Verbindung zur Verbindung zur Datenbank bei. Dies vereinfacht die Methodensignaturen und verbessert die Lesbarkeit des Codes. In unserem Fall ist dies nicht zu vermeiden, da Sie einen Link zur Transaktion übertragen müssen.
Eine ziemlich elegante Lösung besteht darin, den Link zur Transaktion (Bereich) in den Kontext zu stellen, da der Kontext als Pass-Through-Parameter positioniert ist. Dann können wir unseren Code weiter vereinfachen:
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) } }
Dieses Beispiel zeigt, dass wir die vollständige Isolation der Manager beibehalten, die Lesbarkeit des Codes verbessert und ihre gemeinsame Arbeit in einem einzigen Bereich erreicht haben.
Replikationsunterstützung
Die Bibliothek unterstützt auch die Verwendung der Replikation. Alle Anfragen vom Typ Exec werden an den Master gesendet. Anfragen vom Slave-Typ werden an einen zufällig ausgewählten Slave übertragen. Geben Sie zur Unterstützung der Replikation einfach mehrere Datenquellen an:
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() ... }
Wenn Sie beim Öffnen einer Datenbank eine einzelne Datenquelle verwenden, wird diese im normalen Modus ohne zusätzlichen Aufwand geöffnet.
Metriken
Wie Sie wissen, sind Metriken billig und Protokolle teuer. Daher wurde beschlossen, die Standardmetriken zu unterstützen.
Abfrageprofilerstellung und Protokollierung
Es ist sehr wichtig, Datenbankabfragen während des Debuggens zu protokollieren. Ich habe jedoch keinen hochwertigen Protokollierungsmechanismus ohne Overhead in der Produktion gesehen. Mit der Bibliothek können Sie dieses Problem elegant lösen, indem Sie die Datenbank umschließen. Um die Datenbank zu profilieren, übergeben Sie einfach den entsprechenden Aktivator:
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() ... }
Fazit
Mit dem vorgeschlagenen Paket können Sie die Interaktionsmöglichkeiten mit der Datenbank erweitern und unnötige Details verbergen. Auf diese Weise können Sie die Qualität des Codes verbessern und ihn trotz der zunehmenden Komplexität der Anwendung lose und transparent verbinden.