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

你可能感兴趣的:(Go ORM 之 Gorm)