Go ORM 之 Gorm

简介

gorm 是 go orm 实现之一,这篇文章将以 mysql 为例,带你体验 gorm 80%+ 的内容。

安装
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
连接池
db, err := gorm.Open(mysql.Open("root:root@tcp(localhost:3306)/demo?parseTime=true&loc=Asia%2FShanghai"), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }
    sqlDb, _ := db.DB()
    sqlDb.SetMaxOpenConns(5)
    sqlDb.SetMaxIdleConns(2)
    sqlDb.SetConnMaxIdleTime(time.Minute)

这样就初始化了一个最大连接数为5,最大空闲连接数为2,最大空闲时间为1分钟的连接池。后续直接使用 db 操作数据库即可。

AutoMigrate

gorm 使用结构体标识 table ,即一个 struct 对应数据库里的一个 table 。

type BaseModel struct {
    ID        uint      `gorm:"primary_key" json:"id"`
    CreatedAt JsonTime  `json:"created_at"`
    UpdatedAt JsonTime  `json:"updated_at"`
    DeletedAt gorm.DeletedAt `sql:"index" json:"-"`
}

type User struct {
    BaseModel
    Username string `json:"username"`
    Password string `json:"password"`
    Avatar string `json:"avatar"`
    Age *int
}

err = db.AutoMigrate(&models.User{})
    if err != nil {
        log.Fatal(err)
    }

JsonTime 是内嵌 time.Time 的自定义类型,主要用于格式化日期,以更好的符合国内使用者的习惯。

type JsonTime struct {
    time.Time
}

func (t JsonTime)MarshalJSON()([]byte,error)  {
    str := fmt.Sprintf("\"%s\"", t.Format("2006-01-02 15:04:05"))
    return []byte(str),nil
}

func (t JsonTime)Value()(driver.Value,error)  {
    var zeroTime time.Time
    if t.Time.UnixNano() == zeroTime.UnixNano() {
        return nil,nil
    }
    return t.Time,nil
}

func (t *JsonTime)Scan(v interface{}) error  {
    value,ok := v.(time.Time)
    if ok {
        *t = JsonTime{Time: value}
        return nil
    }
    return fmt.Errorf("error %v",v)
}

运行代码,gorm 就会自动帮你把 table 创建出来,名称规范遵循 蛇形命名

创建记录
age := 20
    user := models.User{Username:"gorm",Password:"",Age:&age}
    db.Debug().Create(&user)
    fmt.Println(user.ID)

Debug() 将会在终端显示运行的 sql 语句:

48-1.png

也可以使用 map 创建记录,但是必须通过 Model() 或者 Table() 指定表名。

user := map[string]interface{}{
        "username":"map",
        "password":"",
        "age":20,
    }
    db.Debug().Model(&models.User{}).Create(&user)
48-2.png
更新
var user models.User
    db.Last(&user)
    user.Avatar = "xxxx"
    *user.Age += 1
    db.Debug().Save(&user)
    fmt.Println(user)

只要模型具有 ID 属性且不为空,Save() 将做全字段更新,可以使用 Select 指定需要更新的字段,只需更新一个字段则使用 UpdateColumn() 更为方便。

使用 struct 更新默认不会更新零值,可以通过 Select 或者使用 map 更新解决。

删除
tx := db.Debug().Where("name = ?", "jinzhu").Delete(&email)
    if tx.Error != nil {
        log.Fatal(tx.Error)
    }
    fmt.Println(tx.RowsAffected)

gorm 默认使用软删除,Delete 方法实际是做更新操作。如果强制删除,则添加 .Unscoped() 方法。

查询

查询涉及到的内容就比较多了,gorm 使用链式 Api ,跟其他语言的 ORM 使用起来非常类似。

var user models.User
db.Debug().First(&user)
db.Debug().Where("username = ?","purelight").First(&user)
db.Debug().Find(&user,18)
var age int
db.Debug().Select("age").Model(&models.User{}).First(&age)

