1. 连接数据库

gorm是一个使用Go语言编写的ORM框架。它文档齐全,对开发者友好,支持MySQL,SQL Server,SQLite3,PostgreSQL。在对性能要求不高,或者数据库使用不频繁的项目中可以使用,如自动化部署系统等。Gorm支持以下特性:

  • 全功能 ORM
  • 关联 (Has One,Has Many,Belongs To,Many To Many,多态,单表继承)
  • Create,Save,Update,Delete,Find 中钩子方法
  • 支持 Preload、Joins 的预加载
  • 事务,嵌套事务,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…
  • 每个特性都经过了测试的重重考验
  • 开发者友好

image.png文档链接:
v2.x: https://gorm.io/zh_CN/docs/index.html
v1.x: https://v1.gorm.io/zh_CN/docs/index.html

1.1. 连接MySQL

1.1.1. v1.x 连接mysql

  1. var DB *gorm.DB
  2. func init() {
  3. // 这些配置应该从配置文件读取
  4. // 想要正确解析Time.time,需要使用 parseTime=True 参数
  5. mysqlInfo := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True", "root", "123456", "10.4.7.3:3306", "gorm")
  6. var err error
  7. DB, err = gorm.Open("mysql", mysqlInfo) // 会检测mysql信息是否正确
  8. if err != nil {
  9. logger.Fatalf("open mysql failed,error:%s", err.Error())
  10. return
  11. }
  12. if err = DB.DB().Ping(); err != nil { // 尝试是否能连接上mysql
  13. logger.Fatalf("connect mysql failed,error:%s", err.Error())
  14. return
  15. }
  16. // 设置连接池的属性
  17. DB.DB().SetMaxOpenConns(200)
  18. DB.DB().SetMaxIdleConns(20)
  19. DB.LogMode(true) // debug 模式,会显示执行的sql语句
  20. }

1.1.2. v2.0 连接mysql

  1. import (
  2. "fmt"
  3. "go_learn/logger"
  4. "gorm.io/driver/mysql"
  5. "gorm.io/gorm"
  6. glog "gorm.io/gorm/logger"
  7. )
  8. var DB *gorm.DB
  9. func init() {
  10. mysqlInfo := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True", "root", "123456", "10.4.7.3:3306", "gorm")
  11. var err error
  12. DB, err = gorm.Open(mysql.Open(mysqlInfo), &gorm.Config{
  13. Logger: glog.Default.LogMode(glog.Silent), // 添加该日志级别,不打印SQL语句.默认报错时打印SQL语句
  14. }) // 会检测是否能连接数据库
  15. if err != nil {
  16. logger.Fatalf("open mysql failed,error:%s", err.Error())
  17. return
  18. }
  19. sqlDB, err := DB.DB()
  20. if err != nil {
  21. logger.Fatalf("connect mysql failed,error:%s", err.Error())
  22. return
  23. }
  24. // 设置连接池的属性
  25. sqlDB.SetMaxOpenConns(200)
  26. sqlDB.SetMaxIdleConns(20)
  27. }

1.2. 连接SQLite3

1.2.1. v1.x 连接SQLite3

  1. import (
  2. "github.com/jinzhu/gorm"
  3. _ "github.com/jinzhu/gorm/dialects/sqlite"
  4. )
  5. func main() {
  6. db, err := gorm.Open("sqlite3", "/tmp/gorm.db")
  7. defer db.Close()
  8. }

1.2.2. v2.0 连接SQLite3

  1. import (
  2. "gorm.io/gorm"
  3. "gorm.io/driver/sqlite"
  4. )
  5. func main() {
  6. db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
  7. if err != nil {
  8. panic("failed to connect database")
  9. }
  10. }

2. 模型定义

所谓的模型定义,其实就是定义一个结构体,将这个结构体与数据表对应起来,将来从数据库查询到的数据直接用这个结构体对象接收,同能将该结构体对象直接保存至数据库中一条记录。定义好模型后,可通过自动迁移命令实现数据表创建和更新。

2.1. gorm.Model

gorm.Model是gorm内置的一个GoLang结构体,可以嵌入用户定义的模型中,其包含四个字段,主键ID、创建时间、更新时间、删除时间。这四个字段会在数据表发生变化的时候自动更新。

  • 默认情况下,GORM 会使用 ID 作为表的主键,除非显示指定了主键
  • 对于有 CreatedAt 字段的模型,创建记录时,如果该字段值为零值,则将该字段的值设为当前时间
  • 对于有 UpdatedAt 字段的模型,更新记录时,将该字段的值设为当前时间。创建记录时,如果该字段值为零值,则将该字段的值设为当前时间
  • 对于有 DeleteAt 字段的模型,默认将其作为逻辑删除参数。不同版本中DeleteAt字段数据类型可能不同

