• Welcome to Journal web site.

我是 PHP 程序员

- 开发无止境 -

Next
Prev

Gorm v2全解

Data: 2022-07-25 02:23:29Form: JournalClick: 4

文档

Gorm官方文档地址:Gorm

工具

  1. Gorm v2:go get -u gorm.io/gorm
  2. mysql:gorm.io/driver/mysql

为啥选择Gorm


可以留意一下:gen,和Gorm是一个作者。

连接到数据库

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

GORM 配置

GORM 提供的配置可以在初始化时使用.

type Config struct {
  SkipDefaultTransaction   bool
  NamingStrategy           schema.Namer
  Logger                   logger.Interface
  NowFunc                  func() time.Time
  DryRun                   bool
  PrepareStmt              bool
  DisableNestedTransaction bool
  AllowGlobalUpdate        bool
  DisableAutomaticPing     bool
  DisableForeignKeyConstraintWhenMigrating bool
}
 

跳过默认事务

为了确保数据一致性,GORM 会在事务里执行写入操作(创建、更新、删除)。如果没有这方面的要求,在初始化时禁用它,会获得大约 30%+ 性能提升。

&gorm.Config{
  SkipDefaultTransaction: true,
}
 

命名策略

GORM 允许用户通过覆盖默认的NamingStrategy来更改命名约定。

// NamingStrategy tables, columns naming strategy
NamingStrategy schema.Namer

type Namer interface {
    TableName(table string) string
    SchemaName(table string) string
    ColumnName(table, column string) string
    JoinTableName(table string) string
    RelationshipFKName(Relationship) string
    CheckerName(table, column string) string
    IndexName(table, column string) string
}
 
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
  NamingStrategy: schema.NamingStrategy{
    TablePrefix: "t_",   // 表前缀, table for `User` would be `t_users`
    SingularTable: true, // 表单数, table for `User` would be `user` with this option enabled
    NoLowerCase: true, // skip the snake_casing of names(跳过蛇形命名,一般不用)
    NameReplacer: strings.NewReplacer("CID", "Cid"), // use name replacer to change struct/field name before convert it to db name(一般不用)
  },
})
 

Logger

实现gorm.logger.Interface接口以覆盖gorm的日志功能

// Logger
Logger logger.Interface
 

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

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,         // 禁用彩色打印
  },
)

// 全局模式
&gorm.Config{
  Logger: newLogger,
}

// 新建会话模式
tx := db.Session(&Session{Logger: newLogger})
tx.First(&user)
tx.Model(&user).Update("Age", 18)
 
日志级别

GORM 定义了这些日志级别:Silent、Error、Warn、Info

&gorm.Config{
  Logger: logger.Default.LogMode(logger.Silent),
}
 

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

db.Debug().Where("name = ?", "jinzhu").First(&User{})
 

NowFunc

更改创建时间使用的函数

 &gorm.Config{
  NowFunc: func() time.Time {
    return time.Now().Local()
  },
}
 

CreateBatchSize

批量插入的数量,

CreateBatchSize:1000
 

DryRun

生成 SQL 但不执行,可以用于准备或测试生成的 SQL,

&gorm.Config{
  DryRun: false,
}
 

PrepareStmt

PreparedStmt 在执行任何 SQL 时都会创建一个 prepared statement 并将其缓存,以提高后续的效率。

&gorm.Config{
  // 禁止
  PrepareStmt: false,
}
 

AllowGlobalUpdate

启用全局 update/delete。

禁用嵌套事务

在一个事务中使用 Transaction 方法,GORM 会使用 SavePoint(savedPointName),RollbackTo(savedPointName) 提供嵌套事务支持,可以通过 DisableNestedTransaction 选项关闭它。

DisableAutomaticPing

在完成初始化后,GORM 会自动 ping 数据库以检查数据库的可用性,若要禁用该特性,可将其设置为 true。

DisableForeignKeyConstraintWhenMigrating

