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…
- 每个特性都经过了测试的重重考验
- 开发者友好
文档链接:
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
var DB *gorm.DB
func init() {
// 这些配置应该从配置文件读取
// 想要正确解析Time.time,需要使用 parseTime=True 参数
mysqlInfo := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True", "root", "123456", "10.4.7.3:3306", "gorm")
var err error
DB, err = gorm.Open("mysql", mysqlInfo) // 会检测mysql信息是否正确
if err != nil {
logger.Fatalf("open mysql failed,error:%s", err.Error())
return
}
if err = DB.DB().Ping(); err != nil { // 尝试是否能连接上mysql
logger.Fatalf("connect mysql failed,error:%s", err.Error())
return
}
// 设置连接池的属性
DB.DB().SetMaxOpenConns(200)
DB.DB().SetMaxIdleConns(20)
DB.LogMode(true) // debug 模式,会显示执行的sql语句
}
1.1.2. v2.0 连接mysql
import (
"fmt"
"go_learn/logger"
"gorm.io/driver/mysql"
"gorm.io/gorm"
glog "gorm.io/gorm/logger"
)
var DB *gorm.DB
func init() {
mysqlInfo := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True", "root", "123456", "10.4.7.3:3306", "gorm")
var err error
DB, err = gorm.Open(mysql.Open(mysqlInfo), &gorm.Config{
Logger: glog.Default.LogMode(glog.Silent), // 添加该日志级别,不打印SQL语句.默认报错时打印SQL语句
}) // 会检测是否能连接数据库
if err != nil {
logger.Fatalf("open mysql failed,error:%s", err.Error())
return
}
sqlDB, err := DB.DB()
if err != nil {
logger.Fatalf("connect mysql failed,error:%s", err.Error())
return
}
// 设置连接池的属性
sqlDB.SetMaxOpenConns(200)
sqlDB.SetMaxIdleConns(20)
}
1.2. 连接SQLite3
1.2.1. v1.x 连接SQLite3
import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
func main() {
db, err := gorm.Open("sqlite3", "/tmp/gorm.db")
defer db.Close()
}
1.2.2. v2.0 连接SQLite3
import (
"gorm.io/gorm"
"gorm.io/driver/sqlite"
)
func main() {
db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
}
2. 模型定义
所谓的模型定义,其实就是定义一个结构体,将这个结构体与数据表对应起来,将来从数据库查询到的数据直接用这个结构体对象接收,同能将该结构体对象直接保存至数据库中一条记录。定义好模型后,可通过自动迁移命令实现数据表创建和更新。
2.1. gorm.Model
gorm.Model是gorm内置的一个GoLang结构体,可以嵌入用户定义的模型中,其包含四个字段,主键ID、创建时间、更新时间、删除时间。这四个字段会在数据表发生变化的时候自动更新。
- 默认情况下,GORM 会使用 ID 作为表的主键,除非显示指定了主键
- 对于有 CreatedAt 字段的模型,创建记录时,如果该字段值为零值,则将该字段的值设为当前时间
- 对于有 UpdatedAt 字段的模型,更新记录时,将该字段的值设为当前时间。创建记录时,如果该字段值为零值,则将该字段的值设为当前时间
- 对于有 DeleteAt 字段的模型,默认将其作为逻辑删除参数。不同版本中DeleteAt字段数据类型可能不同
如果产品需求中,需要以上的四个字段,则可用直接嵌套 gorm.Model 结构体。
type Model struct {
ID uint `gorm:"primary_key"` // 主键,自增ID
CreatedAt time.Time // 创建时间,如果为空值,则默认使用当前时间。填充到秒
UpdatedAt time.Time // 创建时间,如果为空值,则默认使用当前时间。填充到秒
DeletedAt gorm.DeletedAt `gorm:"index"` // 2.0 版本,逻辑删除
// DeletedAt *time.Time `gorm:"index"` // 1.x 版本,逻辑删除
}
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 中,否则可能报错或者不生效。
type Node struct {
UUID string `gorm:"type:varchar(36) primary key"`
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
Username string `gorm:"type:varchar(32) default 'root'"`
Password string `gorm:"type:varchar(256)"`
SSHPort int `gorm:"type:smallint unsigned default 22"`
SSHKey string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `gorm:"index"`
}
// 迁移函数
func migrate() {
DB.Set("gorm:table_options", "CHARSET=utf8mb4").AutoMigrate(&Node{})
}
mysql> desc nodes ;
+-----------+----------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+----------------------+------+-----+---------+-------+
| uuid | varchar(36) | NO | PRI | NULL | |
| name | varchar(32) | NO | UNI | NULL | |
| ip | char(15) | YES | MUL | NULL | |
| username | varchar(32) | YES | | root | |
| password | varchar(256) | YES | | NULL | |
| ssh_port | smallint(5) unsigned | YES | | 22 | |
| ssh_key | text | YES | | NULL | |
| create_at | datetime | YES | | NULL | |
| update_at | datetime | YES | | NULL | |
| delete_at | datetime | YES | MUL | NULL | |
+-----------+----------------------+------+-----+---------+-------+
10 rows in set (0.00 sec)
mysql> show create table nodes \G
*************************** 1. row ***************************
Table: nodes
Create Table: CREATE TABLE `nodes` (
`uuid` varchar(36) NOT NULL,
`name` varchar(32) NOT NULL,
`ip` char(15) DEFAULT NULL,
`username` varchar(32) DEFAULT 'root',
`password` varchar(256) DEFAULT NULL,
`ssh_port` smallint(5) unsigned DEFAULT '22',
`ssh_key` text,
`create_at` datetime DEFAULT NULL,
`update_at` datetime DEFAULT NULL,
`delete_at` datetime DEFAULT NULL,
PRIMARY KEY (`uuid`),
UNIQUE KEY `name` (`name`),
KEY `idx_nodes_ip` (`ip`),
KEY `idx_nodes_delete_at` (`delete_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
2.3.2. 结构体嵌套
这里的结构体嵌套是指 A 结构体 嵌套了 B结构体,并且B结构体字段作为A结构体的平级字段。
type User struct {
Username string `gorm:"type:varchar(32) default 'root'"`
Password string `gorm:"type:varchar(256)"`
SSHPort int `gorm:"type:smallint unsigned default 22"`
SSHKey string `gorm:"type:text"`
}
type Master struct {
UUID string `gorm:"type:varchar(36) primary key"`
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
User
CreatedAt time.Time
UpdatedAt time.Time
DeleteAt *time.Time `gorm:"index"`
}
// 等同于
type Master struct {
UUID string `gorm:"type:varchar(36) primary key"`
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
Username string `gorm:"type:varchar(32) default 'root'"`
Password string `gorm:"type:varchar(256)"`
SSHPort int `gorm:"type:smallint unsigned default 22"`
SSHKey string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
DeleteAt *time.Time `gorm:"index"`
}
// gorm.Model 嵌入
type Master struct {
gorm.Model
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
Username string `gorm:"type:varchar(32) default 'root'"`
Password string `gorm:"type:varchar(256)"`
SSHPort int `gorm:"type:smallint unsigned default 22"`
SSHKey string `gorm:"type:text"`
}
// 等同于
type Master struct {
ID uint `gorm:"primary_key"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time `sql:"index"`
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
Username string `gorm:"type:varchar(32) default 'root'"`
Password string `gorm:"type:varchar(256)"`
SSHPort int `gorm:"type:smallint unsigned default 22"`
SSHKey string `gorm:"type:text"`
}
对于非匿名字段的结构体嵌入,可以使用tag来实现平级字段的效果。
type User struct {
Username string `gorm:"type:varchar(32) default 'root'"`
Password string `gorm:"type:varchar(256)"`
SSHPort int `gorm:"type:smallint unsigned default 22"`
SSHKey string `gorm:"type:text"`
}
type Master struct {
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
User User `gorm:"embedded"`
}
// 相当于
type Master struct {
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
Username string `gorm:"type:varchar(32) default 'root'"`
Password string `gorm:"type:varchar(256)"`
SSHPort int `gorm:"type:smallint unsigned default 22"`
SSHKey string `gorm:"type:text"`
}
2.3.3. 自定义类型-map
在开发中,经常会将map或者结构体对象作为一个字段存入数据库,比如json字段的一级字段会进行拆分存储到数据库,但是更深层级的字段直接用字符串的方式存入数据库,为了方便存储和读取,会对该类型实现 Scanner 和 Valuer 接口。其中 Scanner 用于将 DB 中记录转为 Go 中对象,Valuer 将 Go 中对象转为 DB 中记录。
// model/master.go
type Master struct {
UUID string `gorm:"type:varchar(36) primary key"`
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
User map[string]string `gorm:"type:text"`
}
func NewMaster(uuid, name, ip string, user map[string]string) *Master {
return &Master{
UUID: uuid,
Name: name,
IP: ip,
User: user,
}
}
func (m *Master) Create() error {
return DB.Create(m).Error
}
func GetMasterRecord(column, condition string) (*Master, error) {
master := &Master{}
err := DB.Where(fmt.Sprintf("%s = ?", column), condition).First(master).Error
return master, err
}
func main() {
user := map[string]string{
"name": "root",
"password": "123456",
}
master := model.NewMaster("207720a7-0d8a-4864-a445-e435148d303f", "master-01", "10.4.7.5", user)
err := master.Create()
if err != nil {
logger.Errorf("Insert failed, err:%s", err.Error())
return
}
master, err = model.GetMasterRecord("uuid", "207720a7-0d8a-4864-a445-e435148d303f")
if err != nil {
logger.Errorf("Insert failed, err:%s", err.Error())
return
}
fmt.Printf("%#v\n", master)
}
[root@duduniao orm]# go run main.go
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:31)
[2020-09-06 15:39:57] sql: converting argument $4 type: unsupported type map[string]string, a map
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:31)
[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]')
[0 rows affected or returned ]
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字段做以下调整:
// 自定义类型别名
type User map[string]string
// 实现Valuer接口,将 Go --> DB(string)
func (u User) Value() (driver.Value, error) {
if u == nil {
return nil, nil
}
marshal, _ := json.Marshal(u)
if string(marshal) == "{}" {
return nil, nil
}
return marshal, nil
}
// 实现Scanner接口,将 DB(string) --> Go
func (u *User) Scan(src interface{}) error {
if src == nil {
return nil
}
err := json.Unmarshal(src.([]byte), u)
if err != nil {
return err
}
return nil
}
type Master struct {
UUID string `gorm:"type:varchar(36) primary key"`
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
User User `gorm:"type:text"` // 修改类型
}
mysql> select * from masters ;
+--------------------------------------+-----------+----------+-------------------------------------+
| uuid | name | ip | user |
+--------------------------------------+-----------+----------+-------------------------------------+
| 207720a7-0d8a-4864-a445-e435148d303f | master-01 | 10.4.7.5 | {"name":"root","password":"123456"} |
+--------------------------------------+-----------+----------+-------------------------------------+
[root@duduniao orm]# go run main.go
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:22)
[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
1 114 100 34 58 34 49 50 51 52 53 54 34 125]')
[1 rows affected or returned ]
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:27)
[2020-09-06 16:11:41] [2.06ms] SELECT * FROM `masters` WHERE (uuid = '207720a7-0d8a-4864-a445-e435148d303f') LIMIT 1
[1 rows affected or returned ]
&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为空或者零值情况:
func main() {
user := make(map[string]string, 2)
err := model.NewMaster("bebf9dd7-11f8-4777-a2ad-a6a83cda3a94", "master-02", "10.4.7.6", user).Create()
if err != nil {
logger.Errorf("Insert failed, err:%s", err.Error())
return
}
//
err = model.NewMaster("542d52c1-6209-45c5-b65e-232fbbefe505", "master-03", "10.4.7.6", nil).Create()
if err != nil {
logger.Errorf("Insert failed, err:%s", err.Error())
return
}
//
record, err := model.GetMasterRecord("uuid", "542d52c1-6209-45c5-b65e-232fbbefe505")
if err != nil {
logger.Errorf("Query failed, err:%s", err.Error())
return
}
fmt.Printf("%#v\n", *record)
}
[root@duduniao orm]# go run main.go
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:22)
[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)
[1 rows affected or returned ]
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:22)
[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)
[1 rows affected or returned ]
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/master.go:27)
[2020-09-06 16:27:25] [1.49ms] SELECT * FROM `masters` WHERE (uuid = '542d52c1-6209-45c5-b65e-232fbbefe505') LIMIT 1
[1 rows affected or returned ]
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 序列化后为
{}
时,存入的依旧是 NULL2.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 }
```go
func service(svc model.Service) {
// 插入记录
if err := svc.Create(); err != nil {
logger.Errorf("Insert failed, err:%s", err.Error())
return
}
// 查询记录
record, err := model.GetSvcRecord("uuid", svc.UUID)
if err != nil {
logger.Errorf("Query failed, err:%s", err.Error())
return
}
fmt.Printf("%#v, prots:%v\n", record, record.Config.Ports)
}
func main() {
svc := model.Service{
UUID: "92670658-4595-4efb-b7a6-d87e7c3f7946",
Name: "nginx",
Config: model.Config{
Ports: []int{80, 443},
},
}
service(svc)
//
svc = model.Service{
UUID: "f73ce9c0-6e48-424f-ae03-d5de71bc386a",
Name: "httpd",
Config: model.Config{},
}
service(svc)
}
mysql> select * from services ;
+--------------------------------------+-------+-----------------------------------------------+
| uuid | name | config |
+--------------------------------------+-------+-----------------------------------------------+
| 92670658-4595-4efb-b7a6-d87e7c3f7946 | nginx | {"Ports":[80,443],"Command":"","LogLevel":""} |
| f73ce9c0-6e48-424f-ae03-d5de71bc386a | httpd | NULL |
+--------------------------------------+-------+-----------------------------------------------+
[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
34 34 44 34 76 111 103 76 101 118 101 108 34 58 34 34 125]')
[1 rows affected or returned ]
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/service.go:17)
[2020-09-06 17:09:38] [1.67ms] SELECT * FROM `services` WHERE (uuid = '92670658-4595-4efb-b7a6-d87e7c3f7946') LIMIT 1
[1 rows affected or returned ]
&model.Service{UUID:"92670658-4595-4efb-b7a6-d87e7c3f7946", Name:"nginx", Config:model.Config{Ports:[]int{80, 443}, Command:"", LogLevel:""}}, prots:[80 443]
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/service.go:12)
[2020-09-06 17:09:38] [1.94ms] INSERT INTO `services` (`uuid`,`name`,`config`) VALUES ('f73ce9c0-6e48-424f-ae03-d5de71bc386a','httpd',NULL)
[1 rows affected or returned ]
(/mnt/e/Projects/learn/go_learn/day16-db/orm/model/service.go:17)
[2020-09-06 17:09:38] [2.35ms] SELECT * FROM `services` WHERE (uuid = 'f73ce9c0-6e48-424f-ae03-d5de71bc386a') LIMIT 1
[1 rows affected or returned ]
&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. 创建新记录
// 数据表
type Node struct {
UUID string `gorm:"type:varchar(36) primary key"`
Name string `gorm:"type:varchar(32) unique not null"`
IP string `gorm:"type:char(15);index"`
Username string `gorm:"type:varchar(32);default:'root'"`
Password string `gorm:"type:varchar(256)"`
SSHPort int `gorm:"type:smallint unsigned;default:22"`
SSHKey string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
DeleteAt gorm.DeletedAt `gorm:"index"`
}
func NewNode(uuid, name, ip, username, password, sshKey string, port int) *Node {
return &Node{
UUID: uuid,
IP: ip,
Username: username,
Password: password,
SSHKey: sshKey,
SSHPort: port,
Name: name,
}
}
func (n *Node) Create() error {
return DB.Create(n).Error
}
3.1.1. 通过结构体创建单条记录
在一般的情况下,使用结构体对象指针方式插入数据比较常见,避免使用结构体对象作为参数传递,否则导致部分字段无法正确填充数值,比如 CreatedAt 字段。在插入记录过程中,如果对象的该字段为 零值,这些字段定义的默认值不会被保存到数据库,您需要使用指针类型或 Scanner/Valuer 来避免这个问题。如下面两条记录在空值时,采用了 default 的值
func insert() {
node := model.NewNode("b1d504fe-0768-4dd0-bc09-e3c1ee0396b2","node-01","10.4.7.3","root","123456","",0)
if err := node.Create(); err != nil {
logger.Errorf("Insert %s failed, err:%s", node.Name, err.Error())
return
}
//
node = model.NewNode("2b8b9e30-14f2-49cc-a2a0-0294dd3e421d","node-02","10.4.7.3","","","",22)
if err := node.Create(); err != nil {
logger.Errorf("Insert %s failed, err:%s", node.Name, err.Error())
return
}
}
mysql> select * from nodes ;
+--------------------------------------+---------+----------+----------+----------+----------+---------+---------------------+---------------------+-----------+
| uuid | name | ip | username | password | ssh_port | ssh_key | created_at | updated_at | delete_at |
+--------------------------------------+---------+----------+----------+----------+----------+---------+---------------------+---------------------+-----------+
| 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 |
| 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 |
+--------------------------------------+---------+----------+----------+----------+----------+---------+---------------------+---------------------+-----------+
同通过结构体列表插入多记录的功能仅 2.x 版本支持
func insertList() {
node01 := model.NewNode("b1d504fe-0768-4dd0-bc09-e3c1ee0396b2", "node-01", "10.4.7.3", "duduniao", "123456", "x", 22)
node02 := model.NewNode("4f89f716-383d-495c-a392-3af34ae61638", "node-02", "10.4.7.4", "ansible", "123456", "x", 2222)
node03 := model.NewNode("c023038c-da5f-4a82-8e5c-ffa6ae36dab3", "node-03", "10.4.7.5", "ansible", "123456", "x", 2222)
nodes := []*model.Node{node01, node02, node03}
if err := model.DB.Create(nodes).Error; err != nil {
logger.Errorf("Insert failed, err:%s", err.Error())
return
}
}
func main() {
insertList()
}
[root@duduniao orm]# go run main.go
2020/09/11 00:09:56 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:84
[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{}
可以批量插入多条记录。
func insert() {
node := map[string]interface{}{
"uuid": "b1d504fe-0768-4dd0-bc09-e3c1ee0396b2",
"name": "node",
"ip": "10.4.7.3",
"username": "duduniao",
"password": "123456",
"ssh_port": 22,
"ssh_key": "xxx",
}
if err:=model.DB.Model(&model.Node{}).Create(node).Error; err != nil {
logger.Errorf("Insert failed, err:%s",err.Error())
return
}
}
func main() {
insert()
}
3.2. 更新记录
// 数据表
type Node struct {
UUID string `gorm:"type:varchar(36);primary_key"`
Name string `gorm:"type:varchar(32);unique;not null"`
IP string `gorm:"type:char(15);index"`
Username string `gorm:"type:varchar(32);default:'root'"`
Password string `gorm:"type:varchar(256)"`
SSHPort int `gorm:"type:smallint unsigned;default:22"`
SSHKey string `gorm:"type:text"`
CreatedAt time.Time
UpdatedAt time.Time
DeleteAt *time.Time `gorm:"index"`
}
3.2.1. 保存所有字段
使用 Save() 方法可以更新当前对象的所有字段,如果model中没有指定主键(tag中type内主键不能被识别),则执行 insert 语句,存在主键则执行 update。
func main() {
var node model.Node
model.DB.First(&node)
fmt.Printf("%#v\n", node)
node.Username = "duduniao"
model.DB.Save(&node)
}
[root@duduniao orm]# go run main.go
(/mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:95)
[2020-09-10 21:48:09] [0.77ms] SELECT * FROM `nodes` ORDER BY `nodes`.`uuid` ASC LIMIT 1
[1 rows affected or returned ]
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:
time.Time{wall:0x0, ext:63735342450, loc:(*time.Location)(nil)}, DeleteAt:(*time.Time)(nil)}
(/mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:98)
[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-
09-10 21:48:09', `delete_at` = NULL WHERE `nodes`.`uuid` = 'b1d504fe-0768-4dd0-bc09-e3c1ee0396b2'
3.2.2. 更新单列
更新单列,即更新数据表中的单个字段,这种情况下需要指定条件,否则会报错。更新多条记录时,不会更新 updatedAt 字段。UpdateColumn 不更新 UpdatedAt 字段。
func main() {
err := model.DB.Model(&model.Node{}).Where("uuid = ?", "b1d504fe-0768-4dd0-bc09-e3c1ee0396b2").Update("username", "ubuntu").Error
if err != nil {
logger.Errorf("update failed,err:%s", err.Error())
return
}
}
// UPDATE `nodes` SET `username`="ubuntu",`updated_at`="2020-09-11 21:42:38.769" WHERE uuid = "b1d504fe-0768-4dd0-bc09-e3c1ee0396b2"
func main() {
var node model.Node
model.DB.Where("username = ?", "ansible").First(&node) // 根据where条件选择
model.DB.Model(&node).Update("username", "root")
}
// 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 字段。
func main() {
req := map[string]interface{}{
"username": "duduniao",
"password": "666666",
}
if err := model.DB.Table("nodes").Where("username != 'root' ").Updates(req).Error; err != nil {
logger.Errorf("Update failed, err:%s",err.Error())
return
}
}
//UPDATE `nodes` SET `password`="666666",`username`="duduniao" WHERE username != 'root'
3.2.4. 使用SQL表达式更新
gorm.Expr() 支持SQL表达式,如官方给出的案例:
// product 的 ID 是 `3`
DB.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
DB.Model(&product).Updates(map[string]interface{}{"price": gorm.Expr("price * ? + ?", 2, 100)})
// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
DB.Model(&product).UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3;
DB.Model(&product).Where("quantity > 1").UpdateColumn("quantity", gorm.Expr("quantity - ?", 1))
// UPDATE "products" SET "quantity" = quantity - 1 WHERE "id" = 3 AND quantity > 1;
3.3. 删除记录
3.3.1. 删除记录
可以根据指定的对象删除数据,也可以根据根据 where 条件删除。如果想要清空记录,可以使用恒为真的条件,比如 Where(“1 = 1”)。
func main() {
var node model.Node
model.DB.First(&node)
model.DB.Delete(&node)
model.DB.Where("username = 'root'").Delete(&model.Node{})
model.DB.Where("1 = 1").Delete(&model.Node{})
}
[root@heyingsheng orm]# go run main.go
2020/09/11 22:57:49 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:109
[0.795ms] [rows:1] SELECT * FROM `nodes` WHERE `nodes`.`delete_at` IS NULL ORDER BY `nodes`.`uuid` LIMIT 1
2020/09/11 22:57:49 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:110
[2.815ms] [rows:1] UPDATE `nodes` SET `delete_at`="2020-09-11 22:57:49.894" WHERE `nodes`.`uuid` = "4f89f716-383d-495c-a392-3af34ae61638"
2020/09/11 22:57:49 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:111
[4.092ms] [rows:1] UPDATE `nodes` SET `delete_at`="2020-09-11 22:57:49.897" WHERE username = 'root'
2020/09/11 22:57:49 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:112
[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 字段为当前的时间戳。在查询语句中,查询条件自动跳过了软删除的记录。
func main() {
var node model.Node
model.DB.Unscoped().First(&node) // 查找被软删除的记录
model.DB.Unscoped().Where("1=1").Delete(&node) // 永久删除
}
2020/09/11 23:05:31 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:109
[0.889ms] [rows:1] SELECT * FROM `nodes` ORDER BY `nodes`.`uuid` LIMIT 1
2020/09/11 23:05:31 /mnt/e/Projects/learn/go_learn/day16-db/orm/main.go:110
[3.476ms] [rows:1] DELETE FROM `nodes` WHERE 1=1 AND `nodes`.`uuid` = "4f89f716-383d-495c-a392-3af34ae61638"