Golang与数据库交互的演变

在本文中,我想总结一下使用运行golang的数据库的问题。 解决简单问题时,这些问题通常不可见。 随着项目的发展,问题也随之而来。 其中最热门的:


  • 降低数据库应用程序的连接性
  • 调试模式查询日志
  • 使用副本

本文基于github.com/adverax/echo/database/sql软件包。 使用此程序包的语义尽可能接近标准数据库/ sql程序包,因此我认为任何人都不会在使用它时遇到问题。


适用范围


通常,大型系统会尝试使系统各组成部分的职责明确区域松散地连接在一起。 因此,发布者/订户设计模式被广泛实践。 考虑一个在系统中注册新用户的小例子。


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

在此示例中,我们主要对OnSignup事件感兴趣。 为简化起见,处理程序由单个函数表示(在现实生活中,所有过程都更加复杂)。 在事件签名中,我们严格规定了第一个参数的类型,这通常会产生深远的影响。
假设现在我们要扩展应用程序的功能,并在用户成功注册的情况下向他的个人帐户发送一条消息。 理想情况下,该消息应与用户注册放在同一笔交易中。


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

从示例中可以看到,我们被迫更改事件的签名。 该解决方案不是干净的,它暗示处理程序具有数据库查询执行上下文的知识。 一个更干净的解决方案是使用通用数据库和事务接口-作用域。


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

要实现此方法,我们需要对嵌套事务的支持,因为处理程序可以使用事务。 幸运的是,这不是问题,因为大多数DBMS支持SAVEPOINT机制。


数据库和上下文


在正常实践中,如上所述,与数据库的连接不作为参数传递,并且每个管理器都保留与数据库的连接的链接。 这简化了方法签名并提高了代码的可读性。 在我们的情况下,无法避免这种情况,因为您需要将链接转移到事务。
一个相当不错的解决方案是将链接链接到上下文中的事务(作用域),因为上下文被定位为传递参数。 然后,我们可以进一步简化代码:


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

此示例表明,我们保持了管理者的完全隔离,提高了代码的可读性,并在单个范围内完成了他们的联合工作。


复制支持


该库还支持使用复制。 所有Exec类型的请求都发送给Master。 从设备类型的请求将传输到随机选择的从设备。 要支持复制,只需指定几个数据源:


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

如果在打开数据库时使用单个数据源,它将以正常模式打开而不会产生额外的开销。


指标


如您所知,指标很便宜,日志很昂贵。 因此,决定增加对默认指标的支持。


查询分析和记录


在调试期间记录数据库查询是非常必要的。 但是,我还没有看到高质量的日志记录机制,其生产开销为零。 该库使您可以通过包装数据库来优雅地解决此问题。 要分析数据库,只需将适当的激活器传递给它:


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

结论


建议的程序包使您可以扩展与数据库交互的可能性,同时隐藏不必要的细节。 尽管应用程序的复杂性不断提高,这仍使您可以提高代码质量,使其保持松散连接和透明状态。

Source: https://habr.com/ru/post/zh-CN445908/


All Articles