Golang e a evolução da interação com o banco de dados

Neste artigo, quero resumir os problemas de trabalhar com bancos de dados executando golang. Ao resolver problemas simples, esses problemas geralmente não são visíveis. À medida que o projeto cresce, o problema também aumenta. O mais tópico deles:


  • Diminuindo a conectividade de um aplicativo de banco de dados
  • Registro de consulta no modo de depuração
  • Trabalhar com réplicas

O artigo é baseado no pacote github.com/adverax/echo/database/sql. A semântica do uso deste pacote é o mais próxima possível do pacote padrão de banco de dados / sql, então não acho que alguém tenha problemas para usá-lo.


Âmbito de aplicação


Como regra, os grandes sistemas tentam se conectar livremente com uma clara área de responsabilidade de cada componente do sistema. Portanto, os padrões de design de publicador / assinante são amplamente praticados. Considere um pequeno exemplo de registro de um novo usuário no sistema.


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: func(db *sql.DB, user *User) error { }, } err := manager.Signup(&User{...}) if err != nil { panic(err) } } 

Neste exemplo, estamos principalmente interessados ​​no evento OnSignup. Para simplificar, o manipulador é representado por uma única função (na vida real, tudo é mais complicado). Na assinatura do evento, prescrevemos rigidamente o tipo do primeiro parâmetro, que geralmente tem consequências de longo alcance.
Suponha que agora desejemos expandir a funcionalidade de nosso aplicativo e, no caso de um registro bem-sucedido do usuário, envie uma mensagem para sua conta pessoal. Idealmente, a mensagem deve ser colocada na mesma transação que o registro do usuário.


 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{ // ... OnSignup: func(db *sql.Tx, user *User) error { }, } err := manager.Signup(&User{...}) if err != nil { panic(err) } } 

Como você pode ver no exemplo, fomos forçados a alterar a assinatura do evento. Essa solução não é limpa e implica que os manipuladores tenham conhecimento do contexto da execução da consulta ao banco de dados. Uma solução muito mais limpa seria usar um banco de dados genérico e a interface da transação - escopo.


 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{ // ... OnSignup: func(scope sql.Scope, user *User) error { }, } err := manager.Signup(&User{...}) if err != nil { panic(err) } } 

Para implementar essa abordagem, precisaremos de suporte para transações aninhadas, pois o manipulador, por sua vez, pode usar transações. Felizmente, isso não é um problema, pois a maioria dos DBMSs oferece suporte ao mecanismo SAVEPOINT.


Banco de Dados e Contexto


Na prática normal, a conexão com o banco de dados não é passada como parâmetro, como mostrado acima, e cada gerente mantém um link para a conexão com o banco de dados. Isso simplifica as assinaturas de métodos e melhora a legibilidade do código. No nosso caso, é impossível evitar isso, pois é necessário um link para uma transação.
Uma solução bastante elegante é colocar o link para a transação (escopo) no contexto, porque o contexto é posicionado como um parâmetro de ponta a ponta. Em seguida, podemos simplificar ainda mais nosso código:


 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) } } 

Este exemplo mostra que mantivemos o isolamento completo dos gerentes, aumentamos a legibilidade do código e alcançamos o trabalho conjunto em um único escopo.


Suporte de replicação


A biblioteca também suporta o uso de replicação. Todas as solicitações do tipo Exec são enviadas ao mestre. Solicitações do tipo Escravo são transferidas para um Escravo selecionado aleatoriamente. Para oferecer suporte à replicação, basta especificar várias fontes de dados:


 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() ... } 

Se você usar uma única fonte de dados ao abrir um banco de dados, ela será aberta no modo normal sem sobrecarga adicional.


Métricas


Como você sabe, as métricas são baratas e os logs, caros. Portanto, foi decidido adicionar suporte para as métricas padrão.


Perfil e registro de consulta


É muito necessário registrar consultas no banco de dados durante a depuração. No entanto, eu não vi um mecanismo de registro de alta qualidade com zero de sobrecarga na produção. A biblioteca permite que você resolva esse problema com elegância, agrupando o banco de dados. Para criar um perfil do banco de dados, basta passar o ativador apropriado para ele:


 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() ... } 

Conclusão


O pacote proposto permite expandir as possibilidades de interação com o banco de dados, ocultando detalhes desnecessários. Isso permite melhorar a qualidade do código, deixando-o pouco conectado e transparente, apesar da crescente complexidade do aplicativo.

Source: https://habr.com/ru/post/pt445908/


All Articles