GORM实践 | 青训营笔记

276 阅读5分钟

GORM实践 | 青训营笔记

这是我参与「第三届青训营 -后端场」笔记创作活动的的第5篇笔记

参考官方文档

特性

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 PreloadJoins 的预加载
  • 事务,嵌套事务,Save Point,Rollback To Saved Point
  • Context、预编译模式、DryRun 模式
  • 批量插入,FindInBatches,Find/Create with Map,使用 SQL 表达式、Context Valuer 进行 CRUD
  • SQL 构建器,Upsert,数据库锁,Optimizer/Index/Comment Hint,命名参数,子查询
  • 复合主键,索引,约束
  • Auto Migration
  • 自定义 Logger
  • 灵活的可扩展插件 API:Database Resolver(多数据库,读写分离)、Prometheus…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

连接到数据库

GORM 官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server

MySQL

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

func main() {
   dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
   db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

MySQl 驱动程序提供了 一些高级配置 可以在初始化过程中使用,例如:

db, err := gorm.Open(mysql.New(mysql.Config{
    DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source name
    DefaultStringSize: 256, // string 类型字段的默认长度
    DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
    DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
    DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
    SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}), &gorm.Config{})

GORM 使用database/sql维护连接池

sqlDB, err := db.DB()

// 空闲连接池中连接的最大数量
sqlDB.SetMaxIdleConns(10)

// 打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)

// 连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)

基本用法

官方文档

GORM 模型声明

  • 表名为struct name的snake_cases (下划线连接)复数格式
  • 字段名为field name的snake_case单数格式
  • ID/Id字段为主键,如果为数字,则为自增主键
  • CreatedAt 字段,创建时,保存当前时间
  • UpdatedAt 字段,创建、更新时,保存当前时间
  • gorm.DeletedAt 字段,默认开启 soft delete 模式

GORM 定义一个 gorm.Model 结构体,其包括字段 ID、CreatedAt、UpdatedAt、DeletedAt

// gorm.Model 的定义
type Model struct {
    ID        uint `gorm:"primaryKey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
}

对于匿名字段,GORM 会将其字段包含在父结构体中,例如:

type User struct {
    gorm.Model
    Name string
}

// 等效于
type User struct {
    ID        uint `gorm:"primaryKey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
    Name      string
}

对于正常的结构体字段,你也可以通过标签 embedded 将其嵌入,例如:

type Author struct {
    Name  string
    Email string
}

type Blog struct {
    ID      int
    Author  Author `gorm:"embedded"`
    Upvotes int32
}

// 等效于
type Blog struct {
    ID      int64
    Name    string
    Email   string
    Upvotes int32
}

并且,您可以使用标签 embeddedPrefix 来为 db 中的字段名添加前缀,例如:

type Blog struct {
    ID      int
    Author  Author `gorm:"embedded;embeddedPrefix:author_"`
    Upvotes int32
}

// 等效于
type Blog struct {
    ID          int64
    AuthorName  string
    AuthorEmail string
    Upvotes     int32
}

GORM增删改查

点击查看代码🐋
    package main

    import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    )
    type Userinfo struct {
       Id     uint
       Name   string
       Gender string
       Hobby  string
    }

    func main() {
       // 连接数据库
       dsn := "root:123456@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
       db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
       if err != nil {
          panic(err1)
       }

       //连接池
       sqlDB, err := db.DB()
       if err != nil {
          panic(err)
       }
       // 空闲连接池中连接的最大数量
       sqlDB.SetMaxIdleConns(10)

       // 打开数据库连接的最大数量。
       sqlDB.SetMaxOpenConns(100)

       // 连接可复用的最大时间。
       sqlDB.SetConnMaxLifetime(time.Hour)
       //自动迁移

       //创建1
       db.AutoMigrate(&Userinfo{})
       u1 := Userinfo{Id: 1, Name: "张三", Gender: "男", Hobby: "学习"}

       db.Create(&u1)

       //创建2: 创建记录并更新给出的字段
       u2 := Userinfo{Id: 1, Name: "李四", Gender: "男", Hobby: "游戏"}
       db.Select("Name", "Age", "CreatedAt").Create(&u2)

       //创建3: 创建记录且一同忽略传递给略去的字段值
       u3 := Userinfo{Id: 1, Name: "王五", Gender: "男", Hobby: "吃饭"}
       db.Select("Name", "Age", "CreatedAt").Create(&u3)

       //批量插入
       var users = []Userinfo{{Name: "张三"}, {Name: "李四"}, {Name: "王五"}}
       db.Create(&users)

         var users = []User{{name: "张三_1"}, ....,{Name: "张三_10000"}}
       // 数量为 100
       db.CreateInBatches(users, 100)

       // 获取第一条记录(主键升序)
       db.First(&user)

       // 获取一条记录,没有指定排序字段
       db.Take(&user)

       // 获取最后一条记录(主键降序)
       db.Last(&user)

       result := db.First(&user)
       result.RowsAffected  // 返回找到的记录数
       result.Error         // returns error or nil

       // 检查 ErrRecordNotFound 错误
       errors.Is(result.Error, gorm.ErrRecordNotFound)

       // 更新
       db.First(&user)
       user.Name = "张三"
       user.Age = 100
       db.Save(&user)

       db.Delete(&email)
       // 带条件的删除
       db.Where("name = ?", "张三").Delete(&email)
    }

GORM设计原理

应用程序——>操作接口——>database/sql——>连接/操作接口——>数据库

  • Gorm finisher执行GORM statement

  • SQL是怎么生成的——扩展子句

  • SQL是怎么生成的——选择子句

  • GORM插件的工作:注册、移动、替代、获取、before

  • 多数据库、读写分离: 负载均衡策略

  • ConnPool: GORM——>写DB/读DB

    1. 缓存操作
    2. 预编译操作
    3. 数据安全起见:GORM——SQL>ConnPool——插件>数据库
  • 最开始的问题:链接Mysql

    1. Dialector:GORM生成
    2. Options接口

GORM最佳实践

  1. 数据序列化与SQL表达式:

    • gorm.Expr
    • struct定义GormValuer
    • 自定义查询SQL接口
  2. 批量数据操作——批量操作、查询:

    • 批量创建
    • 批量查询
  3. 批量数据加速操作:

    • 关闭默认事务
    • 默认批量导入会调用HOOKS方法
    • 使用Prepared Statement
  4. 代码复用、分库分表、sharding——sharding:

  5. 混沌工程、压力测试:检查系统状态能不能发现某一错误。

  6. logger、trace

  7. Gen代码生成/RAW-SQL

  8. 安全问题:

    • 安全:以参数传入数据
    • 危险:sql注入数据

Gorm-gen

基于 GORM, 更安全更友好的ORM工具 Gorm-gen

  • 自动生成 CRUD 和 DIY 方法
  • 自动根据表结构生成模型(model)代码
  • 事务、嵌套事务、保存点、回滚事务点
  • 完全兼容 GORM
  • 更安全、更友好
  • 多种生成代码模式