En este artículo quiero resumir los problemas de trabajar con bases de datos que ejecutan Golang. Al resolver problemas simples, estos problemas generalmente no son visibles. A medida que el proyecto crece, también lo hace el problema. El más actual de ellos:
- Disminución de la conectividad de una aplicación de base de datos
- Registro de consultas en modo de depuración
- Trabaja con réplicas
El artículo se basa en el paquete github.com/adverax/echo/database/sql. La semántica de usar este paquete es lo más cercana posible al paquete estándar de base de datos / sql, por lo que no creo que nadie tenga problemas para usarlo.
Alcance
Como regla general, los sistemas grandes intentan conectarse libremente con un área clara de responsabilidad de cada componente del sistema. Por lo tanto, los patrones de diseño de editor / suscriptor se practican ampliamente. Considere un pequeño ejemplo de registrar un nuevo usuario en el 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{
En este ejemplo, estamos principalmente interesados en el evento OnSignup. Para simplificar, el controlador está representado por una sola función (en la vida real, todo es más complicado). En la firma del evento, prescribimos rígidamente el tipo del primer parámetro, que generalmente tiene consecuencias de largo alcance.
Supongamos que ahora queremos expandir la funcionalidad de nuestra aplicación y, en caso de un registro de usuario exitoso, enviar un mensaje a su cuenta personal. Idealmente, el mensaje debe colocarse en la misma transacción que el registro del usuario.
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 puede ver en el ejemplo, nos vimos obligados a cambiar la firma del evento. Esta solución no es limpia e implica que los manejadores tienen conocimiento del contexto de la ejecución de la consulta de la base de datos. Una solución mucho más limpia sería utilizar una base de datos genérica y una interfaz de transacción: alcance.
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 este enfoque, necesitaremos soporte para transacciones anidadas, ya que el controlador, a su vez, puede usar transacciones. Afortunadamente, esto no es un problema, ya que la mayoría de los DBMS admiten el mecanismo SAVEPOINT.
Base de datos y contexto
En la práctica normal, la conexión a la base de datos no se pasa como un parámetro, como se muestra arriba, y cada administrador mantiene un enlace a la conexión a la base de datos. Esto simplifica las firmas de métodos y mejora la legibilidad del código. En nuestro caso, es imposible evitar esto, porque necesita transferir un enlace a la transacción.
Una solución bastante elegante es poner el enlace a la transacción (alcance) en el contexto, porque el contexto se posiciona como un parámetro de paso. Entonces podemos simplificar aún más nuestro 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 ejemplo muestra que hemos mantenido el aislamiento completo de los gerentes, aumentado la legibilidad del código y logrado su trabajo conjunto en un solo alcance.
Soporte de replicación
La biblioteca también admite el uso de la replicación. Todas las solicitudes de tipo Exec se envían a Master. Las solicitudes del tipo Esclavo se transfieren a un Esclavo seleccionado al azar. Para admitir la replicación, solo especifique varias fuentes de datos:
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 utiliza una única fuente de datos al abrir una base de datos, se abrirá en modo normal sin sobrecarga adicional.
Métricas
Como sabe, las métricas son baratas y los registros son caros. Por lo tanto, se decidió agregar soporte para las métricas predeterminadas.
Consulta de perfiles y registro
Es muy necesario registrar las consultas de la base de datos durante la depuración. Sin embargo, no he visto un mecanismo de registro de alta calidad con cero gastos generales en la producción. La biblioteca le permite resolver este problema con elegancia envolviendo la base de datos. Para crear un perfil de la base de datos, simplemente páselo:
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() ... }
Conclusión
El paquete propuesto le permite ampliar las posibilidades de interacción con la base de datos, mientras oculta detalles innecesarios. Esto le permite mejorar la calidad del código, dejándolo débilmente conectado y transparente, a pesar de la creciente complejidad de la aplicación.