GORM的使用|青训营笔记

110 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1天

1. 理解database/sql

database/sql包是Go标准库中提供的围绕sql(或类似sql)数据库的通用接口。sql包必须与数据库驱动程序一起使用,而这些驱动程序由第三方开发,并不在标准库中。

1.1 database/sql基本用法

import (  
   "database/sql"  
   "fmt"   
   _ "github.com/go-sql-driver/mysql"  
)  
  
type Todo struct {  
   ID    int  
   Title string  
}  
  
func main() {  
   dsn := "root:19991101@tcp(127.0.0.1:3306)/bubble"  
   db, err := sql.Open("mysql", dsn)  
   if err != nil {  
      fmt.Println("Connect database failed ! err: ", err)  
   }  
   rows, err := db.Query("select id, title from todos where id = ?", 1)  
  
   if err != nil {  
      // xxx  
   }  
  
   defer func() {  
      err = rows.Close()  
   }()  
  
   var todos []Todo  
   for rows.Next() {  
      var todo Todo  
      err := rows.Scan(&todo.ID, &todo.Title)  
      if err != nil {  
         // ...  
      }  
      fmt.Printf("%#v\n", todo)  
      todos = append(todos, todo)  
   }  
   if rows.Err() != nil {  
      //..  
   }  
}

1.2 连接池的维护

  1. 设置连接最大空闲时间
func (db *DB) SetConnMaxIdleTime(d time.Duration)
  1. 设置连接可以重复使用的最大时间
func (db *DB) SetConnMaxLifetime(d time.Duration)
  1. 设置空闲连接池中的最大连接数
func (db *DB) SetMaxIdleConns(n int)
  1. 设置数据库的最大打开连接数
func (db *DB) SetMaxOpenConns(n int)
  1. 返回连接池状态
func (db *DB) Stats() DBStats

连接池状态包含以下信息

type DBStats struct {
	MaxOpenConnections int // Maximum number of open connections to the database.

	// Pool Status
	OpenConnections int // The number of established connections both in use and idle.
	InUse           int // The number of connections currently in use.
	Idle            int // The number of idle connections.

	// Counters
	WaitCount         int64         // The total number of connections waited for.
	WaitDuration      time.Duration // The total time blocked waiting for a new connection.
	MaxIdleClosed     int64         // The total number of connections closed due to SetMaxIdleConns.
	MaxIdleTimeClosed int64         // The total number of connections closed due to SetConnMaxIdleTime.
	MaxLifetimeClosed int64         // The total number of connections closed due to SetConnMaxLifetime.
}

1.3 Connector接口取代dsn

直接使用基于字符串的dsn连接数据库有诸多不便,如:

  1. 密码中的一些字符可能会引起转义问题
  2. 容易忘记导入数据库驱动包 因此,database\sql包提供了另一种基于Connector连接数据库的方式,使用示例如下:
import (  
   "database/sql"  
   "fmt"   
   "github.com/go-sql-driver/mysql")  
  
func main() {  
   connector, _ := mysql.NewConnector(&mysql.Config{  
      User:   "root",  
      Passwd: "19991101",  
      Net:    "tcp",  
      Addr:   "127.0.0.1:3306",  
      DBName: "bubble",     
      Collation:            "utf8mb4_general_ci", // 设定字符序  
      AllowNativePasswords: true,                 // 允许使用本机密码登录  
   })  
   db := sql.OpenDB(connector)  
   err := db.Ping()  
   if err != nil {  
      fmt.Println("database connect failed, error", err)  
   }  
}
  1. Collation参数的作用是设置字符序,如果不进行设置可能会报错:unknown collation
  2. AllowNativePasswords参数的作用是运行使用本机密码进行登录,如果不进行设置,可能会报错: this user requires mysql native password authentication 此外,通过Connector连接器连接数据库不会返回err,要验证是否连接成功,需要调用DB.Ping()方法

2. GORM的基础使用

2.1 连接数据库

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

2.2 CRUD

2.2.1 创建数据

创建单条数据

todo := Todo{Title : "事项1",
		Status : false}
result := db.Create(&todo) // 通过数据的指针来创建 

todo.ID 返回插入数据的主键 result.Error返回error result.RowsAffected返回插入记录的条数

批量创建数据 可以通过传入切片批量创建数据

var todos = []Todo{{Title: "事项1"}, {Title: "事项2"}, {Title: "事项3"}}  
db.Create(todos)  
for _, todo := range todos {  
   fmt.Println(todo.ID) // 1,2,3  
}

分批创建数据 CreateInBatches

