Dans cet article, je veux résumer les problèmes de travail avec des bases de données exécutant golang. Lors de la résolution de problèmes simples, ces problèmes ne sont généralement pas visibles. À mesure que le projet se développe, le problème augmente également. Le plus d'actualité:
- Diminution de la connectivité d'une application de base de données
- Journalisation des requêtes en mode débogage
- Travailler avec des répliques
L'article est basé sur le package github.com/adverax/echo/database/sql. La sémantique de l'utilisation de ce package est aussi proche que possible du package standard de base de données / sql, donc je ne pense pas que quiconque aura des problèmes à l'utiliser.
Portée
En règle générale, les grands systèmes essaient de se connecter de manière lâche avec une zone de responsabilité claire pour chaque composant du système. Par conséquent, les modèles de conception éditeur / abonné sont largement pratiqués. Prenons un petit exemple d'enregistrement d'un nouvel utilisateur dans le système.
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{
Dans cet exemple, nous nous intéressons principalement à l'événement OnSignup. Pour simplifier, le gestionnaire est représenté par une seule fonction (dans la vie réelle, tout est plus compliqué). Dans la signature de l'événement, nous prescrivons de manière rigide le type du premier paramètre, ce qui a généralement des conséquences de grande portée.
Supposons que maintenant nous voulons étendre les fonctionnalités de notre application et en cas d'enregistrement réussi de l'utilisateur, envoyer un message à son compte personnel. Idéalement, le message doit être placé dans la même transaction que l'enregistrement de l'utilisateur.
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{
Comme vous pouvez le voir sur l'exemple, nous avons été obligés de changer la signature de l'événement. Cette solution n'est pas propre et implique que les gestionnaires ont connaissance du contexte de l'exécution de la requête de base de données. Une solution beaucoup plus propre consisterait à utiliser une base de données générique et une interface de transaction - portée.
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{
Pour implémenter cette approche, nous aurons besoin de la prise en charge des transactions imbriquées, car le gestionnaire, à son tour, peut utiliser les transactions. Heureusement, ce n'est pas un problème, car la plupart des SGBD prennent en charge le mécanisme SAVEPOINT.
Base de données et contexte
Dans la pratique normale, la connexion à la base de données n'est pas transmise en tant que paramètre, comme indiqué ci-dessus, et chaque gestionnaire conserve un lien vers la connexion à la base de données. Cela simplifie les signatures de méthode et améliore la lisibilité du code. Dans notre cas, il est impossible d'éviter cela, car vous devez transférer un lien vers la transaction.
Une solution assez élégante consiste à placer le lien vers la transaction (portée) dans le contexte, car le contexte est positionné comme un paramètre d'intercommunication. Ensuite, nous pouvons simplifier davantage notre code:
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) } }
Cet exemple montre que nous avons maintenu l'isolement complet des managers, augmenté la lisibilité du code et réalisé leur travail commun dans un seul périmètre.
Prise en charge de la réplication
La bibliothèque prend également en charge l'utilisation de la réplication. Toutes les demandes de type Exec sont envoyées au maître. Les demandes de type esclave sont transférées vers un esclave sélectionné au hasard. Pour prendre en charge la réplication, spécifiez simplement plusieurs sources de données:
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() ... }
Si vous utilisez une seule source de données lors de l'ouverture d'une base de données, elle sera ouverte en mode normal sans surcharge supplémentaire.
Mesures
Comme vous le savez, les mesures sont bon marché et les journaux sont chers. Par conséquent, il a été décidé d'ajouter la prise en charge des mesures par défaut.
Profilage et journalisation des requêtes
Il est très nécessaire de consigner les requêtes de base de données pendant le débogage. Cependant, je n'ai pas vu un mécanisme de journalisation de haute qualité avec zéro frais généraux en production. La bibliothèque vous permet de résoudre élégamment ce problème en encapsulant la base de données. Pour profiler la base de données, il suffit de lui passer l'activateur approprié:
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() ... }
Conclusion
Le package proposé vous permet d'étendre les possibilités d'interaction avec la base de données, tout en cachant des détails inutiles. Cela vous permet d'améliorer la qualité du code, le laissant vaguement connecté et transparent, malgré la complexité croissante de l'application.