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{
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{
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{
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.