如果产品需求中,需要以上的四个字段,则可用直接嵌套 gorm.Model 结构体。

  1. type Model struct {
  2. ID uint `gorm:"primary_key"` // 主键,自增ID
  3. CreatedAt time.Time // 创建时间,如果为空值,则默认使用当前时间。填充到秒
  4. UpdatedAt time.Time // 创建时间,如果为空值,则默认使用当前时间。填充到秒
  5. DeletedAt gorm.DeletedAt `gorm:"index"` // 2.0 版本,逻辑删除
  6. // DeletedAt *time.Time `gorm:"index"` // 1.x 版本,逻辑删除
  7. }

2.2. gorm 的tag

2.2.1. 基本字段的tag属性

注意: 在type中可以指定字段属性,比如主键、唯一约束等,但是非常不推荐。原因是,写在type中的属性不能被gorm识别到,从而导致部分gorm的方法出现异常。比如使用 DB.Save() 方法无法正确识别主键,导致执行的是Insert 语句而不是Update!

标签名 说明
column 指定 db 列名
type 列数据类型,推荐使用兼容性好的通用类型,例如:所有数据库都支持 bool、int、uint、float、string、time、bytes 并且可以和其他标签一起使用,例如:not null
size
, autoIncrement
… 像 varbinary(8)
这样指定数据库数据类型也是支持的。在使用指定数据库数据类型时,它需要是完整的数据库数据类型,如:MEDIUMINT UNSINED not NULL AUTO_INSTREMENT
size 指定列大小,例如:size:256
primaryKey 指定列为主键
unique 指定列为唯一
default 指定列的默认值。如果默认是字符串,需要用单引号。否则不起作用
precision 指定列的精度
scale 指定列大小
not null 指定列为 NOT NULL
autoIncrement 指定列为自动增长
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
无读权限
- 忽略该字段,-
无读写权限

2.2.2. 指定外键相关约束

foreignKey 指定外键
references 指定引用
polymorphic 指定多态类型
polymorphicValue 指定多态值、默认表名
many2many 指定连接表表名
jointForeignKey 指定连接表的外键
joinReferences 指定连接表的引用外键
constraint 关系约束,例如:OnUpdate
OnDelete

2.3. 模型定义案例

模型定义的字段类型为基本数据类型、实现了 Scanner 和 Valuer 接口的自定义数据类型。连接的数据库为MySQL

2.3.1. 基础数据类型的模型

