• Welcome to Journal web site.

我是 PHP 程序员

- 开发无止境 -

Next
Prev

一文详解Gorm

Data: 2018-05-11 12:17:03Form: JournalClick: 7

Gorm入门到精通

一 ORM框架介绍和选择

1.1 ORM介绍和选择

ORM是“对象-关系-映射”的简称,Go语言中常用的ORM框架有很多,如gorm、facebook-ent、xorm、upper/db、gorose等,这里以gorm为例。

gorm

注: 本文内容基于官方文档, 由于迭代原因,建议以最新官方文档为主

老牌国产Golang orm框架。支持主流关系型数据库。中文文档适合新人入手,国内使用较多。最新版本2.x,比1.x有较大改动

注意:Gorm最新地址为https://github.com/go-gorm/gorm,之前https://github.com/jinzhu/gorm地址为v1旧版本

Gorm最新源码地址:https://github.com/go-gorm/gorm

V1版本地址:https://github.com/jinzhu/gorm

中文文档地址:https://gorm.io/zh_CN/

1.2 Gorm介绍

Gorm 是 Golang 的一个 orm 框架。ORM 是通过实例对象的语法,完成关系型 数据库的操作,是"对象-关系映射"(Object/Relational Mapping) 的缩写。使用 ORM 框架可以让我们更方便的操作数据库。

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

作者是中国人,中文文档齐全,对开发者友好,支持主流数据库。

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

二 gorm连接数据库

go get -u gorm.io/gorm  // gorm
go get gorm.io/driver/mysql  // 驱动

2.1 快速链接mysql

package main
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"
    // loc 设置本地系统的时间, 前提 parseTime=True
    // 更多参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 
	_, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	//db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger:logger.Default.LogMode(logger.Info)})  打印sql日志
	if err != nil {
		panic(err) // 如果数据库不存在会报错

	}
}

2.2 其他配置

**注意:**想要正确的处理 time.Time ,您需要带上 parseTime 参数, (更多参数) 要支持完整的 UTF-8 编码,您需要将 charset=utf8 更改为 charset=utf8mb4 查看 获取详情

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

2.3 加入日志打印sql

Gorm 有一个 默认 logger 实现,默认情况下,它会打印慢 SQL 和错误

Logger 接受的选项不多,您可以在初始化时自定义它,例如:

newLogger := logger.New(
  log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
  logger.Config{
    SlowThreshold: time.Second,   // 慢 SQL 阈值
    LogLevel:      logger.Silent, // 日志级别  不输出日志
    IgnoreRecordNotFoundError: true,   // 忽略ErrRecordNotFound(记录未找到)错误
    Colorful:      false,         // 禁用彩色打印
  },
)

// 全局模式
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    Logger: newLogger
})

// 新建会话模式
tx := db.Session(&Session{Logger: newLogger})
日志级别

GORM 定义了这些日志级别:SilentErrorWarnInfo

db, err := gorm.Open(mysql.Open("test.db"), &gorm.Config{Logger: newLogger})
 

Debug

Debug 单个操作,将当前操作的 log 级别调整为 logger.Info

db.Debug().Where("name = ?", "jinzhu").First(&User{})
 
具体代码
package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type  User struct {
	ID int
}
func main() {
	// 日志配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 阈值
			LogLevel:                  logger.Info, // 日志级别为info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
			Colorful:                  true,        // 彩色打印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err) // 如果数据库不存在会报错
	}
    // 数据库迁移
	db.AutoMigrate(&User{})
}
 

三 automigrate功能

AutoMigrate自动创建表,可以打开Navicat查看表结构

AutoMigrate 自动创建表目前而言还有部分bug,没有Django的完善,例如: 无法自动识别修改字段, 无法删除表字段,并且有以下特殊约定

  1. 默认以结构体名字小写 +s 作为表名
  2. 字段名go中是驼峰,mysql中换成下划线
  3. go中字段类型会自动映射成mysql的不同类型,有默认的对应关系

gorm.Model结构体嵌套

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type Product struct {
	gorm.Model 
	Code  string
	Price uint
}
func main() {
	// 日志配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 阈值
			LogLevel:                  logger.Info, // 日志级别为info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
			Colorful:                  true,        // 彩色打印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err) // 如果数据库不存在会报错
	}
	db.AutoMigrate(&Product{}) // 可以加多个
    
    // 可以通过Set设置附加参数,下面设置表的存储引擎为InnoDB
	db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
}
 

四 快速增删改查

