gorm是一个Golang写的,开发人员友好的ORM库。前面配置章节我们已经使用gorm对我们设计的mysql数据库进行了连接。这一节我们再讲讲怎么配置gorm。
gorm支持多种数据库连接,目前官方列出来的支持库有:MySQL, PostgreSQL, SQLite, SQL Server 四种数据库连接。在我们要开发的博客网站中,我们选择使用 MySQL 来作为后端数据库。
数据库连接
连接MySQL数据库,需要引入 gorm 和mysql两个包:
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
同时连接mysql需要使用tcp套接字符串来连接,因此需要先构建套接字符串:
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{
DisableForeignKeyConstraintWhenMigrating: true,
})
这一句的意思是用户名:密码@tcp(数据库ip或域名:端口)/数据库名称?charset=数据库编码&parseTime=True&loc=Local
。为了让数据库可以支持emoji,也就是微信聊天里的表情符号,比如🙂,我们需要将charset设置为utf8mb4。当然只在这里设置是不够的,还需要在设计数据库表的时候,将表编码和字符串字段的编码都设置为utf8mb4才可以支持4字节的utf8编码。
mysql.Config{} 和 gorm.Config{} 还支持更多的详细配置,这里先不深入去介绍,初步学习不用一下子就把所有的东西都学完,一下子很难记住那么多,后面需要用到的再去根据需求来使用其中的配置功能。
为了让mysql可以更好的工作,往往,我们还需要再设置一下给连接对象设置空闲时的最大连接数、设置与数据库的最大打开连接数,每一个连接的生命周期等信息。
sqlDB, err := db.DB()
if err != nil {
return err
}
sqlDB.SetMaxIdleConns(1000)
sqlDB.SetMaxOpenConns(100000)
sqlDB.SetConnMaxLifetime(-1)
- db.DB() 是获得db连接对象
- SetMaxIdleConns 是设置空闲时的最大连接数
- SetMaxOpenConns 设置与数据库的最大打开连接数
- SetConnMaxLifetime 每一个连接的生命周期等信息
这几个配置在数据库大量读写的时候,非常有用,可以保证在大量并发读写的时候,数据库依然可以正常工作。
自动迁移表
gorm还有一个强大的功能,就是自动迁移表功能。启用自动迁移模式可以保持mysql表更新到最新。
上一节我们已经创建好了5个表的模型,并且提到了可以使用 AutoMigrate 函数来实现自动迁移,现在我们将它们添加为自动迁移模式。我们重新打开config/config.go,在InitDB()函数中添加上下面的代码:
db.AutoMigrate(&model.Admin{}, &model.Article{}, &model.ArticleData{}, &model.Attachment{}, &model.Category{})
添加完成后的InitDB()函数为:
func InitDB(setting *mysqlConfig) error {
var db *gorm.DB
var err error
url := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
setting.User, setting.Password, setting.Host, setting.Port, setting.Database)
setting.Url = url
db, err = gorm.Open(mysql.Open(url), &gorm.Config{
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
return err
}
sqlDB, err := db.DB()
if err != nil {
return err
}
sqlDB.SetMaxIdleConns(1000)
sqlDB.SetMaxOpenConns(100000)
sqlDB.SetConnMaxLifetime(-1)
db.AutoMigrate(&model.Admin{}, &model.Article{}, &model.ArticleData{}, &model.Attachment{}, &model.Category{})
DB = db
return nil
}
OK,到这里只要我们每次运行这个项目,他都会先执行自动迁移,来保证数据库表的字段更新到最新。
写入数据
上一节,我们这是定义了模型结构体,还没对它进行读写操作。为了方便,我们给每一个模型都添加一个Save() 函数,来统一管理他们的创建和更新数据操作:
admin.go
func (admin *Admin) Save(db *gorm.DB) error {
if admin.Id == 0 {
admin.CreatedTime = time.Now().Unix()
}
admin.UpdatedTime = time.Now().Unix()
if err := db.Save(admin).Error; err != nil {
return err
}
return nil
}
Save()函数接受一个*gorm.DB 的指针,并返回一个error错误。我们必须传入 db *gorm.DB 是因为我们这里不能直接使用config.go 下的DB变量,如果我们在这里直接使用config.go 的DB变量的话,就会造成循环依赖的问题,golang是不允许循环依赖的。返回error错误是为了验证是否执行成功,当执行成功的时候,返回值为nil,执行失败的时候,返回值是失败的原因。
Save()函数是Admin模型的内部方法,这个方法操作的是Admin的指针,也就是说这里的admin已经是一个指针了,我们使用db.Save(admin)的时候,就不能再使用指针引用比如写成db.Save(&admin)是错误的,将会导致无法插入和更新数据。
category.go
func (category *Category) Save(db *gorm.DB) error {
if category.Id == 0 {
category.CreatedTime = time.Now().Unix()
}
category.UpdatedTime = time.Now().Unix()
if err := db.Save(category).Error; err != nil {
return err
}
return nil
}
Save()函数在执行Save保存的时候,需要判断下是新建还是更新,我们通过Id值来判断,如果值为零,则认为是插入,所以我们需要给 CreatedTime 赋值为当前的时间戳,同时每次 Save我们都认为是一次更新操作,因此还需要给 UpdatedTime 赋值为当前时间戳,来说明这个数据是这个时候进行了更新操作。
article.go
func (article *Article) Save(db *gorm.DB) error {
if article.Id == 0 {
article.CreatedTime = time.Now().Unix()
}
if err := db.Debug().Save(article).Error; err != nil {
return err
}
if article.ArticleData != nil {
article.ArticleData.Id = article.Id
if err := db.Debug().Save(article.ArticleData).Error; err != nil {
return err
}
}
return nil
}
articles表这里我们进行数据保存的时候,并没有看到我们定义的article_data表的插入和更新操作。那是因为gorm内部会自动根据它们的外键关系处理这一个插入、更新操作。因为我们的Article模型里面定义了ArticleData字段,它是一个一对一的关系。
attachment.go
func (attachment *Attachment) Save(db *gorm.DB) error {
if attachment.Id == 0 {
attachment.CreatedTime = time.Now().Unix()
}
attachment.UpdatedTime = time.Now().Unix()
if err := db.Save(attachment).Error; err != nil {
return err
}
attachment.GetThumb()
return nil
}
func (attachment *Attachment) GetThumb() {
//如果是一个远程地址,则缩略图和原图地址一致
if strings.HasPrefix(attachment.FileLocation, "http") {
attachment.Logo = attachment.FileLocation
attachment.Thumb = attachment.FileLocation
} else {
pfx := "/uploads/"
attachment.Logo = pfx + attachment.FileLocation
paths, fileName := filepath.Split(attachment.FileLocation)
attachment.Thumb = pfx + paths + "thumb_" + fileName
}
}
Attachment 模型的Save操作,我们还定义了GetThumb() 函数,GetThumb() 函数也是Attachment模型的内部方法,它将自动根据我们定义的上传路径,和展示路径,组织Logo、Thumb字段的显示数据,因为*Attachment是一个指针,我们执行了Save操作后,可以通过指针直接修改attachment变量的值,因此,我们在Save()执行保存后,调用 attachment.GetThumb() 可以立即处理好Logo、Thumb字段的数据。
至此,我们就可以对每一个模型使用Save()函数来保存数据了,Save()函数内部会自动判断该数据是插入还是更新数据,然后执行插入和更新的操作。