gorm详解

lilei · · 402 次点击 · 开始浏览    置顶
这是一个创建于 的主题,其中的信息可能已经有所发展或是发生改变。

1、gorm的常用用法。自定义类型。clause 2、gorm+gen 提高crud的速度 3、trace链路追踪。 4、gorm集群和分库分表。 5、日志。 6、gorm.DB链式操作需要注意的地方 https://gorm.io/zh_CN/docs/index.html gorm提供的文档非常齐全了,总结一下在工作中的一些实践,如果对您有帮我,您的star就是对我的鼓励https://github.com/luxun9527/go-lib 如有任何问题也欢迎留言。 **安装mysql** ```powershell docker run -p 33606:3306 --name mysql8 --restart always \ -e MYSQL_ROOT_PASSWORD=root \ -v /root/docker/mysql/conf/my.cnf:/etc/mysql/my.cnf \ -v /root/docker/mysql/data:/var/lib/mysql \ -d mysql:8.0 ``` ## 1、gorm的常用用法,curd,自定义类型,clause子句。 ### 新增 ```go func TestCreate(t *testing.T) { user := &User{ Username: "", Age: 0, Fav: "", CreatedAt: 0, UpdatedAt: 0, } //INSERT INTO `user` (`username`,`age`,`fav`,`created_at`,`updated_at`) VALUES ('',0,'',1692947238,1692947238) if err := db.Create(user).Error; err != nil { log.Panicf("create no select err %v", err) } log.Printf("user1 = %+v", user) //INSERT INTO `user` (`fav`,`created_at`,`updated_at`) VALUES ('',1692947405,1692947405) if err := db.Select("fav").Create(user).Error; err != nil { log.Panicf("create select field err %v", err) } log.Printf("user2 = %+v", user) //INSERT INTO `user` (`username`,`age`,`created_at`,`updated_at`,`id`) VALUES ('',0,1692947813,1692947813,14) if err := db.Omit("fav").Create(user).Error; err != nil { log.Panicf("create Omit field err %v", err) } log.Printf("user2 = %+v", user) //https://gorm.io/zh_CN/docs/models.html https://gorm.io/zh_CN/docs/create.html#%E9%BB%98%E8%AE%A4%E5%80%BC //官方的文档很全主要是验证一些模糊的地方 //1、不使用select指定,零值字段也会被插入。使用select只会插入指定的字段。可以使用tag指定默认值 `gorm:"default:18"` //2、created_at updated_at会被填充当前时间插入。 https://gorm.io/zh_CN/docs/models.html#%E5%88%9B%E5%BB%BA-x2F-%E6%9B%B4%E6%96%B0%E6%97%B6%E9%97%B4%E8%BF%BD%E8%B8%AA%EF%BC%88%E7%BA%B3%E7%A7%92%E3%80%81%E6%AF%AB%E7%A7%92%E3%80%81%E7%A7%92%E3%80%81Time%EF%BC%89 //3、插入后会将主键赋值回来,使用select的方式插入不会赋值回来。 } 2024/04/21 23:24:21 E:/demoproject/go-lib/sdk/gorm/curd_test.go:74 [3.177ms] [rows:1] INSERT INTO `user` (`username`,`age`,`fav`,`created_at`,`updated_at`,`deleted_at`) VALUES ('',0,'',1713713061,1713713061,'0') 2024/04/21 23:24:21 user1 = &{ID:7 Username: Age:0 Fav: CreatedAt:1713713061 UpdatedAt:1713713061 DeletedAt:0} 2024/04/21 23:24:21 E:/demoproject/go-lib/sdk/gorm/curd_test.go:79 [3.227ms] [rows:1] INSERT INTO `user` (`fav`,`created_at`,`updated_at`) VALUES ('',1713713061,1713713061) 2024/04/21 23:24:21 user2 = &{ID:7 Username: Age:0 Fav: CreatedAt:1713713061 UpdatedAt:1713713061 DeletedAt:0} 2024/04/21 23:24:21 E:/demoproject/go-lib/sdk/gorm/curd_test.go:84 Error 1062 (23000): Duplicate entry '7' for key 'user.PRIMARY' [0.534ms] [rows:0] INSERT INTO `user` (`username`,`age`,`created_at`,`updated_at`,`deleted_at`,`id`) VALUES ('',0,1713713061,1713713061,'0',7) 2024/04/21 23:24:21 create Omit field err Error 1062 (23000): Duplicate entry '7' for key 'user.PRIMARY' --- FAIL: TestCreate (0.01s) panic: create Omit field err Error 1062 (23000): Duplicate entry '7' for key 'user.PRIMARY' [recovered] panic: create Omit field err Error 1062 (23000): Duplicate entry '7' for key 'user.PRIMARY' ``` ### 修改 ```go func TestUpdate(t *testing.T) { u := &User{ID: 1} //更新单个列 只有非空的字段会被修改 //UPDATE `user` SET `username`='hello',`updated_at`=1692948530 WHERE `id` = 1 db.Model(&u).Update("username", "hello") //更新多个列,只有非空的字段会被修改 // UPDATE `user` SET `username`='hello12',`updated_at`=1692948530 WHERE `id` = 1 db.Model(&u).Updates(&User{Username: "hello12"}) //select 更新指定了列,不会忽略零值 // UPDATE `user` SET `username`='',`updated_at`=1692948530 WHERE `id` = 1 db.Model(&u).Select("username").Updates(&User{ Username: "", Fav: "", }) //忽略指定的列,会忽略空值。 //UPDATE `user` SET `updated_at`=1695796004 WHERE `id` = 1 db.Model(&u).Omit("fav").Updates(&User{ Username: "", Fav: "1", }) //UPDATE `user` SET `id`=0,`username`='',`age`=0,`created_at`=0,`updated_at`=1695796443,`deleted_at`=0 WHERE `id` = 1 //select 所有再忽略指定的列,不会忽略零值 db.Select("*").Omit("fav").Updates(&User{ ID: 1, Username: "", Fav: "1", }) //指定条件更新,会忽略空值 // UPDATE `user` SET `id`=1,`username`='1',`updated_at`=1692948928 WHERE username = 'admin' AND `id` = 1 db.Where("username = ?", "admin").Updates(User{Username: "1", ID: 1}) //默认没有指定条件不会全局更新。db.Model(&User{}).Update("name", "jinzhu").Error gorm.ErrMissingWhereClause //如果要更新零字段,要使用map或者select指定字段。 select 所有不会忽略零值 // UPDATE `user` SET `id`=1,`username`='',`age`=0,`fav`='',`created_at`=0,`updated_at`=1692948946 WHERE `id` = 1 db.Select("*").Updates(User{ ID: 1, Username: "", Age: 0, Fav: "", CreatedAt: 0, UpdatedAt: 0, }) } ``` ### 删除 ```go func TestDelete(t *testing.T) { //UPDATE `user` SET `deleted_at`=1713714861 WHERE id = 1 AND `user`.`deleted_at` = 0 db.Where("id = ?", 3).Delete(&User{}) //DELETE FROM `user` WHERE id = 3 db.Unscoped().Where("id = ?", 3).Delete(&User{}) } ``` ### 查询 ```go func TestSelect(t *testing.T) { //普通的查询,官方文档上写的比较全,主要是显示预加载。 //========================================一对一 一个用户有一个profile====================== var users1 []*User if err := db.Model(&User{}).Where("id = ?", 6).Preload("Profile").Find(&users1).Error; err != nil { log.Println(err) } //SELECT * FROM `card` WHERE `card`.`user_id` = 6 //SELECT * FROM `user` WHERE id = 6 //SELECT * FROM `profile` WHERE `profile`.`user_id` = 6 //===================================================一对多,一个用户有多个cards================================= var users2 []*User if err := db.Model(&User{}).Where("id = ?", 6).Preload("Cards").Preload("Profile").Find(&users2).Error; err != nil { log.Println(err) } for _, v := range users2 { log.Printf("%+v", v) } /* [0.529ms] [rows:0] SELECT * FROM `card` WHERE `card`.`user_id` = 6 2024/04/23 22:57:55 E:/demoproject/go-lib/sdk/gorm/curd_test.go:215 [1.098ms] [rows:0] SELECT * FROM `profile` WHERE `profile`.`user_id` = 6 2024/04/23 22:57:55 E:/demoproject/go-lib/sdk/gorm/curd_test.go:215 [3.268ms] [rows:1] SELECT * FROM `user` WHERE id = 6 AND `user`.`deleted_at` = 0 2024/04/23 22:57:55 &{ID:6 Username: Age:0 Fav: CompanyID:1 CreatedAt:1713713017 UpdatedAt:1713713017 Cards:[] Profile:{ID:0 UserID:0 Nickname: Desc: CreatedAt:0 UpdatedAt:0 DeletedAt:0} Company:{ID:0 Name:} DeletedAt:0} */ //===============================belong to 一个用户属于一家公司===================== var ( user User company Company ) //查用户所属的公司。 //SELECT * FROM `user` WHERE id = 1 AND `user`.`deleted_at` = 0 LIMIT 1 db.Where("id = ?", 6).Take(&user) //SELECT * FROM `company` WHERE `company`.`id` = 1 if err := db.Debug().Model(&user).Association("Company").Find(&company); err != nil { log.Println("belong to error", err) } user.Company = company log.Printf("user =%+v", user) //user ={ID:6 Username: Age:0 Fav: CompanyID:0 CreatedAt:1713713017 UpdatedAt:1713713017 Company:{ID:1 Name:test} DeletedAt:0} //=====================================join 预加载========================================================== printLine("join 预加载") var u User if err := db.Joins("Company").Take(&u, 6).Error; err != nil { log.Panicf("join error %v", err) } //SELECT `user`.`id`,`user`.`username`,`user`.`age`,`user`.`fav`,`user`.`company_id`,`user`.`created_at`,`user`.`updated_at`,`user`.`deleted_at`, //`Company`.`id` AS `Company__id`,`Company`.`name` AS `Company__name` FROM `user` //LEFT JOIN `company` `Company` ON `user`.`company_id` = `Company`.`id` WHERE `user`.`id` = 4 AND `user`.`deleted_at` = 0 LIMIT 1 log.Printf("join u =%+v", u) } ``` ### 自定义类型 https://github.com/go-gorm/datatypes https://gorm.io/zh_CN/docs/data_types.html# 自定义的数据类型必须实现 [Scanner](https://pkg.go.dev/database/sql#Scanner) 和 [Valuer](https://pkg.go.dev/database/sql/driver#Valuer) 接口,以便让 GORM 知道如何将该类型接收、保存到数据库https://github.com/go-gorm/datatypes/blob/master/date.go 示例 其他高级的用法参考https://gorm.io/zh_CN/docs/data_types.html# ```go package main import ( "database/sql" "database/sql/driver" "testing" "time" ) type CustomDateModel struct { ID int32 `gorm:"column:id;not null;" json:"user_id"` CustomDate Date `gorm:"column:custom_date;not null;" json:"custom_date"` } // TableName Card's table name func (*CustomDateModel) TableName() string { return "custom_date" } func TestCustomType(t *testing.T) { db.Create(&CustomDateModel{CustomDate: Date(time.Now())}) //INSERT INTO `custom_date` (`custom_date`) VALUES ('2024-05-05 00:00:00') } type Date time.Time //将数据库中的数据转换为Date类型 func (date *Date) Scan(value interface{}) (err error) { nullTime := &sql.NullTime{} err = nullTime.Scan(value) *date = Date(nullTime.Time) return } //将Date类型转换为数据库中的数据 func (date Date) Value() (driver.Value, error) { y, m, d := time.Time(date).Date() return time.Date(y, m, d, 0, 0, 0, 0, time.Time(date).Location()), nil } // GormDataType gorm common data type func (date Date) GormDataType() string { return "date" } func (date Date) GobEncode() ([]byte, error) { return time.Time(date).GobEncode() } func (date *Date) GobDecode(b []byte) error { return (*time.Time)(date).GobDecode(b) } func (date Date) MarshalJSON() ([]byte, error) { return time.Time(date).MarshalJSON() } func (date *Date) UnmarshalJSON(b []byte) error { return (*time.Time)(date).UnmarshalJSON(b) } ``` ### clause子句 https://blog.csdn.net/dorlolo/article/details/127238144 [https://gorm.io/zh_CN/docs/create.html#Upsert-%E5%8F%8A%E5%86%B2%E7%AA%81](https://gorm.io/zh_CN/docs/create.html#Upsert-及冲突) gorm与子句生成器有关的类,按父级到子集排列为 [DB](https://github.com/go-gorm/gorm/blob/master/gorm.go#L89) **-->** [Statement](https://github.com/go-gorm/gorm/blob/master/statement.go) **-->** [Clause](https://github.com/go-gorm/gorm/blob/master/clause/clause.go) **-->** [Expression](https://github.com/go-gorm/gorm/blob/master/clause/expression.go) (分别对应 数据库连接对象–> 语句 --> 子句 --> 表达式),它们都是以属性形式保存在父类中。只要知道这个结构,看源码就会轻松很多。 ![img](https://cdn.nlark.com/yuque/0/2024/png/12466223/1714919325706-db595e7b-bd0e-407e-9b48-7eab3a2f9a0d.png) ```go user := &User{} db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user) //INSERT INTO `user` (`username`,`age`,`fav`,`company_id`,`created_at`,`updated_at`,`deleted_at`) VALUES ('',0,'',0,1714919066,1714919066,0) ON DUPLICATE KEY UPDATE `id`=`id` ``` **2. 为单个字段实现开发构造器** 2.1 实现方法 只要自定义的模型字段中包含以下的其中一种方法即可, ```go CreateClauses(*Field) []clause.Interface QueryClauses(*Field) []clause.Interface UpdateClauses(*Field) []clause.Interface DeleteClauses(*Field) []clause.Interface ``` 这个返回值类型可以实现StatementModifier接口 或者 clause.Interface接口都行。使用实例。 ```go package main import ( "go-lib/sdk/gorm/gen/dao/model" "gorm.io/gorm" "gorm.io/gorm/clause" "gorm.io/gorm/schema" "testing" ) type ClauseUser struct { model.User C CustomClause } func TestClauses(t *testing.T) { user := &User{} db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user) //INSERT INTO `user` (`username`,`age`,`fav`,`company_id`,`created_at`,`updated_at`,`deleted_at`) VALUES ('',0,'',0,1714919066,1714919066,0) ON DUPLICATE KEY UPDATE `id`=`id` db.Clauses(clause.Eq{ Column: "id", Value: 1, }).Find(&user) //SELECT * FROM `user` WHERE `id` = 1 AND `user`.`deleted_at` = 0 AND `user`.`id` = 19 db.Find(&ClauseUser{}) //SELECT * FROM `user` WHERE `user`.`c` IS NULL } type CustomClause int32 func (CustomClause) QueryClauses(f *schema.Field) []clause.Interface { return []clause.Interface{CustomClauseQuery{Field: f}} } type CustomClauseQuery struct { Field *schema.Field } func (sd CustomClauseQuery) Name() string { return "" } func (sd CustomClauseQuery) Build(builder clause.Builder) { } func (sd CustomClauseQuery) MergeClause(c *clause.Clause) { } func (sd CustomClauseQuery) ModifyStatement(stmt *gorm.Statement) { stmt.AddClause(clause.Where{Exprs: []clause.Expression{ clause.Eq{Column: clause.Column{Table: clause.CurrentTable, Name: sd.Field.DBName}, Value: nil}, }}) } ``` 详情用法可以参考https://github.com/go-gorm/soft_delete 软删除的做法。 ## 2、gorm + gen https://gorm.io/zh_CN/gen/dao.html https://gorm.io/zh_CN/gen/database_to_structs.html 相对于gorm gen提供对表的curd进一步的封装,能提高我们的开发效率,具体有如下优势。 **1、直接使用表结构生成model,不用自己手写model** ``` go install gorm.io/gen/tools/gentool@latest gentool --dsn="root:root@tcp(192.168.2.200:33606)/test?charset=utf8mb4&parseTime=True&loc=Local" --db=mysql -outPath=gen/dao/query -fieldSignable=true ``` **如果有定制需求可以参考**。https://gorm.io/zh_CN/gen/database_to_structs.html **有一些坑** 1、https://github.com/go-gorm/gen/issues/755自定义方法CommonMethod 需要和生成代码不在同一个包 [https://gorm.io/zh_CN/gen/database_to_structs.html#%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95](https://gorm.io/zh_CN/gen/database_to_structs.html#模板方法) ```go // Code generated by gorm.io/gen. DO NOT EDIT. // Code generated by gorm.io/gen. DO NOT EDIT. // Code generated by gorm.io/gen. DO NOT EDIT. package model const TableNameUser = "user" // User mapped from table <user> type User struct { ID int32 `gorm:"column:id;primaryKey;autoIncrement:true" json:"id"` Username string `gorm:"column:username;not null;comment:用户名" json:"username"` Age uint32 `gorm:"column:age;not null;comment:年龄" json:"age"` Fav string `gorm:"column:fav;not null;comment:爱好" json:"fav"` CompanyID int32 `gorm:"column:company_id;not null;comment:公司Id" json:"company_id"` CreatedAt uint64 `gorm:"column:created_at;not null;comment:创建时间" json:"created_at"` UpdatedAt uint64 `gorm:"column:updated_at;not null;comment:修改时间" json:"updated_at"` DeletedAt uint64 `gorm:"column:deleted_at;not null" json:"deleted_at"` } // TableName User's table name func (*User) TableName() string { return TableNameUser } ``` **2、使用变量名而不是直接表字段名,避免写错字段。** ```go gen ====> users, err := u.WithContext(ctx).Where(u.Name.Neq("modi")).Find() gorm====> db.Where("name=?","modi").Find() // SELECT * FROM users WHERE name <> 'modi'; ``` **3、对查询的进一步封装,在查询上更加方便。** ```go dao := query.Use(db) //card := dao.Card user := dao.User company := dao.Company ctx := context.Background() _, err := user.WithContext(ctx).Find() if err != nil { log.Panicf("err:%v", err) } var u []*User if db.Find(&u).Error != nil { log.Panicf("err:%v", err) } //gen join dao.User.WithContext(ctx).Join(company, user.CompanyID.EqCol(company.ID)).Find() //gorm join db.Joins("LEFT JOIN Company c ON c.id = u.company_id").Find(&u) // gen分页 dao.User.WithContext(ctx).FindByPage(0, 10) //gorm 分页 db.Offset(0).Limit(10).Find(&u) // gen子查询 subQuery := company.WithContext(ctx). Select(company.ID) // _, err = user.WithContext(ctx). Where(user.Columns(user.CompanyID).In(subQuery)). Find() //SELECT * FROM `user` WHERE `user`.`company_id` IN (SELECT `company`.`id` FROM `company`) ``` **需要注意的的是:** https://github.com/go-gorm/gen/issues/900 gen的Select方法和Where都是不能自定义的,当进行一些复杂查询的时候,需要自定义的时候如使用json函数`db.Select("JSON_OBJECT(key1,val1,key2,val2...)")` gen是无法做到的。但是我们可以使用ast来生成对应的代码。具体可以参考。https://github.com/luxun9527/go-lib/blob/master/utils/ast_apply/add_rawselect_rawmethod.go ## 3、trace链路追踪 将gorm加入到链路追踪中。使用WithContext传递。 ```go package main import ( "context" "fmt" "github.com/uptrace/opentelemetry-go-extra/otelgorm" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/exporters/jaeger" "go.opentelemetry.io/otel/sdk/resource" tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.21.0" "log" "testing" ) const ( service = "trace-demo1" // 服务名 environment = "production" // 环境 id = 1 // id ) func tracerProvider(url string) (*tracesdk.TracerProvider, error) { // Create the Jaeger exporter // 创建 Jaeger exporter exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint(url))) if err != nil { return nil, err } tp := tracesdk.NewTracerProvider( // Always be sure to batch in production. tracesdk.WithBatcher(exp), // Record information about this application in a Resource. tracesdk.WithResource(resource.NewWithAttributes( semconv.SchemaURL, semconv.ServiceNameKey.String(service), attribute.String("environment", environment), attribute.Int64("ID", id), )), ) return tp, nil } func TestGormTrace(t *testing.T) { ctx := context.Background() tp, err := tracerProvider("http://192.168.11.185:14268/api/traces") if err != nil { log.Fatal(err) } if err := db.Use(otelgorm.NewPlugin( otelgorm.WithDBName("test"), otelgorm.WithTracerProvider(tp), )); err != nil { log.Fatal(err) } otel.SetTracerProvider(tp) tracer := otel.Tracer("gormtracer") ctx, span := tracer.Start(ctx, "gormtest") defer span.End() user := &User{ Username: "test1", Age: 1, Fav: "1", } //INSERT INTO `user` (`username`,`age`,`fav`,`created_at`,`updated_at`) VALUES ('',0,'',1692947238,1692947238) if err := db.WithContext(ctx).Create(user).Error; err != nil { fmt.Println("create err", err) } //otelplay.PrintTraceID(ctx) tp.Shutdown(ctx) } ``` ![img](https://cdn.nlark.com/yuque/0/2024/png/12466223/1714832378749-287c1857-8b4f-46a5-9ee3-335f988ab481.png) ## 4、主从集群,分库分表 refer https://www.freesion.com/article/92701394177/ https://www.cnblogs.com/cao-lei/p/13603043.html 主从延迟 https://jishuin.proginn.com/p/763bfbd589f7 https://blog.csdn.net/JesseYoung/article/details/40585809 ### mysql使用docker安装mysql主从集群 进入Master库mysql客户端 `mysql -uroot -h127.0.0.1 -P33307 -proot`。 如果本地没有mysql客户端则进入容器中 `docker exec -it mysql-master /bin/bash `在容器中的端口为 3306 输入`show master status`查看Master状态: ![img](https://cdn.nlark.com/yuque/0/2022/png/12466223/1661091602934-b7fad82d-5d74-440c-85a5-49822c41913b.png) 记住File和Position,后面需要用到。此时一定不要操作Master库,否则将会引起Master状态的变化,File和Position字段也将会进行变化。 #### 4.2 slave配置 ```powershell docker run -p 33307:3306 --name mysql8s --restart always \ -e MYSQL_ROOT_PASSWORD=root \ -v /root/docker/mysql/slave/conf/my.cnf:/etc/mysql/my.cnf \ -v /root/docker/mysql/slave/data:/var/lib/mysql \ -d mysql:8.0 ``` 进入mysql客户端执行命令 ``` change master to master_host='192.168.2.200', master_user='root', master_password='root', master_port=33606, master_log_file='mysql-bin.000009', master_log_pos=814, master_connect_retry=30; ``` master_host :Master库的地址局域网的ip master_port:Master的端口号,指的是容器的端口号 master_user:用于数据同步的用户 master_password:用于同步的用户的密码 master_log_file:指定 Slave 从哪个日志文件开始复制数据,即上文中提到的 File 字段的值 master_log_pos:从哪个 Position 开始读,即上文中提到的 Position 字段的值 master_connect_retry:如果连接失败,重试的时间间隔,单位是秒,默认是60秒 2、 启动slave ``` start slave; ``` 3、查看slave ``` show slave status \G; ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/12466223/1661092118656-e79f4ede-b693-476c-98b0-1612688c40a7.png) ### 4.3 slave常见错误 1、日志设置错误![img](https://cdn.nlark.com/yuque/0/2022/png/12466223/1661092380180-bf21cc9b-29a1-4ad2-83b6-c9dc5526c8e4.png) ``` master_log_file='mysql-bin1.000004', master_log_pos=157 ``` ![img](https://cdn.nlark.com/yuque/0/2022/png/12466223/1661092437579-b3d0d006-ed30-4957-b956-927d84c03ab6.png) 检查日志是否和master的一致 2、master ip端口配置错误配置错误![img](https://cdn.nlark.com/yuque/0/2022/png/12466223/1661092499187-53369e2a-3979-4526-98f1-ba7a77b745ae.png) ### 其他命令 `stop slave; `停止slave `reset master;` 重置master ### 测试是否成功 ```plsql #创建数据库 create DATABASE test1; use test1; #创建数据表 CREATE TABLE IF NOT EXISTS `runoob_tbl`( `runoob_id` INT UNSIGNED AUTO_INCREMENT, `runoob_title` VARCHAR(100) NOT NULL, `runoob_author` VARCHAR(40) NOT NULL, `submission_date` DATE, PRIMARY KEY ( `runoob_id` ) ); #插入数据 INSERT INTO runoob_tbl (runoob_title, runoob_author, submission_date) VALUES("学习 PHP", "菜鸟教程", NOW()); docker exec -it mysql8s mysql -uroot -proot use test1; select * from runoob_tbl; ``` ### `gorm mysql主从集群` https://gorm.io/zh_CN/docs/dbresolver.html ```go package main import ( "github.com/spf13/cast" "github.com/zeromicro/go-zero/core/stringx" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/logger" "gorm.io/plugin/dbresolver" "log" "os" "testing" "time" ) const TableNameRunoobTbl = "runoob_tbl" // RunoobTbl mapped from table <runoob_tbl> type RunoobTbl struct { RunoobID int32 `gorm:"column:runoob_id;primaryKey;autoIncrement:true" json:"runoob_id"` RunoobTitle string `gorm:"column:runoob_title;not null" json:"runoob_title"` RunoobAuthor string `gorm:"column:runoob_author;not null" json:"runoob_author"` SubmissionDate time.Time `gorm:"column:submission_date" json:"submission_date"` } // TableName RunoobTbl's table name func (*RunoobTbl) TableName() string { return TableNameRunoobTbl } const ( masterDsn = "root:root@tcp(192.168.2.200:33606)/test1?charset=utf8mb4&parseTime=True&loc=Local" slaveDsn = "root:root@tcp(192.168.2.200:33307)/test1?charset=utf8mb4&parseTime=True&loc=Local" ) // gentool --dsn="root:root@tcp(192.168.2.99:33307)/test1?charset=utf8mb4&parseTime=True&loc=Local" --onlyModel=true --db=mysql --tables=runoob_tbl -outPath=./ -fieldMap="decimal:string;tinyint:int32;" func TestDbresolve(t *testing.T) { newLogger := logger.New( log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer logger.Config{ SlowThreshold: time.Second, // Slow SQL threshold LogLevel: logger.Info, // Log level IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger Colorful: true, // Disable color }, ) masterDB, err := gorm.Open(mysql.New(mysql.Config{ DSN: masterDsn, }), &gorm.Config{ Logger: newLogger, }) if err != nil { log.Fatal(err) } if err := masterDB.Use( dbresolver.Register(dbresolver.Config{ Sources: []gorm.Dialector{mysql.New(mysql.Config{ DSN: masterDsn, })}, Replicas: []gorm.Dialector{mysql.New(mysql.Config{ DSN: slaveDsn, })}, Policy: dbresolver.RandomPolicy{}, TraceResolverMode: true, }). SetMaxIdleConns(10). SetConnMaxLifetime(time.Hour). SetMaxOpenConns(200), ); err != nil { log.Fatal(err) } d := &RunoobTbl{ RunoobTitle: cast.ToString(time.Now().Unix()), RunoobAuthor: stringx.Randn(10), SubmissionDate: time.Now(), } masterDB.Create(d) if masterDB.Where("runoob_id=?", 2).Find(d).Error != nil { log.Println(err) } log.Println(d) /* 2024/05/05 21:48:01 E:/demoproject/go-lib/sdk/gorm/dbresolver_test.go:78 [9.653ms] [rows:1] [source] INSERT INTO `runoob_tbl` (`runoob_title`,`runoob_author`,`submission_date`) VALUES ('1714916881','XVG0Iay48N','2024-05-05 21:48:01.959') 2024/05/05 21:48:01 E:/demoproject/go-lib/sdk/gorm/dbresolver_test.go:80 [1.594ms] [rows:0] [replica] SELECT * FROM `runoob_tbl` WHERE runoob_id=2 AND `runoob_tbl`.`runoob_id` = 2 */ } ``` ### gorm 水平分表 https://gorm.io/zh_CN/docs/sharding.html 这个分表暂时不支持mysql如果mysql进行水平分表的话,要在业务代码中实现。 ## 5、日志 https://gorm.io/zh_CN/docs/logger.html 自定义日志需要实现这几个接口。 ```go type Interface interface { LogMode(LogLevel) Interface Info(context.Context, string, ...interface{}) Warn(context.Context, string, ...interface{}) Error(context.Context, string, ...interface{}) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) } ``` 在实际的开发中常常使的比较多的是 `**moul.io/zapgorm2**` 这个库 ## 6、gorm.DB链式操作需要注意的地方 **1、gorm 并发不安全。** https://juejin.cn/post/7134002645651439630 首先,我们需要先去理解几乎每个方法中都会调用的函数:tx = db.getInstance()。 ```go func (db *DB) getInstance() *DB { if db.clone > 0 { tx := &DB{Config: db.Config, Error: db.Error} if db.clone == 1 { // clone with new statement tx.Statement = &Statement{ DB: tx, ConnPool: db.Statement.ConnPool, Context: db.Statement.Context, Clauses: map[string]clause.Clause{}, Vars: make([]interface{}, 0, 8), } } else { // with clone statement tx.Statement = db.Statement.clone() tx.Statement.DB = tx } return tx } return db } ``` 将上述改写并简化一下,大概是这么个逻辑: ```go func (db *DB) getInstance() *DB { switch db.clone: case 0: return db case 1: return newStatement() // 一个全新的,空白的Statement case 2: return db.cloneStatement() // 将之前的Statement复制一份 } ``` 当clone=1时,这个*gorm.DB 实例总是并发安全的,因为它总是会返回一个全新的*gorm.DB 实例,不会对老*gorm.DB 实例有什么读写。 当clone=2时,这个*gorm.DB 实例也总是并发安全的,因为任何的 Chain Method 和 Finisher Method 都只会去读和复制当前*gorm.DB 实例的值,而不会修改,因此只会对这个*gorm.DB 实例并发读,那么当然是并发安全的。 当clone=0时,这个*gorm.DB 实例就**不并发安全**。 那clone字段分别会在什么情况下等于0、1、2呢? - **在使用gorm.Open()之后,新建出来的\*gorm.DB 实例clone字段总是1。** - 在调用(*gorm.Gorm).Session()时,如果Session{}.NewDB为false,则为返回的*gorm.DB 实例clone字段是2,如果为true,则为1。 - 在调用(*gorm.Gorm).Session()时,如果Session{}.Initialized为true,则返回的*gorm.DB 实例clone字段是0。这条规则优先级高于Session.NewDB。 - **在调用了任意Chain Method、Finisher Method之后,返回的Gorm对象clone字段是0。** 在日常的开发中,我们喜欢将db定义为一个全局变量,**要注意的是绝对不要将一个db.clone=0的db赋值回全局变量下面是一个错误用法。** ```go var users1 []*User //初始的db.clone为1 执行where db.clone为0 db = db.Where("id = ?", 6) db.Where("name = ?", "lisi").Find(&users1) ``` **下面也是链式操作不注意db.clone造成的问题。** https://gorm.io/zh_CN/docs/method_chaining.html#Reusability-and-Safety Reusability and Safety A critical aspect of GORM is understanding when a *gorm.DB instance is safe to reuse. Following a Chain Method or Finisher Method, GORM returns an initialized *gorm.DB instance. This instance is not safe for reuse as it may carry over conditions from previous operations, potentially leading to contaminated SQL queries. For example: ### Example of Unsafe Reuse ``` queryDB := DB.Where("name = ?", "jinzhu") // First query queryDB.Where("age > ?", 10).First(&user) // SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10 // Second query with unintended compounded condition queryDB.Where("age > ?", 20).First(&user2) // SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10 AND age > 20 ``` ### Example of Safe Reuse To safely reuse a *gorm.DB instance, use a New Session Method: ``` queryDB := DB.Where("name = ?", "jinzhu").Session(&gorm.Session{}) // First query queryDB.Where("age > ?", 10).First(&user) // SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 10 // Second query, safely isolated queryDB.Where("age > ?", 20).First(&user2) // SQL: SELECT * FROM users WHERE name = "jinzhu" AND age > 20 ``` In this scenario, using Session(&gorm.Session{}) ensures that each query starts with a fresh context, preventing the pollution of SQL queries with conditions from previous operations. This is crucial for maintaining the integrity and accuracy of your database interactions. **核心就是当你进行了一次链式操作,要注意这个db的db.clone字段已经为零,可以一些其他的方法去重置如session**

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

402 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传