在 AutoMigrate 或 CreateTable 时,GORM 会自动创建外键约束,若要禁用该特性,可将其设置为 true。

禁止该选项,实际开发中一般不用创建外键约束,外键只用代码约束。

连接Mysql并测试连接

DSN:[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]

var handler *gorm.DB

func init() {
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN:                       "root:@tcp(127.0.0.1:3306)/gorm?charset=utf8mb4&parseTime=True&loc=Local", // DSN data source name
		DefaultStringSize:         191,                                                                       // string 类型字段的默认长度
		DisableDatetimePrecision:  true,                                                                      // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
		DontSupportRenameIndex:    true,                                                                      // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
		DontSupportRenameColumn:   true,                                                                      // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
		SkipInitializeWithVersion: false,                                                                     // 根据当前 MySQL 版本自动配置
	}), &gorm.Config{
		// 不使用默认的事务
		SkipDefaultTransaction: false,
		// 不建立实际的外键约束,约束靠代码实现
		DisableForeignKeyConstraintWhenMigrating: true,
		// 表名迁移为单数
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
		},
		// 日志
		Logger: logger.Default.LogMode(logger.Info),
	})

	if err != nil {
		log.Println(err)
	} else {
		fmt.Println(db)
	}
}

// &{0xc0000dc630 <nil> 0 0xc0001a0380 1}
 

常规数据库接口 sql.DB

GORM 提供了 DB 方法,可用于从当前 *gorm.DB 返回一个通用的数据库接口 *sql.DB

// 获取通用数据库对象 sql.DB,然后使用其提供的功能
sqlDB, err := db.DB()

// Ping
sqlDB.Ping()

// Close
sqlDB.Close()

// 返回数据库统计信息
sqlDB.Stats()

// 获取通用数据库对象 sql.DB ,然后使用其提供的功能
sqlDB, err := db.DB()

// SetMaxIdleConns 用于设置连接池中空闲连接的最大数量。
sqlDB.SetMaxIdleConns(10)

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

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

模型定义

  1. GORM 倾向于约定优于配置默认情况下,GORM 使用 ID 作为主键,使用结构体名的 蛇形复数 作为表名,字段名的 蛇形 作为列名,并使用 CreatedAt、UpdatedAt 字段追踪创建、更新时间。
  2. 如果同意以上约定,配置文件就不需要写一堆了,否则就可以自定义约定。

约定

  1. 使用 ID 作为主键

  2. GORM 使用结构体名的 蛇形命名 作为表名。对于结构体 User,根据约定,其表名为 users

  3. TableName 不支持动态变化,它会被缓存下来以便后续使用,可以使用 Table 方法临时指定表名

  4. GORM 允许用户通过覆盖默认的命名策略更改默认的命名约定,命名策略被用于构建: TableName、ColumnName、JoinTableName、RelationshipFKName、CheckerName、IndexName。

    type Animal struct {
      AnimalID int64     `gorm:"column:beast_id"`         // 将列名设为 `beast_id`
      Birthday time.Time `gorm:"column:day_of_the_beast"` // 将列名设为 `day_of_the_beast`
      Age      int64     `gorm:"column:age_of_the_beast"` // 将列名设为 `age_of_the_beast`
    }
    
     
  5. 对于有 CreatedAt 字段的模型,创建记录时,如果该字段值为零值,则将该字段的值设为当前时间;对于有 UpdatedAt 字段的模型,更新记录时,将该字段的值设为当前时间。创建记录时,如果该字段值为零值,则将该字段的值设为当前时间;可以通过将 autoUpdateTime 标签置为 false 来禁用时间戳追踪

    type User struct {
      UpdatedAt time.Time `gorm:"autoUpdateTime:false"`
    }
    
     

gorm.Model

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"`
}

type User struct {
  gorm.Model
  Name string
}
 

定义指针与非指针的区别

问题原文:

The docs of gorm https://gorm.io/docs/models.html present an example below.
The field Name and Email are described with string and *string. What is the main difference here?
Also how to provide the datatype for the images field storing a list of images link?
Should it be []string or []*string?