4.1 快速增删改查

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type Product struct {
	gorm.Model
	Code  string
	Price uint
}
func main() {
	// 日志配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 阈值
			LogLevel:                  logger.Info, // 日志级别为info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
			Colorful:                  true,        // 彩色打印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err) // 如果数据库不存在会报错
	}
	//1 迁移 表,可以写多个
	//db.AutoMigrate(&Product{})

	//2 插入数据
	db.Create(&Product{Code: "D42", Price: 100})
	db.Create(&Product{Code: "D43", Price: 150})

	//3 查询数据
	var product Product  // 定义空Product结构体对象
	db.First(&product, 1) // 根据整型主键查找
	db.First(&product, "code = ?", "F43") // 查找 code 字段值为 D43 的记录
	//fmt.Println(product)

	//4  更新 - 将 product 的 price 更新为 200
	db.Model(&product).Update("Price", 200)
	// Update - 更新多个字段
	db.Model(&product).Updates(Product{Price: 200, Code: "F43"}) // 仅更新非零值字段
	db.Model(&product).Updates(map[string]interface{}{"Price": 300, "Code": "F42"})

	//5  Delete - 删除 product
	db.Delete(&product, 1)  // 软删除
}
 

4.2 逻辑删除

只要使用了gorm.Model结构体继承,DeletedAt DeletedAt `gorm:"index"`  字段
执行删除是其实是update语句,并没有真正的删除
 

五 零值问题

var product Product
db.First(&product)
db.Model(&product).Updates(Product{Price: 200, Code: ""}) // 仅更新非零值字段
// UPDATE `products` SET `updated_at`='2022-09-03 23:48:51.96',`price`=200 WHERE `products`.`deleted_at` IS NULL AND `id` = 2

// 可以看到Code字段不会更新,这是合理的,因为如果零值字段也更新,Product表中好多数据都会被更新为空
 

5.1 使用sql.NullString更新

//表模型修改为
type Product struct {
	gorm.Model
	Code  sql.NullString
	Price uint
}
// 修改语句为
db.Model(&product).Updates(Product{Price: 200, Code: sql.NullString{"",true}}) 	
 

5.2 使用指针解决

//表模型修改为
type Product struct {
	gorm.Model
	Code  *string
	Price uint
}
// 修改语句为
var product Product
db.First(&product)
var empty = ""
db.Model(&product).Updates(Product{Price: 200, Code: &empty}) // 仅更新非零值字段
 

六 表结构定义细节

6.1 模型定义

模型是标准的 struct,由 Go 的基本数据类型、实现了 Scanner 和 Valuer 接口的自定义类型及其指针或别名组成

例如:

type User struct {
  ID           uint
  Name         string
  Email        *string
  Age          uint8
  Birthday     *time.Time
  MemberNumber sql.NullString
  ActivatedAt  sql.NullTime
  CreatedAt    time.Time
  UpdatedAt    time.Time
}
 

6.2 约定

GORM 倾向于约定,而不是配置。默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAtUpdatedAt 字段追踪创建、更新时间

遵循 GORM 已有的约定,可以减少您的配置和代码量。如果约定不符合您的需求,GORM 允许您自定义配置它们

6.3 gorm.Model

GORM 定义一个 gorm.Model 结构体,其包括字段 IDCreatedAtUpdatedAtDeletedAt

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

您可以将它嵌入到您的结构体中,以包含这几个字段,详情请参考 嵌入结构体

6.4 高级选项

字段级权限控制

可导出的字段在使用 GORM 进行 CRUD 时拥有全部的权限,此外,GORM 允许您用标签控制字段级别的权限。这样您就可以让一个字段的权限是只读、只写、只创建、只更新或者被忽略

注意: 使用 GORM Migrator 创建表时,不会创建被忽略的字段

type User struct {
  Name string `gorm:"<-:create"` // allow read and create
  Name string `gorm:"<-:update"` // allow read and update
  Name string `gorm:"<-"`        // allow read and write (create and update)
  Name string `gorm:"<-:false"`  // allow read, disable write permission
  Name string `gorm:"->"`        // readonly (disable write permission unless it configured)
  Name string `gorm:"->;<-:create"` // allow read and create
  Name string `gorm:"->:false;<-:create"` // createonly (disabled read from db)
  Name string `gorm:"-"`            // ignore this field when write and read with struct
  Name string `gorm:"-:all"`        // ignore this field when write, read and migrate with struct
  Name string `gorm:"-:migration"`  // ignore this field when migrate with struct
}
 
