由网络副手--寻路人于2022.06.17 12:50:00发布在Go语言 GO 语言 Gorm V1 升级为V2 过程纪录 阅读2051 评论0 喜欢1 ## 一、背景 由于公司之前用了Gorm V1 版本,目前遇到最大的瓶颈,使用场景中,几个痛点还是比较明显 1. 不支持读写分离,数据库单实例流量瓶颈无法解决 2. 不支持批量插入、更新 3. 以前的Update 变更为了 Updates,不支持非零更新 4. Save 支持非0更新 解决方案: 1. 解决上 问题1, 如果快速方式可以通过数据库的代理是实现,阿里云存在数据代理,能快速解决,但是成本比较高 2. 问题2 的解决就比较麻烦,需要单独再造轮子。 最终方案 升级Gorm V1 ---> Gorm V2 ## 二、系统升级 > ### ①、梳理影响点 >> 数据实例化方式,Gorm V2 采用读写分离、负载调度、8库连接池化处理,自然此处要变更。 老版本就不帖了,下面直接帖一下新版本V2 的实例化代码 >> 1. Scan 列表查询,显示的为空和返回的结构变了 >> `老版本代码 var res []*models.Materials err := s.db.Model(&models.Materials{}).Where("(bind = 0 AND type = 2) OR id = ?", id).Scan(&res).Error >> 新版本代码 var res []*models.Materials err := s.db.Model(&models.Materials{}).Where("(bind = 0 AND type = 2) OR id = ?", id).Scan(&res).Error if len(res) < 1 && err == nil { return make([]*models.Materials, 0, 0), nil //数据为空,通过此种处理才和V1 版本相同,v2 版本返回的是 nil } >> ` >> 2. Find 查询结果变了, V1 数据为空 返回 gorm.ErrRecordNotFound V2 返回 nil >> `老版本代码 var res *models.Materials err := s.db.Model(&models.Materials{}).Where("(bind = 0 AND type = 2) OR id = ?", id).Find(&res).Error if err == gorm.ErrRecordNotFound { return nil, nil } >> 新版本代码 var res *models.Materials err := s.db.Model(&models.Materials{}).Where("(bind = 0 AND type = 2) OR id = ?", id).Find(&res).Error if res.Id < 1 && err == nil { return nil, nil //数据为空,通过此种处理才和V1 版本相同,v2 版本 Find err 返回为 nil } >> ` >> 3. Count V2 版本默认为 int64, V1 为 int, V1 数据为空 返回 gorm.ErrRecordNotFound V2 返回 nil >> `老版本代码 var count int64 err := s.db.Table("simple_log"). Where("sid = (?) ).Count(&count).Error if err == gorm.ErrRecordNotFound { return 0, nil } return int(count), err >> 新版本代码 var count int64 err := s.db.Table("simple_log"). Where("sid = (?) ).Count(&count).Error if err == nil && count < 1 { return 0, nil } return int(count), err >> ` > ### ②、涉及所有代码改造 >> 哈哈~,基本系统改了一个遍,更改后。 建议大家变更前后,新老系统的返回结果,可以编写个shell 针对结果md5 对比,查漏补缺,最终统计下来,文件更改88个文件 165 处修改 ## 三、使用记录 > ### V1 版本文档 https://v1.gorm.io/zh_CN/docs/query.html > ### V2 版本文档 https://gorm.io/docs/ > ### 程序使用V2 组件代码 >> 1. 数据库使用Nacos 配置,配置的格式如下,采用Yaml 方式解析。 如果有伙伴问为啥没有在配置文件把数据库连接处理成Dsn格式,那样也可以。 本文之所以保留此种场景,考虑是不影响V1的配置文件,保证V1可用的前提下,调试V2,而且格式清晰 ``` database: ip: rw-.mysql.rds.aliyuncs.com port: 3306 database: database user: username password: password params: charset=utf8mb4&parseTime=True&loc=Local&timeout=10s #2 Error 3 Warn 4 Info lof_mode_v2: 4 max_connections: 300 max_open_conns: 100 max_idle_conns: 50 #只读实例 slave_array : - ip: rr.mysql.rds.aliyuncs.com port: 3306 database: database user: username password: password params: charset=utf8mb4&parseTime=True&loc=Local&timeout=10s ``` >> 2. 数据库连接 >>> ``` import ( "fmt" "gopkg.in/yaml.v2" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" "gorm.io/gorm/schema" "gorm.io/plugin/dbresolver" "log" "sync" "time" ) var storeInterface dao.IStore var storeOnce sync.Once type Store struct { db *gorm.DB } func newStore(db *gorm.DB) *Store { return &Store{ db: db, } } func StoreInstance(dbByte []byte) dao.IStore { storeOnce.Do(func() { c := new(models.DB) err := yaml.Unmarshal(dbByte, c) if err != nil { panic(err) } dnsStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", c.User, c.Password, c.Ip, c.Port, c.Database, c.Params) db1Dsn := mysql.New(mysql.Config{ DSN: dnsStr, // DSN data source name DefaultStringSize: 256, // string 类型字段的默认长度 DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 DontSupportRenameColumn: false, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 }) db, err := gorm.Open(db1Dsn, &gorm.Config{ NamingStrategy: schema.NamingStrategy{ TablePrefix: "", SingularTable: true, }, Logger: logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), logger.Config{ SlowThreshold: 500 * time.Millisecond, Colorful: true, IgnoreRecordNotFoundError: true, LogLevel: logger.LogLevel(c.LogModeV2), }), }) if err != nil { panic(err) } //判断是否存在只读实例 if len(c.SlaveArray) > 0 { replicas := make([]gorm.Dialector, 0, len(c.SlaveArray)) for _, v := range c.SlaveArray { dnsSlaveStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s", v.User, v.Password, v.Ip, v.Port, v.Database, v.Params) dbSlaveDsn := mysql.New(mysql.Config{ DSN: dnsSlaveStr, // DSN data source name DefaultStringSize: 256, // string 类型字段的默认长度 DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 DontSupportRenameColumn: false, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 }) replicas = append(replicas, dbSlaveDsn) } err = db.Use( dbresolver.Register(dbresolver.Config{ //Sources: []gorm.Dialector{mysql.Open(dnsStr)}, Replicas: replicas, // sources/replicas 负载均衡策略 Policy: dbresolver.RandomPolicy{}, }). SetConnMaxIdleTime(time.Hour). SetConnMaxLifetime(time.Hour). SetMaxOpenConns(c.MaxOpenConns). SetMaxIdleConns(c.MaxIdleConns), ) } if err != nil { panic(err) } storeInterface = newStore(db) }) return storeInterface } func (s *Store) StoreClone() dao.IStore { return newStore(s.db) } func (s *Store) BeginTx() (dao.IStore, error) { // begin自动开启一个新的数据库连接,必须用begin后的*DB连接(而不是原连接s本身)回滚或提交 // 即使是新连接也不能事务套事务 d := s.db.Begin() return newStore(d), d.Error } func (s *Store) Rollback() error { return s.db.Rollback().Error } func (s *Store) Commit() error { return s.db.Commit().Error } func (s *Store) Close() { //现在不需要关闭了 } ``` >> 3. 数据库交互看文档即可啦 赞 1 分享 赏 您可以选择一种方式赞助本站 支付宝扫码赞助 BraveDu 署名: 网络副手~寻路人