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()函数内部会自动判断该数据是插入还是更新数据,然后执行插入和更新的操作。