gorm详解

1、gorm的常用用法。自定义类型。clause

2、gorm+gen 提高crud的速度

3、trace链路追踪。

4、gorm集群和分库分表。

5、日志。

6、gorm.DB链式操作需要注意的地方

gorm.io/zh_CN/docs/index.html

gorm提供的文档非常齐全了,总结一下在工作中的一些实践,如果对您有帮我,您的star就是对我的鼓励github.com/luxun9527/go-lib 如有任何问题也欢迎留言。

安装mysql

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子句。

新增

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'

修改

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

删除

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

查询

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

自定义类型

github.com/go-gorm/datatypes

gorm.io/zh_CN/docs/data_types.html...

自定义的数据类型必须实现 ScannerValuer 接口,以便让 GORM 知道如何将该类型接收、保存到数据库github.com/go-gorm/datatypes/blob/... 示例

其他高级的用法参考gorm.io/zh_CN/docs/data_types.html...

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子句

blog.csdn.net/dorlolo/article/deta...

gorm.io/zh_CN/docs/create.html#Ups...

gorm与子句生成器有关的类,按父级到子集排列为 DB –> Statement –> Clause –> Expression (分别对应 数据库连接对象–> 语句 –> 子句 –> 表达式),它们都是以属性形式保存在父类中。只要知道这个结构,看源码就会轻松很多。

img

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 实现方法

只要自定义的模型字段中包含以下的其中一种方法即可,

CreateClauses(*Field) []clause.Interface
QueryClauses(*Field) []clause.Interface
UpdateClauses(*Field) []clause.Interface
DeleteClauses(*Field) []clause.Interface

这个返回值类型可以实现StatementModifier接口 或者 clause.Interface接口都行。使用实例。

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

详情用法可以参考github.com/go-gorm/soft_delete 软删除的做法。

2、gorm + gen

gorm.io/zh_CN/gen/dao.html

gorm.io/zh_CN/gen/database_to_stru...

相对于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

如果有定制需求可以参考gorm.io/zh_CN/gen/database_to_stru...

有一些坑

1、https://github.com/go-gorm/gen/issues/755自定义方法CommonMethod 需要和生成代码不在同一个包

gorm.io/zh_CN/gen/database_to_stru...

// 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、使用变量名而不是直接表字段名,避免写错字段。

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、对查询的进一步封装,在查询上更加方便。

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

需要注意的的是:

github.com/go-gorm/gen/issues/900 gen的Select方法和Where都是不能自定义的,当进行一些复杂查询的时候,需要自定义的时候如使用json函数db.Select("JSON_OBJECT(key1,val1,key2,val2...)") gen是无法做到的。但是我们可以使用ast来生成对应的代码。具体可以参考。github.com/luxun9527/go-lib/blob/m...

3、trace链路追踪

将gorm加入到链路追踪中。使用WithContext传递。

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

4、主从集群,分库分表

refer www.freesion.com/article/927013941...

www.cnblogs.com/cao-lei/p/13603043...

主从延迟 jishuin.proginn.com/p/763bfbd589f7

blog.csdn.net/JesseYoung/article/d...

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

记住File和Position,后面需要用到。此时一定不要操作Master库,否则将会引起Master状态的变化,File和Position字段也将会进行变化。

4.2 slave配置

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

4.3 slave常见错误

1、日志设置错误img

master_log_file='mysql-bin1.000004', master_log_pos=157

img

检查日志是否和master的一致

2、master ip端口配置错误配置错误img

其他命令

stop slave;停止slave

reset master; 重置master

测试是否成功

#创建数据库
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主从集群

gorm.io/zh_CN/docs/dbresolver.html

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 水平分表

gorm.io/zh_CN/docs/sharding.html

这个分表暂时不支持mysql如果mysql进行水平分表的话,要在业务代码中实现。

5、日志

gorm.io/zh_CN/docs/logger.html 自定义日志需要实现这几个接口。

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 并发不安全。

juejin.cn/post/7134002645651439630

首先,我们需要先去理解几乎每个方法中都会调用的函数:tx = db.getInstance()。

 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
 }

将上述改写并简化一下,大概是这么个逻辑:

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赋值回全局变量下面是一个错误用法。

    var users1 []*User
    //初始的db.clone为1 执行where db.clone为0
    db = db.Where("id = ?", 6)

    db.Where("name = ?", "lisi").Find(&users1)

下面也是链式操作不注意db.clone造成的问题。

gorm.io/zh_CN/docs/method_chaining...

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

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 1

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!