以下描述了一个节点主机的数据模型,为了方便,将约束放到了 type 字段中,针对mysql数据库,调整了对于的数据类型,因此相关约束需要放到 type 中,否则可能报错或者不生效。

  1. type Node struct {
  2. UUID string `gorm:"type:varchar(36) primary key"`
  3. Name string `gorm:"type:varchar(32) unique not null"`
  4. IP string `gorm:"type:char(15);index"`
  5. Username string `gorm:"type:varchar(32) default 'root'"`
  6. Password string `gorm:"type:varchar(256)"`
  7. SSHPort int `gorm:"type:smallint unsigned default 22"`
  8. SSHKey string `gorm:"type:text"`
  9. CreatedAt time.Time
  10. UpdatedAt time.Time
  11. DeletedAt *time.Time `gorm:"index"`
  12. }
  1. // 迁移函数
  2. func migrate() {
  3. DB.Set("gorm:table_options", "CHARSET=utf8mb4").AutoMigrate(&Node{})
  4. }
  1. mysql> desc nodes ;
  2. +-----------+----------------------+------+-----+---------+-------+
  3. | Field | Type | Null | Key | Default | Extra |
  4. +-----------+----------------------+------+-----+---------+-------+
  5. | uuid | varchar(36) | NO | PRI | NULL | |
  6. | name | varchar(32) | NO | UNI | NULL | |
  7. | ip | char(15) | YES | MUL | NULL | |
  8. | username | varchar(32) | YES | | root | |
  9. | password | varchar(256) | YES | | NULL | |
  10. | ssh_port | smallint(5) unsigned | YES | | 22 | |
  11. | ssh_key | text | YES | | NULL | |
  12. | create_at | datetime | YES | | NULL | |
  13. | update_at | datetime | YES | | NULL | |
  14. | delete_at | datetime | YES | MUL | NULL | |
  15. +-----------+----------------------+------+-----+---------+-------+
  16. 10 rows in set (0.00 sec)
  17. mysql> show create table nodes \G
  18. *************************** 1. row ***************************
  19. Table: nodes
  20. Create Table: CREATE TABLE `nodes` (
  21. `uuid` varchar(36) NOT NULL,
  22. `name` varchar(32) NOT NULL,
  23. `ip` char(15) DEFAULT NULL,
  24. `username` varchar(32) DEFAULT 'root',
  25. `password` varchar(256) DEFAULT NULL,
  26. `ssh_port` smallint(5) unsigned DEFAULT '22',
  27. `ssh_key` text,
  28. `create_at` datetime DEFAULT NULL,
  29. `update_at` datetime DEFAULT NULL,
  30. `delete_at` datetime DEFAULT NULL,
  31. PRIMARY KEY (`uuid`),
  32. UNIQUE KEY `name` (`name`),
  33. KEY `idx_nodes_ip` (`ip`),
  34. KEY `idx_nodes_delete_at` (`delete_at`)
  35. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

2.3.2. 结构体嵌套

这里的结构体嵌套是指 A 结构体 嵌套了 B结构体,并且B结构体字段作为A结构体的平级字段。

  1. type User struct {
  2. Username string `gorm:"type:varchar(32) default 'root'"`
  3. Password string `gorm:"type:varchar(256)"`
  4. SSHPort int `gorm:"type:smallint unsigned default 22"`
  5. SSHKey string `gorm:"type:text"`
  6. }
  7. type Master struct {
  8. UUID string `gorm:"type:varchar(36) primary key"`
  9. Name string `gorm:"type:varchar(32) unique not null"`
  10. IP string `gorm:"type:char(15);index"`
  11. User
  12. CreatedAt time.Time
  13. UpdatedAt time.Time
  14. DeleteAt *time.Time `gorm:"index"`
  15. }
  16. // 等同于
  17. type Master struct {
  18. UUID string `gorm:"type:varchar(36) primary key"`
  19. Name string `gorm:"type:varchar(32) unique not null"`
  20. IP string `gorm:"type:char(15);index"`
  21. Username string `gorm:"type:varchar(32) default 'root'"`
  22. Password string `gorm:"type:varchar(256)"`
  23. SSHPort int `gorm:"type:smallint unsigned default 22"`
  24. SSHKey string `gorm:"type:text"`
  25. CreatedAt time.Time
  26. UpdatedAt time.Time
  27. DeleteAt *time.Time `gorm:"index"`
  28. }
  1. // gorm.Model 嵌入
  2. type Master struct {
  3. gorm.Model
  4. Name string `gorm:"type:varchar(32) unique not null"`
  5. IP string `gorm:"type:char(15);index"`
  6. Username string `gorm:"type:varchar(32) default 'root'"`
  7. Password string `gorm:"type:varchar(256)"`
  8. SSHPort int `gorm:"type:smallint unsigned default 22"`
  9. SSHKey string `gorm:"type:text"`
  10. }
  11. // 等同于
  12. type Master struct {
  13. ID uint `gorm:"primary_key"`
  14. CreatedAt time.Time
  15. UpdatedAt time.Time
  16. DeletedAt *time.Time `sql:"index"`
  17. Name string `gorm:"type:varchar(32) unique not null"`
  18. IP string `gorm:"type:char(15);index"`
  19. Username string `gorm:"type:varchar(32) default 'root'"`
  20. Password string `gorm:"type:varchar(256)"`
  21. SSHPort int `gorm:"type:smallint unsigned default 22"`
  22. SSHKey string `gorm:"type:text"`
  23. }

对于非匿名字段的结构体嵌入,可以使用tag来实现平级字段的效果。

  1. type User struct {
  2. Username string `gorm:"type:varchar(32) default 'root'"`
  3. Password string `gorm:"type:varchar(256)"`
  4. SSHPort int `gorm:"type:smallint unsigned default 22"`
  5. SSHKey string `gorm:"type:text"`
  6. }
  7. type Master struct {
  8. Name string `gorm:"type:varchar(32) unique not null"`
  9. IP string `gorm:"type:char(15);index"`
  10. User User `gorm:"embedded"`
  11. }
  12. // 相当于
  13. type Master struct {
  14. Name string `gorm:"type:varchar(32) unique not null"`
  15. IP string `gorm:"type:char(15);index"`
  16. Username string `gorm:"type:varchar(32) default 'root'"`
  17. Password string `gorm:"type:varchar(256)"`
  18. SSHPort int `gorm:"type:smallint unsigned default 22"`
  19. SSHKey string `gorm:"type:text"`
  20. }

2.3.3. 自定义类型-map

在开发中,经常会将map或者结构体对象作为一个字段存入数据库,比如json字段的一级字段会进行拆分存储到数据库,但是更深层级的字段直接用字符串的方式存入数据库,为了方便存储和读取,会对该类型实现 Scanner 和 Valuer 接口。其中 Scanner 用于将 DB 中记录转为 Go 中对象,Valuer 将 Go 中对象转为 DB 中记录。

  1. // model/master.go
  2. type Master struct {
  3. UUID string `gorm:"type:varchar(36) primary key"`
  4. Name string `gorm:"type:varchar(32) unique not null"`
  5. IP string `gorm:"type:char(15);index"`
  6. User map[string]string `gorm:"type:text"`
  7. }
  8. func NewMaster(uuid, name, ip string, user map[string]string) *Master {
  9. return &Master{
  10. UUID: uuid,
  11. Name: name,
  12. IP: ip,
  13. User: user,
  14. }
  15. }
  16. func (m *Master) Create() error {
  17. return DB.Create(m).Error
  18. }
  19. func GetMasterRecord(column, condition string) (*Master, error) {
  20. master := &Master{}
  21. err := DB.Where(fmt.Sprintf("%s = ?", column), condition).First(master).Error
  22. return master, err
  23. }
  1. func main() {
  2. user := map[string]string{
  3. "name": "root",
  4. "password": "123456",
  5. }
  6. master := model.NewMaster("207720a7-0d8a-4864-a445-e435148d303f", "master-01", "10.4.7.5", user)
  7. err := master.Create()
  8. if err != nil {
  9. logger.Errorf("Insert failed, err:%s", err.Error())
  10. return
  11. }
  12. master, err = model.GetMasterRecord("uuid", "207720a7-0d8a-4864-a445-e435148d303f")
  13. if err != nil {
  14. logger.Errorf("Insert failed, err:%s", err.Error())
  15. return
  16. }
  17. fmt.Printf("%#v\n", master)
  18. }
  1. [root@duduniao orm]# go run main.go
  2. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:31)
  3. [2020-09-06 15:39:57] sql: converting argument $4 type: unsupported type map[string]string, a map
  4. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:31)
  5. [2020-09-06 15:39:57] [0.15ms] INSERT INTO `masters` (`uuid`,`name`,`ip`,`user`) VALUES ('207720a7-0d8a-4864-a445-e435148d303f','master-01','10.4.7.5','map[name:root password:123456]')
  6. [0 rows affected or returned ]
  7. 2020-09-06 15:39:57.56|Insert failed, err:sql: converting argument $4 type: unsupported type map[string]string, a map