有两个比较重要的,一是确定 table ,这个可以通过 Model() ,Table() ,或者通过 Find 等 Finisher 方法的结构体指针参数确定;二是获取查询结果,除了直接映射到结构体指针和map指针外,还可以使用 .Rows() 然后去遍历。

另外由于默认采用了软删除,所以 gorm 在查询是会自动带上 deleted_at is not null 的条件。

其他常见的 Where,Order,GroupBy,Offset,Limit,Distinct,Join,Count 都是支持的,详细可查阅具体文档。

Scope
func AgeOfAdult(db *gorm.DB) *gorm.DB  {
    return db.Where("age >= ?",18)
}

db.Scopes(AgeOfAdult).First(&user)

很好理解,跟 Laravel 的 scope 相似,用于封装通用过滤条件。

模型关联
  • Belongs To

    type Pet struct {
      BaseModel
      Name *string `json:"name"`
      Age *int `json:"age"`
      UserID int `json:"-"`
      User User `json:"-"`
    }
    

    pet 属于 user ,UserID 是外键,对应的数据表列名是 user_id ,当然,可以通过 tag 修改默认映射的列名,比如我们数据表列名是 u_id ,只需:

    type Pet struct {
      BaseModel
      Name *string `json:"name"`
      Age *int `json:"age"`
      UserID int `json:"-" gorm:"column:u_id"`
      User User `json:"-"`
    }
    

    如果我们的 struct 已经有一个 UID 字段并且就是外键,我们可以重写外键:

    type Pet struct {
      BaseModel
      Name *string `json:"name"`
      Age *int `json:"age"`
      UID int
      User User `json:"-" gorm:"foreignKey:UID"`
    }
    

    如果不是关联的 user 的 id ,比如 name ,则可以重写引用:

    type Pet struct {
      BaseModel
      Name *string `json:"name"`
      Age *int `json:"age"`
      UserID int `json:"-"`
      User User `json:"-" gorm:"references:Name"`
    }
    

    查询使用 Preload() 可以提前加载关联,可以避免 N+1 的问题。

  • Has One

    type User struct {
      gorm.Model
      CreditCard CreditCard
    }
    
    type CreditCard struct {
      gorm.Model
      Number string
      UserID uint
    }
    

    可见,与 Belongs To 相似。

    还有种自引用:

    type Area struct {
      BaseModel
      Name string
      ParentID *uint
      Children []Area `gorm:"ForeignKey:ParentID"`
    }
    
  • HasMany

    type User struct {
      BaseModel
      Username string `json:"username"`
      Password string `json:"password"`
      Avatar string `json:"avatar"`
      Age *int
      CreditCards []CreditCard
    }
    

    就是将 Has One 的单个模型改成 slice 。

  • 多态(适用于 Has One 和 Has Many)

    type Cat struct {
      BaseModel
      Name string
      Animal Animal  `gorm:"polymorphic:Owner;"`
    }
    
    type Dog struct {
      BaseModel
      Name string
      Animal Animal `gorm:"polymorphic:Owner;"`
    }
    
    type Animal struct {
      BaseModel
      Name string
      OwnerID int
      OwnerType string
    }
    
    tx := db.Create(&models.Dog{Name:"dog1",Animal:models.Animal{Name:"dog1"}})
      fmt.Println(tx.Error)
      var dog models.Dog
      tx = db.Debug().Model(models.Dog{}).Preload("Animal").First(&dog)
      if tx.Error != nil {
          log.Fatal(tx.Error)
      }
      fmt.Println(dog.Animal)
    
  • Many To Many

    type Languages struct {
      BaseModel
      Name string
      Users []User `gorm:"many2many:user_languages"`
    }
    
    type User struct {
      BaseModel
      Username string `json:"username"`
      Languages []Languages `gorm:"many2many:user_languages;"`
    }
    

    这里会创建中间表 user_languages ,表仅有两列:user_id 和 language_id 。