创建/更新时间追踪(纳秒、毫秒、秒、Time)

GORM 约定使用 CreatedAtUpdatedAt 追踪创建/更新时间。如果您定义了这种字段,GORM 在创建、更新时会自动填充 当前时间

要使用不同名称的字段,您可以配置 autoCreateTimeautoUpdateTime 标签

如果您想要保存 UNIX(毫/纳)秒时间戳,而不是 time,您只需简单地将 time.Time 修改为 int 即可

type User struct {
  CreatedAt time.Time // 在创建时,如果该字段值为零值,则使用当前时间填充
  UpdatedAt int       // 在创建时该字段值为零值或者在更新时,使用当前时间戳秒数填充
  Updated   int64 `gorm:"autoUpdateTime:nano"` // 使用时间戳填纳秒数充更新时间
  Updated   int64 `gorm:"autoUpdateTime:milli"` // 使用时间戳毫秒数填充更新时间
  Created   int64 `gorm:"autoCreateTime"`      // 使用时间戳秒数填充创建时间
}
 
嵌入结构体

对于匿名字段,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
}
 
字段标签

声明 model 时,tag 是可选的,GORM 支持以下 tag: tag 名大小写不敏感,但建议使用 camelCase 风格

标签名 说明
column 指定 db 列名
type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not nullsize, autoIncrement… 像 varbinary(8) 这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSIGNED not NULL AUTO_INCREMENT
serializer 指定如何将数据序列化和反序列化到数据库中的序列化程序,如: serializer:json/gob/unixtime
size 指定列数据大小/长度, 如: size:256
primaryKey 指定列作为主键
unique 指定列作为unique
default 指定列的默认值
precision 指定列的精度
scale 指定列的比例
not null 指定列不为空
autoIncrement 指定列自增
autoIncrementIncrement 自动递增步长,控制连续列值之间的间隔
embedded 嵌入字段
embeddedPrefix 嵌入嵌入字段的字段列名前缀
autoCreateTime 跟踪当前时间创建时,对于’int’字段,它将跟踪unix秒,使用值’nano/'milli跟踪unix nano/milli秒,如: autoCreateTime:nano
autoUpdateTime 在创建/更新时跟踪当前时间,对于’int’字段,它将跟踪unix秒,使用值’nano/'milli跟踪unix nano/milli秒, 如: autoUpdateTime:milli
index 使用选项创建索引,对多个字段使用相同的名称创建复合索引, 详情参照 Indexes
uniqueIndex 与’index’相同,但创建唯一索引
check 创建检查约束, 如: check:age > 13, 参照 Constraints
<- 设置字段的写入权限, <-:create 仅创建字段, <-:update 仅更新字段, <-:false 没有写权限, <- 创建和更新权限
-> 设置字段读权限, ->:false 没有读权限
- 忽略该字段, - 没有读写权限, -:migration 没有迁移权限, -:all 没有 read/write/migrate 权限
comment 迁移时为字段添加注释

6.5 案例

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"log"
	"os"
	"time"
)

type User struct {
	UserId uint `gorm:"primaryKey"`
	Name string `gorm:"column:user_name;type:varchar(60)"`
	Gender uint `gorm:"index"`
}

func main() {
	// 日志配置
	newLogger := logger.New(
		log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
		logger.Config{
			SlowThreshold:             time.Second, // 慢 SQL 阈值
			LogLevel:                  logger.Info, // 日志级别为info
			IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
			Colorful:                  true,        // 彩色打印
		},
	)

	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: newLogger,
	})
	if err != nil {
		panic(err) // 如果数据库不存在会报错
	}
	db.AutoMigrate(&User{}) 
  // 如果表之前存在会修改,但是只会修改之前存在的字段的属性,无法识别删除字段
}
 

七 Gorm增加操作

我们尽量按照官方文档来看,网上教程都是老版本居多,有时候误差还挺大的

package main

import (
	"database/sql"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"time"
)

type User struct {
	gorm.Model
	Name         string
	Age          uint8
	Birthday     time.Time
}

func main() {
	dsn := "root:123@tcp(127.0.0.1:3306)/gorm_test?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}
	db.AutoMigrate(&User{})

}
 

7.1 创建记录

user := User{Name: "lxx", Age: 18, Birthday: time.Now()}
result := db.Create(&user) // 通过数据的指针来创建
fmt.Println(user.ID) // 返回插入数据的主键
fmt.Println(result.Error
                
                
                
                
                
                
              
Name:
<提交>