以上代码在执行过程中,报错: unsupported type map[string]string ,即无法将 map[string]string 作为文本插入数据库中。对User字段做以下调整:

  1. // 自定义类型别名
  2. type User map[string]string
  3. // 实现Valuer接口,将 Go --> DB(string)
  4. func (u User) Value() (driver.Value, error) {
  5. if u == nil {
  6. return nil, nil
  7. }
  8. marshal, _ := json.Marshal(u)
  9. if string(marshal) == "{}" {
  10. return nil, nil
  11. }
  12. return marshal, nil
  13. }
  14. // 实现Scanner接口,将 DB(string) --> Go
  15. func (u *User) Scan(src interface{}) error {
  16. if src == nil {
  17. return nil
  18. }
  19. err := json.Unmarshal(src.([]byte), u)
  20. if err != nil {
  21. return err
  22. }
  23. return nil
  24. }
  1. type Master struct {
  2. UUID string `gorm:"type:varchar(36) primary key"`
  3. Name string `gorm:"type:varchar(32) unique not null"`
  4. IP string `gorm:"type:char(15);index"`
  5. User User `gorm:"type:text"` // 修改类型
  6. }
  1. mysql> select * from masters ;
  2. +--------------------------------------+-----------+----------+-------------------------------------+
  3. | uuid | name | ip | user |
  4. +--------------------------------------+-----------+----------+-------------------------------------+
  5. | 207720a7-0d8a-4864-a445-e435148d303f | master-01 | 10.4.7.5 | {"name":"root","password":"123456"} |
  6. +--------------------------------------+-----------+----------+-------------------------------------+
  1. [root@duduniao orm]# go run main.go
  2. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:22)
  3. [2020-09-06 16:11:41] [1.19ms] INSERT INTO `masters` (`uuid`,`name`,`ip`,`user`) VALUES ('207720a7-0d8a-4864-a445-e435148d303f','master-01','10.4.7.5','[123 34 110 97 109 101 34 58 34 114 111 111 116 34 44 34 112 97 115 115 119 11
  4. 1 114 100 34 58 34 49 50 51 52 53 54 34 125]')
  5. [1 rows affected or returned ]
  6. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:27)
  7. [2020-09-06 16:11:41] [2.06ms] SELECT * FROM `masters` WHERE (uuid = '207720a7-0d8a-4864-a445-e435148d303f') LIMIT 1
  8. [1 rows affected or returned ]
  9. &model.Master{UUID:"207720a7-0d8a-4864-a445-e435148d303f", Name:"master-01", IP:"10.4.7.5", User:model.User{"name":"root", "password":"123456"}}, password:123456