答案:

Go has default values for every primitive data types.

int -> 0, string -> “”, bool -> false likewise. So if you need to add null value, or load null value to a variable, it should be a pointer. Otherwise it is defaulted.

Default value of a pointer is nil in Go.

And complex data types such as slices, maps keep references. So their default value is nil. So, Images []string here images can be nil.

Below code with pointer types User1 and without pointer types User2 show the difference in default values.

package main

import (
    "fmt"
    "time"
)

type User1 struct {
    Email        *string
    Images       []string
    Birthday     *time.Time
}

type User2 struct {
    Email        string
    Images       []string
    Birthday     time.Time
}

func main() {
    user1 := User1{}
    user2 := User2{}

    fmt.Printf("user1 := %+v \n", user1)
    //Output : user1 := {Email:<nil> Images:[] Birthday:<nil>}
    fmt.Printf("user2 := %+v \n", user2)
    //Output : user2 := {Email: Images:[] Birthday:0001-01-01 00:00:00 +0000 UTC}
}
 

假设定义如下模型:

type Password struct {
	gorm.Model
	Name      string
	ManagerID *uint
	// Team不会出现在表中
	Team []Password `gorm:"foreignkey:ManagerID"`
}
 

执行新增时:

db.Create(&Password{Name:"p1"}
[11.708ms] [rows:1] INSERT INTO `password` (`created_at`,`updated_at`,`deleted_at`,`name`,`manager_id`) VALUES ('2023-03-29 20:17:20.553','2023-03-29 20:17:20.553',NULL,'p1',NULL) 
 

高级选项(标签)

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 将列定义为唯一键
default 定义列的默认值
precision 指定列的精度
scale 指定列大小
not null 指定列为 NOT NULL
autoIncrement 指定列为自动增长
autoIncrementIncrement 自动步长,控制连续记录之间的间隔
embedded 嵌套字段
embeddedPrefix 嵌入字段的列名前缀
autoCreateTime 创建时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoCreateTime:nano
autoUpdateTime 创建/更新时追踪当前时间,对于 int 字段,它会追踪时间戳秒数,您可以使用 nano/milli 来追踪纳秒、毫秒时间戳,例如:autoUpdateTime:milli
index 根据参数创建索引,多个字段使用相同的名称则创建复合索引,查看 索引 获取详情
uniqueIndex index 相同,但创建的是唯一索引
check 创建检查约束,例如 check:age > 13,查看 约束 获取详情
<- 设置字段写入的权限, <-:create 只创建、<-:update 只更新、<-:false 无写入权限、<- 创建和更新权限
-> 设置字段读的权限,->:false 无读权限
- 忽略该字段,- 表示无读写,-:migration 表示无迁移权限,-:all 表示无读写迁移权限
comment 迁移时为字段添加注释

字段级权限控制

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

type User struct {
  Name string `gorm:"<-:create"` // 允许读和创建
  Name string `gorm:"<-:update"` // 允许读和更新
  Name string `gorm:"<-"`        // 允许读和写(创建和更新)
  Name string `gorm:"<-:false"`  // 允许读,禁止写
  Name string `gorm:"->"`        // 只读(除非有自定义配置,否则禁止写)
  Name string `gorm:"->;<-:create"` // 允许读和写
  Name string `gorm:"->:false;<-:create"` // 仅创建(禁止从 db 读)
  Name string `gorm:"-"`  // 通过 struct 读写会忽略该字段
  Name string `gorm:"-:all"`        // 通过 struct 读写、迁移会忽略该字段
  Name string `gorm:"-:migration"`  // 通过 struct 迁移会忽略该字段
}
 

创建/更新时间追踪(纳秒、毫秒、秒、Time)

  1. 想要保存 UNIX(毫/纳)秒时间戳,而不是 time,只需简单地将 time.Time 修改为 int 即可。
  2. GORM 约定使用 CreatedAt、UpdatedAt 追踪创建/更新时间。
type User struct {
  CreatedAt time.Time // 在创建时,如果该字段值为零值,则使用当前时间填充
  UpdatedAt int       // 在创建时该字段值为零值或者在更新时,使用当前时间戳秒数填充
  Updated   int64 `gorm:"autoUpdateTime:nano"` // 使用时间戳纳秒数填充更新时间
  Updated   int64 `gorm:"autoUpdateTime:milli"` // 使用时间戳毫秒数填充更新时间
  Created   int64 `gorm:"autoCreateTime"`      // 使用时间戳秒数填充创建时间
}
 

嵌入结构体

  1. 对于匿名字段,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
    }
    
     
  2. 对于正常的结构体字段,可以通过标签 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
    }
    
     
  3. 也可以使用标签 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
    }
    
     