// 分批创建数据1000条数据 CreateInBatches
var todos2 = []Todo{}  
for i := 0; i < 1000; i++ {  
   str := "事项" + strconv.Itoa(i+1)  
   todo := Todo{Title: str}  
   todos2 = append(todos2, todo)  
}  
// 每批次数量为 100
db.CreateInBatches(todos2, 100)

2.2.2 查找数据

查询单条记录

GORM 提供了 FirstTakeLast 方法,以便从数据库中检索单个对象。当查询数据库时它添加了 LIMIT 1 条件,且没有找到记录时,它会返回 ErrRecordNotFound 错误

var todo = Todo{}  
result := db.First(&todo) // 查询按主键排序的第一条数据  
result = db.Take(&todo) // 查询第一条数据  
result = db.Last(&todo) // 查询按主键排序的最后一条数据  
// 检查 ErrRecordNotFound 错误  
errors.Is(result.Error, gorm.ErrRecordNotFound)

使用find查找多条数据 使用String条件进行查询

// select * from todos  
db.Find(&todos) // 查询所有数据
// select * from todos where title in ("事项1", "事项2", "事项3")
db.Where("title in ?", []string{"事项1", "事项2", "事项3"}).Find(&todos)
// select * from todos where title like '%事项1%'  
db.Where("title like ?", "%事项1%").Find(&todos)

find函数也可以使用结构体和map作为查询条件

// select * from todos where title == "事项1"  
db.Where(&Todo{Title: "事项1", Status: false}).Find(&todos)
db.Where(map[string]interface{}{"Title": "事项2", "Status": false}).Find(&todos)

注意

  1. 使用find函数查询多条数据,如果查询不到数据不会返回err
  2. 使用结构体查询时,gorm只会将非0字段作为查询条件。如果需要将零值字段作为查询条件,可以使用map进行查询

2.2.3 更新数据

更新数据主要使用到Update方法和model方法, 当使用 Model 方法,并且值中有主键值时,主键将会被用于构建条件

更新单列

// update todos set title = '事项one' where title = '事项1'  
db.Model(&Todo{}).Where("title = ?", "事项1").Update("title", "事项one")

更新多列 Updates方法可以使用结构体和map作为更新值

// update todos set title = '吃饭', status = true where id == 1
db.Model(&Todo{ID: 1}).Updates(&Todo{Title : "吃饭", Status: true})  
db.Model(&Todo{ID: 1}).Updates(map[string]interface{}{"Title" : "吃饭", "Status" : true});

更新选定字段 可以使用 SelectOmit在更新时选定、忽略某些字段

db.Model(&Todo{ID: 1}).Select("title").Updates(&Todo{Title: "吃饭", Status: true})  
db.Model(&Todo{ID: 1}).Omit("title").Updates(&Todo{Title: "吃饭", Status: true})

SQL表达式更新

// UPDATE "products" SET "price" = price * 2 + 100, "updated_at" = '2013-11-17 21:34:10' WHERE "id" = 3;
db.Model(&product).Update("price", gorm.Expr("price * ? + ?", 2, 100))  

注意:同find方法一样,在使用结构体更新字段值时,只会更新非0值,如果需要更新零值,可以使用map

2.2.4 删除数据

删除数据主要使用delete方法

删除单条数据

//DELETE from todos where id = 1  
db.Delete(&Todo{ID: 1})

批量删除

// delete form todos where id > 10  
db.Where("id > ?", 10).Delete(&Todo{})
db.Delete(&Todo{}, "id > ?", 10)

软删除 如果模型中包含了 gorm.deletedat 字段(gorm.Model 已经包含了该字段),它将自动获得软删除的能力。拥有软删除能力的模型调用 Delete 时,记录不会从数据库中被真正删除。但 GORM 会将 DeletedAt 置为当前时间, 并且你不能再通过普通的查询方法找到该记录。 可以使用Unscoped找到或永久删除软删除记录

db.Unscoped().Where("age = 20").Find(&users)
db.Unscoped().Delete(&order)

2.3 模型定义

一个结构体可以视为一张表,其属性可以视为各个字段,在GORM中可以使用db.AutoMigrate(&Todo{})自动建表,只需要传入空结构体指针。

自动建表时的一些约定

  1. 表名为struct name的snake_cases复数格式,可以通过实现Tabler接口中的TableName方法设置表名
func (User) TableName() string {  
  return "profiles"  
}
  1. 字段名为field name的snake_case单数格式,可以通过gorm:"column:name"标签设置字段名
  2. ID/id字段默认为主键,如果为数字,则为自增主键,使用标签gorm:"primaryKey"可以将其它字段设为主键
  3. CreateAt字段,创建时,保存当前时间
  4. UpdateAt字段,创建、更新时,保存当前时间
  5. gorm.DeletedAt字段,默认开启soft delete模式

引用参考

Standard library - Go Packages

GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.