针对User为空或者零值情况:

  1. func main() {
  2. user := make(map[string]string, 2)
  3. err := model.NewMaster("bebf9dd7-11f8-4777-a2ad-a6a83cda3a94", "master-02", "10.4.7.6", user).Create()
  4. if err != nil {
  5. logger.Errorf("Insert failed, err:%s", err.Error())
  6. return
  7. }
  8. //
  9. err = model.NewMaster("542d52c1-6209-45c5-b65e-232fbbefe505", "master-03", "10.4.7.6", nil).Create()
  10. if err != nil {
  11. logger.Errorf("Insert failed, err:%s", err.Error())
  12. return
  13. }
  14. //
  15. record, err := model.GetMasterRecord("uuid", "542d52c1-6209-45c5-b65e-232fbbefe505")
  16. if err != nil {
  17. logger.Errorf("Query failed, err:%s", err.Error())
  18. return
  19. }
  20. fmt.Printf("%#v\n", *record)
  21. }
  1. [root@duduniao orm]# go run main.go
  2. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:22)
  3. [2020-09-06 16:27:25] [1.91ms] INSERT INTO `masters` (`uuid`,`name`,`ip`,`user`) VALUES ('bebf9dd7-11f8-4777-a2ad-a6a83cda3a94','master-02','10.4.7.6',NULL)
  4. [1 rows affected or returned ]
  5. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:22)
  6. [2020-09-06 16:27:25] [3.16ms] INSERT INTO `masters` (`uuid`,`name`,`ip`,`user`) VALUES ('542d52c1-6209-45c5-b65e-232fbbefe505','master-03','10.4.7.6',NULL)
  7. [1 rows affected or returned ]
  8. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:27)
  9. [2020-09-06 16:27:25] [1.49ms] SELECT * FROM `masters` WHERE (uuid = '542d52c1-6209-45c5-b65e-232fbbefe505') LIMIT 1
  10. [1 rows affected or returned ]
  11. model.Master{UUID:"542d52c1-6209-45c5-b65e-232fbbefe505", Name:"master-03", IP:"10.4.7.6", User:model.User(nil)}

注意点:

  • Value() 方法接收者与model中定义的类型必须一致,比如 Master 中为User类型,那么Value()的接收者就不能是 *User
  • 当数据库字段为文本型,如text,varchar,存入和读取都用 []byte 类型处理。Value()返回值可以是字符也可以为[]byte,Scan()方法取出的 src 为[]byte 类型
  • Value() 可用灵活处理零值或者空值问题,采用合理的字段存入数据库。比如上述案例中,当 User 序列化后为 {} 时,存入的依旧是 NULL

    2.3.4. 自定义类型-struct

    与map类似,需要自定义 Scan() 和 Value() 方法: ```go // Config 类型 type Config struct { Ports []int Command string LogLevel string }

func (c Config)Value() (driver.Value, error) { if reflect.ValueOf(c).IsZero() { return nil, nil } marshal, err := json.Marshal(c) return marshal, err }

func (c *Config)Scan(src interface{}) error { if src == nil { return nil } err := json.Unmarshal(src.([]byte), c) return err }

type Service struct { UUID string gorm:"type:varchar(36) primary key" Name string gorm:"type:varchar(32) unique not null" Config Config gorm:"type:text" }

func (s *Service)Create() error { return DB.Create(s).Error }

func GetSvcRecord(column, condition string) (*Service, error) { svc := &Service{} err := DB.Where(fmt.Sprintf(“%s = ?”, column), condition).First(svc).Error return svc, err }

  1. ```go
  2. func service(svc model.Service) {
  3. // 插入记录
  4. if err := svc.Create(); err != nil {
  5. logger.Errorf("Insert failed, err:%s", err.Error())
  6. return
  7. }
  8. // 查询记录
  9. record, err := model.GetSvcRecord("uuid", svc.UUID)
  10. if err != nil {
  11. logger.Errorf("Query failed, err:%s", err.Error())
  12. return
  13. }
  14. fmt.Printf("%#v, prots:%v\n", record, record.Config.Ports)
  15. }
  16. func main() {
  17. svc := model.Service{
  18. UUID: "92670658-4595-4efb-b7a6-d87e7c3f7946",
  19. Name: "nginx",
  20. Config: model.Config{
  21. Ports: []int{80, 443},
  22. },
  23. }
  24. service(svc)
  25. //
  26. svc = model.Service{
  27. UUID: "f73ce9c0-6e48-424f-ae03-d5de71bc386a",
  28. Name: "httpd",
  29. Config: model.Config{},
  30. }
  31. service(svc)
  32. }
  1. mysql> select * from services ;
  2. +--------------------------------------+-------+-----------------------------------------------+
  3. | uuid | name | config |
  4. +--------------------------------------+-------+-----------------------------------------------+
  5. | 92670658-4595-4efb-b7a6-d87e7c3f7946 | nginx | {"Ports":[80,443],"Command":"","LogLevel":""} |
  6. | f73ce9c0-6e48-424f-ae03-d5de71bc386a | httpd | NULL |
  7. +--------------------------------------+-------+-----------------------------------------------+
  1. [2020-09-06 17:09:38] [1.75ms] INSERT INTO `services` (`uuid`,`name`,`config`) VALUES ('92670658-4595-4efb-b7a6-d87e7c3f7946','nginx','[123 34 80 111 114 116 115 34 58 91 56 48 44 52 52 51 93 44 34 67 111 109 109 97 110 100 34 58
  2. 34 34 44 34 76 111 103 76 101 118 101 108 34 58 34 34 125]')
  3. [1 rows affected or returned ]
  4. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/service.go:17)
  5. [2020-09-06 17:09:38] [1.67ms] SELECT * FROM `services` WHERE (uuid = '92670658-4595-4efb-b7a6-d87e7c3f7946') LIMIT 1
  6. [1 rows affected or returned ]
  7. &model.Service{UUID:"92670658-4595-4efb-b7a6-d87e7c3f7946", Name:"nginx", Config:model.Config{Ports:[]int{80, 443}, Command:"", LogLevel:""}}, prots:[80 443]
  8. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/service.go:12)
  9. [2020-09-06 17:09:38] [1.94ms] INSERT INTO `services` (`uuid`,`name`,`config`) VALUES ('f73ce9c0-6e48-424f-ae03-d5de71bc386a','httpd',NULL)
  10. [1 rows affected or returned ]
  11. (/mnt/e/Projects/learn/go_learn/day16-db/orm/model/service.go:17)
  12. [2020-09-06 17:09:38] [2.35ms] SELECT * FROM `services` WHERE (uuid = 'f73ce9c0-6e48-424f-ae03-d5de71bc386a') LIMIT 1
  13. [1 rows affected or returned ]
  14. &model.Service{UUID:"f73ce9c0-6e48-424f-ae03-d5de71bc386a", Name:"httpd", Config:model.Config{Ports:[]int(nil), Command:"", LogLevel:""}}, prots:[]