模型迁移

AutoMigrate(大部分情况下够用)

  1. AutoMigrate 用于自动迁移模型。
  2. AutoMigrate 会创建表、缺失的外键、约束、列和索引。 如果大小、精度、是否为空可以更改,则 AutoMigrate 会改变列的类型,它不会删除未使用的列。
db.AutoMigrate(&User{})
或
db.AutoMigrate(new(User))

db.AutoMigrate(&User{}, &Product{}, &Order{})
或
db.AutoMigrate(new(User),new(Product),new(Order))

// 创建表时添加后缀
db.Set("gorm:table_options", "ENGINE=InnoDB").AutoMigrate(&User{})
 

Migrator 接口

  1. GORM 提供了 Migrator 接口,该接口为每个数据库提供了统一的 API 接口,可用来为数据库构建独立迁移
  2. GORM 将基于使用 MySQL 的版本执行不同 SQL。
type Migrator interface {
  // AutoMigrate
  AutoMigrate(dst ...interface{}) error

  // Database
  CurrentDatabase() string
  FullDataTypeOf(*schema.Field) clause.Expr

  // Tables
  CreateTable(dst ...interface{}) error
  DropTable(dst ...interface{}) error
  HasTable(dst interface{}) bool
  RenameTable(oldName, newName interface{}) error
  GetTables() (tableList []string, err error)

  // Columns
  AddColumn(dst interface{}, field string) error
  DropColumn(dst interface{}, field string) error
  AlterColumn(dst interface{}, field string) error
  MigrateColumn(dst interface{}, field *schema.Field, columnType ColumnType) error
  HasColumn(dst interface{}, field string) bool
  RenameColumn(dst interface{}, oldName, field string) error
  ColumnTypes(dst interface{}) ([]ColumnType, error)

  // Constraints
  CreateConstraint(dst interface{}, name string) error
  DropConstraint(dst interface{}, name string) error
  HasConstraint(dst interface{}, name string) bool

  // Indexes
  CreateIndex(dst interface{}, name string) error
  DropIndex(dst interface{}, name string) error
  HasIndex(dst interface{}, name string) bool
  RenameIndex(dst interface{}, oldName, newName string) error
}
 
  1. 获取Migrator对象

    migrator := handler.Migrator()
    
     
  2. 返回当前使用的数据库名

    migrator.CurrentDatabase()
    

  3. 表操作

    // 为 `User` 创建表
    db.Migrator().CreateTable(&User{})
    
    // 将 "ENGINE=InnoDB" 添加到创建 `User` 的 SQL 里去
    db.Set("gorm:table_options", "ENGINE=InnoDB").Migrator().CreateTable(&User{})
    
    // 检查 `User` 对应的表是否存在
    db.Migrator().HasTable(&User{})
    db.Migrator().HasTable("users")
    
    // 如果存在表则删除(删除时会忽略、删除外键约束)
    db.Migrator().DropTable(&User{})
    db.Migrator().DropTable("users")
    
    // 重命名表
    db.Migrator
                    
                    
                    
                    
                    
                    
                  
Name:
<提交>