虽然关联模式中默认的列名可以更改,但是建议开发中还是按照框架约定的规范来,不仅看着舒服,代码还能更简洁。

Preload() 只是会提前加载关联关系,如果我们仅仅只想获取关联关系怎么办?这是应使用 Associations :

var user models.User
    db.Debug().Find(&user,54)
    var langs []models.Languages
    db.Debug().Model(&user).Where("name = ?","English").Association("Languages").Find(&langs)
    fmt.Println(len(langs))
    for _,lang := range langs  {
        fmt.Println(lang.Name)
    }

并且支持多层关联:

var departments []models.Department
    err := db.Debug().Model(&user).Association("Company.Departments").Find(&departments)
    if err != nil {
        log.Fatal(err)
    }
    for _,dep := range departments {
        fmt.Println(dep.Name)
    }
错误处理
if err := db.Where("name = ?", "jinzhu").First(&user).Error; err != nil {
  // 处理错误...
}

主动进行错误处理是个好习惯~

Hook

gorm 提供查询,更新,删除,创建场景下的 hook ,相当完善。


func (user *User)BeforeDelete(db *gorm.DB)(err error)  {
    fmt.Println(user.ID,"即将删除")
    return nil
}

func (user *User)BeforeUpdate(db *gorm.DB)(err error)  {
    fmt.Println(user.ID,"更新")
    return nil
}
链式操作

建议完成参考文档 链式方法

关键是要注意协程安全,想要复用 db ,务必确保其处于 ”新建会话模式“ 。

事务
err := db.Debug().Transaction(func(tx *gorm.DB) error {
        if err := tx.Create(&models.User{Username:"trans1"}).Error;err != nil {
            return err
        }

        tx.Transaction(func(tx2 *gorm.DB) error {
            user := models.User{Username:"trans2"}
            user.ID = 55
            if err := tx.Create(&user).Error;err != nil {
                return err
            }
            return nil
        })

        return nil
    },nil)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println("事务执行成功")

这是自动模式,return error 自动回滚,否则自动提交。另外还有手动控制提交回滚的方式。

gorm 基于 savepoint 支持嵌套事务。

关于事务,gorm 创建更新删除操作默认也是在事务里面执行,配置关闭:SkipDefaultTransaction: true 将会提升不少性能。

Migrator
fmt.Println(db.Migrator().HasTable(models.User{}))

Migrator 更为精细化控制 table metadata 。

Logger
f,err := os.OpenFile("sql.log",os.O_APPEND|os.O_RDWR|os.O_CREATE,os.ModePerm)
    if err != nil {
        log.Fatal(err)
    }
    defer f.Close()
    logger1 := logger.New(log.New(f,"\r\n",log.LstdFlags),logger.Config{
        SlowThreshold: time.Second,   // 慢 SQL 阈值
        LogLevel:      logger.Info, // 日志级别
        IgnoreRecordNotFoundError: true,   // 忽略ErrRecordNotFound(记录未找到)错误
        Colorful:      false,         // 禁用彩色打印
    })
    var user models.User
    var user2 models.User
    tx := db.Session(&gorm.Session{Logger:logger1})
    tx.First(&user)
    tx.Last(&user2)
    fmt.Println(user.Username)
    fmt.Println(user2.Username)
自定义类型
type Book struct {
    ID uint64
    CreatedAt JsonTime
    UpdatedAt JsonTime
    DeletedAt DeletedAt
    Name string
    Tags StringArray `gorm:"type:varchar(255)"`
}

type StringArray []string

func (sa *StringArray)Scan(value interface{}) error  {
    tags,ok := value.([]byte)
    if !ok {
        return errors.New("类型有误")
    }
    *sa = strings.Split(string(tags),",")
    return nil
}

func (sa StringArray)Value() (driver.Value,error){
    if len(sa) == 0 {
        return "",nil
    }
    return strings.Join(sa,","),nil
}