2.3.5. 外键


3. 增删改查

3.1. 创建新记录

  1. // 数据表
  2. type Node struct {
  3. UUID string `gorm:"type:varchar(36) primary key"`
  4. Name string `gorm:"type:varchar(32) unique not null"`
  5. IP string `gorm:"type:char(15);index"`
  6. Username string `gorm:"type:varchar(32);default:'root'"`
  7. Password string `gorm:"type:varchar(256)"`
  8. SSHPort int `gorm:"type:smallint unsigned;default:22"`
  9. SSHKey string `gorm:"type:text"`
  10. CreatedAt time.Time
  11. UpdatedAt time.Time
  12. DeleteAt gorm.DeletedAt `gorm:"index"`
  13. }
  14. func NewNode(uuid, name, ip, username, password, sshKey string, port int) *Node {
  15. return &Node{
  16. UUID: uuid,
  17. IP: ip,
  18. Username: username,
  19. Password: password,
  20. SSHKey: sshKey,
  21. SSHPort: port,
  22. Name: name,
  23. }
  24. }
  25. func (n *Node) Create() error {
  26. return DB.Create(n).Error
  27. }

3.1.1. 通过结构体创建单条记录

在一般的情况下,使用结构体对象指针方式插入数据比较常见,避免使用结构体对象作为参数传递,否则导致部分字段无法正确填充数值,比如 CreatedAt 字段。在插入记录过程中,如果对象的该字段为 零值,这些字段定义的默认值不会被保存到数据库,您需要使用指针类型或 Scanner/Valuer 来避免这个问题。如下面两条记录在空值时,采用了 default 的值

  1. func insert() {
  2. node := model.NewNode("b1d504fe-0768-4dd0-bc09-e3c1ee0396b2","node-01","10.4.7.3","root","123456","",0)
  3. if err := node.Create(); err != nil {
  4. logger.Errorf("Insert %s failed, err:%s", node.Name, err.Error())
  5. return
  6. }
  7. //
  8. node = model.NewNode("2b8b9e30-14f2-49cc-a2a0-0294dd3e421d","node-02","10.4.7.3","","","",22)
  9. if err := node.Create(); err != nil {
  10. logger.Errorf("Insert %s failed, err:%s", node.Name, err.Error())
  11. return
  12. }
  13. }
  1. mysql> select * from nodes ;
  2. +--------------------------------------+---------+----------+----------+----------+----------+---------+---------------------+---------------------+-----------+
  3. | uuid | name | ip | username | password | ssh_port | ssh_key | created_at | updated_at | delete_at |
  4. +--------------------------------------+---------+----------+----------+----------+----------+---------+---------------------+---------------------+-----------+
  5. | 2b8b9e30-14f2-49cc-a2a0-0294dd3e421d | node-02 | 10.4.7.3 | root | | 22 | | 2020-09-06 14:32:17 | 2020-09-06 14:32:17 | NULL |
  6. | b1d504fe-0768-4dd0-bc09-e3c1ee0396b2 | node-01 | 10.4.7.3 | root | 123456 | 22 | | 2020-09-06 14:32:17 | 2020-09-06 14:32:17 | NULL |
  7. +--------------------------------------+---------+----------+----------+----------+----------+---------+---------------------+---------------------+-----------+

