作者:lomtom
个人网站:lomtom.top,
个人公众号:博思奥园
你的支持就是我最大的动力。
Go系列:
前言
所有的后端应用都离不开数据库的操作,在Go中也有一些好用的数据库操作组件,例如Gorm就是一个很不错的选择。
这里是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…
- 每个特性都经过了测试的重重考验
- 开发者友好
当然,你可能用不到gorm
这么多特性,但是也不阻碍Gorm
是Go
中一个非常优秀的ORM
框架。
本文也不探究Gorm
和其他框架的优劣比较,而是从使用者出发,一起来探讨Gorm
在实际开发中的使用。
当然Gorm
本身的官方文档已经非常详细了,如果对本文中的部分Gorm
使用有稍许疑惑的话,请移步官方文档:https://gorm.io/zh_CN/docs/index.html
安装
在控制台执行go get
命令进行安装依赖,驱动根据自己的实际使用进行安装,这里以MySQL
为例。
Gorm
官方支持的数据库类型有: MySQL, PostgreSQL, SQlite, SQL Server
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
在使用时引入依赖即可
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
建立连接
使用Gorm
建立数据库的连接其实很简单,但是要做到好用,那就需要花点心思,在这里,将带领大家怎么从最简单的连接到好用的连接设置。
最基本的连接
func GetDb() *gorm.DB {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err!= nil {
return nil
}
return db
}
注意:
- 想要正确的处理 time.Time ,您需要带上 parseTime 参数,
- 使用charset指定编码,要支持完整的 UTF-8 编码,您需要将 charset=utf8 更改为 charset=utf8mb4
更多参数设置:https://github.com/go-sql-driver/mysql#parameters
设置连接池
Gorm
同样支持连接池,Gorm
使用 database/sql
维护连接池
分别使用SetMaxIdleConns
,SetMaxOpenConns
,SetConnMaxLifetime
来设置最大空闲连接数、最大连接数和设置连接空闲超时参数。
func GetDb() *gorm.DB {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
})
if err != nil {
return nil
}
sqlDB, err := db.DB()
if err != nil {
log.Printf("database setup error %v", err)
}
sqlDB.SetMaxIdleConns(10) //最大空闲连接数
sqlDB.SetMaxOpenConns(100) //最大连接数
sqlDB.SetConnMaxLifetime(time.Hour) //设置连接空闲超时
return db
}
全局连接
为了方便使用,我们可以在一开始就使用一个全局变量来保存数据库的连接,在使用时直接调用即可,而不需要再次进行数据库的初始化。
var db *gorm.DB
// GetDb 获取连接
func GetDb() *gorm.DB {
return db
}
将之前的函数改为给db进行初始化并赋值,在使用的时候直接调用GetDb
函数即可
func DbInit(){
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
tempDb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
})
if err != nil {
return nil
}
sqlDB, err := tempDb.DB()
if err != nil {
log.Printf("database setup error %v", err)
}
sqlDB.SetMaxIdleConns(10) //最大空闲连接数
sqlDB.SetMaxOpenConns(100) //最大连接数
sqlDB.SetConnMaxLifetime(time.Hour) //设置连接空闲超时
db = tempDb
}
利用配置文件
到这里,你其实发现已经能够很好的使用Gorm
去建立数据库连接了,但是有没有什么办法像Spring Boot
一样从配置文件中获取连接参数呢,恰好第三章中讲到了怎么使用读取配置文件的方法,那何不利用起来呢?
戳 -> Go(三)Go配置文件
在配置文件中定义数据库连接参数
database:
type: mysql
host: localhost
port: 3306
username: root
password: 123456
dbname: test
max_idle_conn: 10
max_open_conn: 30
conn_max_lifetime: 300
定义相应的结构体
var Database *database
type conf struct {
DB database `yaml:"database"`
}
type database struct {
Type string `yaml:"type"`
Host string `yaml:"host"`
Port string `yaml:"port"`
UserName string `yaml:"username"`
Password string `yaml:"password"`
DbName string `yaml:"dbname"`
MaxIdleConn int `yaml:"max_idle_conn"`
MaxOpenConn int `yaml:"max_open_conn"`
ConnMaxLifetime int `yaml:"conn_max_lifetime"`
}
具体怎么绑定参数,请戳 -> Go(三)Go配置文件
为了更直观的感受,将URI
抽取出来
//获取链接URI
func mySQLUri() string {
return fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=true",
Database.UserName,
Database.Password,
Database.Host,
Database.Port,
Database.DbName)
}
那么最终呈现的就是这样。
var db *gorm.DB
// GetDb 获取连接
func GetDb() *gorm.DB {
return db
}
// DbInit 数据库连接池初始化
func DbInit() {
fmt.Println(mySQLUri())
conn, err1 := gorm.Open(mysql.Open(mySQLUri()), &gorm.Config{})
if err1 != nil {
log.Printf("connect get failed.")
return
}
sqlDB, err := conn.DB()
if err != nil {
log.Printf("database setup error %v", err)
}
sqlDB.SetMaxIdleConns(Database.MaxIdleConn) //最大空闲连接数
sqlDB.SetMaxOpenConns(Database.MaxOpenConn) //最大连接数
sqlDB.SetConnMaxLifetime(time.Duration(Database.ConnMaxLifetime) * time.Second) //设置连接空闲超时
db = conn
}
如果想要在项目启动时自动初始化,将DbInit
方法名改为init
即可,否则,需要在main
方法中自行调用执行初始化。
为了更好的开发,我们可以自定义Gorm
的日志
//初始化数据库日志
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
Colorful: true, // Disable color
},
)
将其作为参数放置在Gorm
参数上gorm.Open(mysql.Open(mySQLUri()), &gorm.Config{})
conn, err1 := gorm.Open(mysql.Open(mySQLUri()), &gorm.Config{
Logger: newLogger,
})
使用
Gorm
的CURD
相对来说叶比较简单。
定义一个结构体User
,除开记录的字段,有编号、姓名、密码三个字段
type User struct {
Id int64 `gorm:"primaryKey;column:id;"`
Username string `gorm:"column:user_name;type:varchar(255);default:(-)" `
Password string `gorm:"column:password;type:varchar(255);default:(-)"`
Deleted gorm.DeletedAt `gorm:"column:deleted;type:timestamp;default:(-)"`
CreateTime time.Time `gorm:"column:create_time;type:timestamp;default:(-)"`
UpdateTime time.Time `gorm:"column:update_time;type:timestamp;default:(-)"`
}
// TableName 自定义表名
func (*User) TableName() string {
return "users"
}
说明:
- 使用
primaryKey
指定主键 - 使用
column:id
指定在数据库中的列名 - 使用
gorm.DeletedAt
标明该字段为删除标志,如果使用了gorm.DeletedAt
,数据库列类型必须为时间格式。 - 使用
type:varchar(255)
标明字段类型 - 使用
default:(-)
设置默认值,-
表示为无默认值。 - 使用
User.TableName
表名数据库名,当使用Model
绑定结构体时,Gorm
会默认调用该方法,除此之外,还可以使用db.Table("user")
显式的标明表名。
查询
- 获取第一个,默认查询第一个
// GetFirst SELECT * FROM users ORDER BY id LIMIT 1;
func GetFirst() (user *User) {
db := config.GetDb()
db.Model(&user).First(&user)
return
}
- 获取最后一个
// GetLast SELECT * FROM users ORDER BY id DESC LIMIT 1;
func GetLast() (user *User) {
db := config.GetDb()
db.Model(&user).Last(&user)
return
}
- 通过主键获取
// GetById SELECT * FROM users WHERE id = 1;
func GetById(id int64) (user *User) {
db := config.GetDb()
db.Model(&user).Find(&user,id)
return
}
等同于
func GetById(id int64) (user *User) {
db := config.GetDb()
db.Model(&user).Where("id = ?",id).Find(&user)
return
}
- 通过主键批量查询
// GetByIds SELECT * FROM users WHERE id IN (1,2,3);
func GetByIds(ids []int64) (user []*User) {
db := config.GetDb()
db.Model(&user).Find(&user,ids)
return
}
等同于
func GetByIds(s []int64) (user []*User) {
db := config.GetDb()
db.Model(&user).Where("id in ?",ids).Find(&user)
return
}
- 获取部分参数,例如只获取名字和密码
// GetSomeParam SELECT username,password FROM users WHERE id = 1;
func GetSomeParam(id int64) (user *User) {
db := config.GetDb()
db.Model(&user).Select("username", "password").Find(&user,id)
return
}
- 分页查询,可以使用
Limit & Offset
进行分页查询
// GetPage SELECT * FROM users OFFSET 5 LIMIT 10;
func GetPage(limit int,offset int) (user []*User) {
db := config.GetDb()
db.Model(&user).Limit(limit).Offset(offset).Find(&user)
return
}
- order
// GetByOrder SELECT * FROM users ORDER BY id desc, username;
func GetByOrder() (user []*User) {
db := config.GetDb()
db.Model(&user).Order("id desc,username").Find(&user)
return
}
等同于
func GetByOrder() (user []*User) {
db := config.GetDb()
db.Model(&user).Order("id desc").Order("username").Find(&user)
return
}
更多请移步:https://gorm.io/zh_CN/docs/query.html
新增
- 创建单个(Create)
func Create(user *User) {
db := config.GetDb()
db.Model(&user).Create(&user)
return
}
- 保存单个(Save)
func Save(user *User) {
db := config.GetDb()
db.Model(&user).Save(&user)
return
}
Create
和Save
的区别:Save
需要插入的数据存在则不进行插入,Create
无论什么情况都执行插入
- 创建多个
func CreateBatch(user []*User) {
db := config.GetDb()
db.Model(&user).Create(&user)
return
}
更多请移步:https://gorm.io/zh_CN/docs/create.html
修改
- 更新单个字段
// UpdateUsername UPDATE users SET username = "lomtom" where id = 1
func UpdateUsername(id int64,username string) {
db := config.GetDb()
db.Model(&User{}).Where("id = ?",id).Update("username",username)
return
}
- 全量/多列更新(根据结构体)
// UpdateByUser UPDATE `user` SET `id`=14,`user_name`='lomtom',`password`='123456',`create_time`='2021-09-26 14:22:21.271',`update_time`='2021-09-26 14:22:21.271' WHERE id = 14 AND `user`.`deleted` IS NULL
func UpdateByUser(user *User) {
db := config.GetDb()
db.Model(&User{}).Where("id = ?",user.Id).Updates(&user)
return
}
更多请移步:https://gorm.io/zh_CN/docs/update.html
删除
- 简单删除(根据
user
里的id进行删除)
// DeleteByUser DELETE from users where id = 28;
// DeleteByUser UPDATE `user` SET `deleted`='2021-09-26 14:25:33.368' WHERE `user`.`id` = 28 AND `user`.`deleted` IS NULL
func DeleteByUser(user *User) {
db := config.GetDb()
db.Model(&User{}).Delete(&user)
return
}
说明: 结构体未加gorm.DeletedAt
标记的字段,直接删除,加了将更新deleted
字段,即实现软删除
- 根据
id
进行删除
// DeleteById UPDATE `user` SET `deleted`='2021-09-26 14:29:55.15' WHERE `user`.`id` = 28 AND `user`.`deleted` IS NULL
func DeleteById(id int64) {
db := config.GetDb()
db.Model(&User{}).Delete(&User{},id)
return
}
事务
同样,Gorm
也有丰富的事务支持。
匿名事务
可使用db.Transaction
匿名方法来表明多个操作在一个事务里面,返回err
将回滚,返回nil
将提交事务
func Transaction() error {
db := config.GetDb()
err := db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&User{Username: "lomtom"}).Error; err != nil {
// 返回任何错误都会回滚事务
return err
}
if err := tx.Delete(&User{}, 28).Error; err != nil {
return err
}
// 返回 nil 提交事务
return nil
})
if err != nil {
return err
}
return nil
}
手动事务
以db.Begin()
表明一个事务的开始,出现错误使用tx.Rollback()
,事务提交使用tx.Commit()
func Transaction1() error {
db := config.GetDb()
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
if err := tx.Create(&User{Username: "lomtom"}).Error; err != nil {
// 回滚事务
tx.Rollback()
return err
}
if err := tx.Delete(&User{}, 28).Error; err != nil {
tx.Rollback()
return err
}
// 提交事务
return tx.Commit().Error
}