这里将切片类型的 tags 转成 , 分隔的文本存入数据库,读取的时候再将文本转成切片使用:

book := models.Book{Name:"ruby",Tags:models.StringArray{"xx","ff"}}
    db.Debug().Create(&book)
    var b1 models.Book
    db.Debug().Where("name = ?","ruby").First(&b1)
    fmt.Println(b1.Tags)
dbresolver

可实现读写分离,负载均衡。

master1 := "root:root@tcp(localhost:33060)/demo?parseTime=true&loc=Asia%2FShanghai"
    replica1 := "root:root@tcp(localhost:33061)/demo?parseTime=true&loc=Asia%2FShanghai"
    replica2 := "root:root@tcp(localhost:33062)/demo?parseTime=true&loc=Asia%2FShanghai"
    db,err := gorm.Open(mysql.Open(master1),&gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }
    db.Use(dbresolver.Register(dbresolver.Config{
        Sources:[]gorm.Dialector{mysql.Open(master1)},
        Replicas:[]gorm.Dialector{mysql.Open(replica1),mysql.Open(replica2)},
        Policy:dbresolver.RandomPolicy{},
    }).SetMaxOpenConns(10).SetMaxIdleConns(5).SetConnMaxIdleTime(time.Minute))

    db.AutoMigrate(&models.User{})

    //age := 10
    //u1 := models.User{Username:"scl",Age:&age}
    //if err = db.Create(&u1).Error;err != nil {
    //  log.Fatal(err)
    //}
    //fmt.Println("u1创建成功")
    //var user models.User
    //db.Debug().First(&user)
    //var rs int
    //db.Debug().Raw("select sleep(120);").Scan(&rs)
    //fmt.Println(user.Username)
    db.Clauses(dbresolver.Write).Debug().Exec("select sleep(300);")

需自行搭配好数据库的主从集群,使用主从依然存在一个问题,创建场景下刚创建的数据立马去查从库,从库大概率没这么快同步完成,这时候要去主库查,其他框架一般有个 Sticky 选项,gorm 这里可以通过 Clause(dbresolver.Write) 指定从主库读取。

安全
userInput := "jinzhu;drop table users;"

// 安全的,会被转义
db.Where("name = ?", userInput).First(&user)

// SQL 注入
db.Where(fmt.Sprintf("name = %v", userInput)).First(&user)

永远不要相信用户的输入。

其它

原生SQL,Context,约束,配置,插件相关内容请查阅 gorm 官方文档。

总结

麻雀虽小,腑脏俱全。基本该有的都有,毕竟我也只深入了解了 gorm 这一个 orm ,与其他 orm 的对比还请参考搜索引擎 。

2022-01-17

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,558评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,002评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,024评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,144评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,255评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,295评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,068评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,478评论 1 305
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,789评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,965评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,649评论 4 336
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,267评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,982评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,223评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,800评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,847评论 2 351

推荐阅读更多精彩内容

  • Go 1.声明变量 2.Go语言常量 3.运算符 4.for循环 5.Go函数 6.数组声明 语言指针 Go 语言...
    TZX_0710阅读 288评论 0 1
  • 胖sir :接着,给你一个馅饼儿 兵长 : 来嘞!!一篇来自ORM的整理笔记... 1 什么是ORM?为什么要⽤O...
    阿兵云原生阅读 158评论 0 1
  • GO语言基础 认识并安装GO语言开发环境 Go语言简介 Go语言是谷歌2009年发布的第二款开源编程语言 go语言...
    进击的大东阅读 438评论 0 0
  • 目录 1.go 各种代码运行 2.go 在线编辑代码运行 3.通过 Gob 包序列化二进制数据 4.使用 ...
    杨言锡阅读 1,122评论 0 1
  • 这篇文章我们主要探究下面这些内容。 gorm的基本用法 如何管理ORM的使用 如何合理规划项目目录结构 安装gor...
    Java天天阅读 1,248评论 1 0