同通过结构体列表插入多记录的功能仅 2.x 版本支持

  1. func insertList() {
  2. node01 := model.NewNode("b1d504fe-0768-4dd0-bc09-e3c1ee0396b2", "node-01", "10.4.7.3", "duduniao", "123456", "x", 22)
  3. node02 := model.NewNode("4f89f716-383d-495c-a392-3af34ae61638", "node-02", "10.4.7.4", "ansible", "123456", "x", 2222)
  4. node03 := model.NewNode("c023038c-da5f-4a82-8e5c-ffa6ae36dab3", "node-03", "10.4.7.5", "ansible", "123456", "x", 2222)
  5. nodes := []*model.Node{node01, node02, node03}
  6. if err := model.DB.Create(nodes).Error; err != nil {
  7. logger.Errorf("Insert failed, err:%s", err.Error())
  8. return
  9. }
  10. }
  11. func main() {
  12. insertList()
  13. }
  1. [root@duduniao orm]# go run main.go
  2. 2020/09/11 00:09:56 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:84
  3. [4.244ms] [rows:3] INSERT INTO `nodes` (`uuid`,`name`,`ip`,`username`,`password`,`ssh_port`,`ssh_key`,`created_at`,`updated_at`,`delete_at`) VALUES ("b1d504fe-0768-4dd0-bc09-e3c1ee0396b2","node-01","10.4.7.3","duduniao","123456",22,"x","2020-09-11 00:09:56.969","2020-09-11 00:09:56.969",NULL),("4f89f716-383d-495c-a392-3af34ae61638","node-02","10.4.7.4","ansible","123456",2222,"x","2020-09-11 00:09:56.969","2020-09-11 00:09:56.969",NULL),("c023038c-da5f-4a82-8e5c-ffa6ae36dab3","node-03","10.4.7.5","ansible","123456",2222,"x","2020-09-11 00:09:56.969","2020-09-11 00:09:56.969",NULL)

3.1.3. 使用map创建记录

此功能仅 v2.x 支持。使用 map 插入记录时,不会自动填充时间戳和主键ID,一般不推荐使用这种方式。同样, []map[string]interface{} 可以批量插入多条记录。

  1. func insert() {
  2. node := map[string]interface{}{
  3. "uuid": "b1d504fe-0768-4dd0-bc09-e3c1ee0396b2",
  4. "name": "node",
  5. "ip": "10.4.7.3",
  6. "username": "duduniao",
  7. "password": "123456",
  8. "ssh_port": 22,
  9. "ssh_key": "xxx",
  10. }
  11. if err:=model.DB.Model(&model.Node{}).Create(node).Error; err != nil {
  12. logger.Errorf("Insert failed, err:%s",err.Error())
  13. return
  14. }
  15. }
  16. func main() {
  17. insert()
  18. }

3.2. 更新记录

  1. // 数据表
  2. type Node struct {
  3. UUID string `gorm:"type:varchar(36);primary_key"`
  4. Name string `gorm:"type:varchar(32);unique;not null"`
  5. IP string `gorm:"type:char(15);index"`
  6. Username string `gorm:"type:varchar(32);default:'root'"`
  7. Password string `gorm:"type:varchar(256)"`
  8. SSHPort int `gorm:"type:smallint unsigned;default:22"`
  9. SSHKey string `gorm:"type:text"`
  10. CreatedAt time.Time
  11. UpdatedAt time.Time
  12. DeleteAt *time.Time `gorm:"index"`
  13. }

3.2.1. 保存所有字段

使用 Save() 方法可以更新当前对象的所有字段,如果model中没有指定主键(tag中type内主键不能被识别),则执行 insert 语句,存在主键则执行 update。

  1. func main() {
  2. var node model.Node
  3. model.DB.First(&node)
  4. fmt.Printf("%#v\n", node)
  5. node.Username = "duduniao"
  6. model.DB.Save(&node)
  7. }
  1. [root@duduniao orm]# go run main.go
  2. (/mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:95)
  3. [2020-09-10 21:48:09] [0.77ms] SELECT * FROM `nodes` ORDER BY `nodes`.`uuid` ASC LIMIT 1
  4. [1 rows affected or returned ]
  5. model.Node{UUID:"b1d504fe-0768-4dd0-bc09-e3c1ee0396b2", Name:"node-01", IP:"10.4.7.3", Username:"root", Password:"123456", SSHPort:22, SSHKey:"x", CreatedAt:time.Time{wall:0x0, ext:63735340576, loc:(*time.Location)(nil)}, UpdatedAt:
  6. time.Time{wall:0x0, ext:63735342450, loc:(*time.Location)(nil)}, DeleteAt:(*time.Time)(nil)}
  7. (/mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:98)
  8. [2020-09-10 21:48:09] [1.27ms] UPDATE `nodes` SET `name` = 'node-01', `ip` = '10.4.7.3', `username` = 'duduniao', `password` = '123456', `ssh_port` = 22, `ssh_key` = 'x', `created_at` = '2020-09-10 13:16:16', `updated_at` = '2020-
  9. 09-10 21:48:09', `delete_at` = NULL WHERE `nodes`.`uuid` = 'b1d504fe-0768-4dd0-bc09-e3c1ee0396b2'

3.2.2. 更新单列

更新单列,即更新数据表中的单个字段,这种情况下需要指定条件,否则会报错。更新多条记录时,不会更新 updatedAt 字段。UpdateColumn 不更新 UpdatedAt 字段。

  1. func main() {
  2. err := model.DB.Model(&model.Node{}).Where("uuid = ?", "b1d504fe-0768-4dd0-bc09-e3c1ee0396b2").Update("username", "ubuntu").Error
  3. if err != nil {
  4. logger.Errorf("update failed,err:%s", err.Error())
  5. return
  6. }
  7. }
  8. // UPDATE `nodes` SET `username`="ubuntu",`updated_at`="2020-09-11 21:42:38.769" WHERE uuid = "b1d504fe-0768-4dd0-bc09-e3c1ee0396b2"
  1. func main() {
  2. var node model.Node
  3. model.DB.Where("username = ?", "ansible").First(&node) // 根据where条件选择
  4. model.DB.Model(&node).Update("username", "root")
  5. }
  6. // UPDATE `nodes` SET `username`="root",`updated_at`="2020-09-11 21:47:31.204" WHERE `uuid` = "4f89f716-383d-495c-a392-3af34ae61638"

3.2.3. 更新多列

同时更新多个列的数据,有两种方案:通过结构体来更新、通过map来更新。通过结构体字段更新,默认忽略零值字段,而map更新不会忽略任何字段。如果非要使用结构体来更新零值字段,可以使用 Select 选择要更新的字段。一般都是推荐使用map来更新。同样,在更新数据表时,需要指定where条件。更新多条记录时,不会更新 updatedAt 字段。UpdateColumns 不更新 UpdatedAt 字段。

  1. func main() {
  2. req := map[string]interface{}{
  3. "username": "duduniao",
  4. "password": "666666",
  5. }
  6. if err := model.DB.Table("nodes").Where("username != 'root' ").Updates(req).Error; err != nil {
  7. logger.Errorf("Update failed, err:%s",err.Error())
  8. return
  9. }
  10. }
  11. //UPDATE `nodes` SET `password`="666666",`username`="duduniao" WHERE username != 'root'

3.2.4. 使用SQL表达式更新

gorm.Expr() 支持SQL表达式,如官方给出的案例:

  1. // product 的 ID 是 `3`
  2. DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
  3. // UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
  4. DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)})
  5. // UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
  6. DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
  7. // UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3;
  8. DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
  9. // UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3 AND quantity > 1;

3.3. 删除记录

3.3.1. 删除记录

可以根据指定的对象删除数据,也可以根据根据 where 条件删除。如果想要清空记录,可以使用恒为真的条件,比如 Where(“1 = 1”)。

  1. func main() {
  2. var node model.Node
  3. model.DB.First(&node)
  4. model.DB.Delete(&node)
  5. model.DB.Where("username = 'root'").Delete(&model.Node{})
  6. model.DB.Where("1 = 1").Delete(&model.Node{})
  7. }
  1. [root@heyingsheng orm]# go run main.go
  2. 2020/09/11 22:57:49 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:109
  3. [0.795ms] [rows:1] SELECT * FROM `nodes` WHERE `nodes`.`delete_at` IS NULL ORDER BY `nodes`.`uuid` LIMIT 1
  4. 2020/09/11 22:57:49 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:110
  5. [2.815ms] [rows:1] UPDATE `nodes` SET `delete_at`="2020-09-11 22:57:49.894" WHERE `nodes`.`uuid` = "4f89f716-383d-495c-a392-3af34ae61638"
  6. 2020/09/11 22:57:49 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:111
  7. [4.092ms] [rows:1] UPDATE `nodes` SET `delete_at`="2020-09-11 22:57:49.897" WHERE username = 'root'
  8. 2020/09/11 22:57:49 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:112
  9. [3.317ms] [rows:3] UPDATE `nodes` SET `delete_at`="2020-09-11 22:57:49.901" WHERE 1 = 1

3.3.2. 硬删除和软删除

在Gorm中使用 DeletedAt 字段可以设置为软删除或者称为逻辑删除,与上述案例中,删除操作仅仅填充了 DeletedAt 字段为当前的时间戳。在查询语句中,查询条件自动跳过了软删除的记录。

  1. func main() {
  2. var node model.Node
  3. model.DB.Unscoped().First(&node) // 查找被软删除的记录
  4. model.DB.Unscoped().Where("1=1").Delete(&node) // 永久删除
  5. }
  1. 2020/09/11 23:05:31 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:109
  2. [0.889ms] [rows:1] SELECT * FROM `nodes` ORDER BY `nodes`.`uuid` LIMIT 1
  3. 2020/09/11 23:05:31 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:110
  4. [3.476ms] [rows:1] DELETE FROM `nodes` WHERE 1=1 AND `nodes`.`uuid` = "4f89f716-383d-495c-a392-3af34ae61638"

3.4. 简单查询