GORM是Go语言的一个ORM框架,使用ORM框架能够更方便地操作各种数据库。GORM提供了丰富的API来简化与数据库的交互,通过数据库方言屏蔽了各个数据库的差异、利用反射获取结构体的字段和tag与数据库做映射,
GORM官方支持的数据库类型:MySQL、PostgreSQL、SQlite、SQL Server等
官网:gorm.io
官方文档:gorm.io/zh_CN/docs
1 Gin中使用GORM
安装GORM包与MySQL(或其他数据库)的驱动:
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
1.1 连接数据库
在models
目录下新建core.go
,建立数据库链接:使用gorm.Open()
函数获取数据库对象gorm.DB
(需传入mysql.Open(dsn)
与GORM设置项&gorm.Config
)。
以连接MySQL数据库为例:
package models
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
var err error
func init() {
dsn := "root:123456@tcp(192.168.0.6:3306)/gin?charset=utf8mb4&parseTime=True&loc=L ocal"
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println(err)
}
}
其他数据库连接方式详见官方文档-连接到数据库。
1.2 定义操作数据库模型
GORM通过将Go结构体(struct
)映射到数据库表来简化数据库交互,定义方式详见官方文档-模型定义。
在实际项目中定义数据库模型注意以下几点:
- 结构体名称必须首字母大写,并与数据库表名对应。【例】表名为
user
,结构体名称定义为User
;表名为article_cate
,结构体名称定义为ArticleCate
。 - 结构体中的字段名称首字母必须大写,并与数据库表中的字段一一对应。【例】以下结构体中的
Id
与数据库中的id
对应,Username
与数据库中的username
对应,Age
与数据库中的age
对应,Email
与数据库中的email
对应,AddTime
与数据库中的add_time
字段对应。 - 默认情况表名是结构体名称的复数形式,即若结构体名称为
User
,表示该模型默认操作的是users
表。可以重写结构体中的自定义方法TableName()
改变结构体的默认表名称。
【例】在models
模块中定义user
模型
package models
type User struct { // 默认表名为`users`
Id int Username
string Age int
Email string
AddTime int
}
func (User) TableName() string {
return "user"
}
// ...
更多模型定义的方法详见官方文档-约定。
预置的gorm.Model
结构体中包含若干重要字段,可以让数据库模型继承之以使用。源码如下:
type Model struct {
ID uint `gorm:"primaryKey"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
}
2 CRUD
在要操作数据表的控制器引入models模块。
更多CRUD语句示例:官方文档-CRUD接口。
2.1 基本使用
增加:使用Create()
方法来添加数据(需传入待插对象的指针),增加成功后会返回刚才增加的记录
func (con UserController) Add(c *gin.Context) {
user := models.User{
Username: "Akira37R",
Age: 108,
Email: "firsttimechadder@hyplus.top",
AddTime: int(time.Now().Unix()),
}
result := models.DB.Create(&user) // 通过数据的指针来创建
if result.RowsAffected > 1 {
fmt.Print(user.Id)
}
fmt.Println(result.RowsAffected)
fmt.Println(user.Id)
c.String(http.StatusOK, "Add成功")
}
查找:使用Find()
方法查找全部元素(需传入对象数组的指针);要想指定条件,可在查找之前使用Where()
方法
func (con UserController) Index(c *gin.Context) {
user := []models.User{}
models.DB.Find(&user)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": user,
})
}
func (con UserController) IndexUsername(c *gin.Context) {
user := []models.User{}
models.DB.Where("username=?", "Akira37" ).Find(&user)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": user,
})
}
修改:使用Find()
查找后修改字段,再使用Save()
方法保存
func (con UserController) Edit(c *gin.Context) {
user := models.User{Id: 7}
models.DB.Find(&user)
user.Username = "gin gorm"
user.Age = 1
models.DB.Save(&user)
c.String(http.StatusOK, "Edit")
}
删除:使用Delete()
方法;要想批量删除,只需在删除前使用Where()
方法过滤
func (con UserController) Delete(c *gin.Context) {
user := models.User{Id: 8}
models.DB.Delete(&user)
c.String(http.StatusOK, "Delete")
}
func (con UserController) DeleteAll(c *gin.Context) {
user := models.User{}
models.DB.Where("id>9").Delete(&user)
c.String(http.StatusOK, "DeleteAll")
}
更多删除指令及对应的SQL语句:
db.Where("email LIKE ?", "%jinzhu%").Delete(Email{}) // DELETE from emails where email LIKE "%jinzhu%";
db.Delete(Email{}, "email LIKE ?", "%jinzhu%") // DELETE from emails where email LIKE "%jinzhu%";
2.2 查询语句详解
更多查询语句详见官方文档-查询。
2.2.1 Where()、Or()
Where()
方法对应SQL中的WHERE
,支持=
、<
、>
、<=
、>=
、!=
、IS NOT NULL
、IS NULL
、BETWEEN ... AND ...
、NOT BETWEEN ... AND ...
、IN
、OR
、AND
、NOT
、LIKE
。
可在语句中使用?
占位符以插值,如以下数例所示:
nav := []models.Nav{}
models.DB.Where("id<3").Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
var n = 5
nav := []models.Nav{}
models.DB.Where("id>?", n).Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
var n1 = 3
var n2 = 9
nav := []models.Nav{}
models.DB.Where("id > ? AND id < ?", n1, n2).Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
nav := []models.Nav{}
models.DB.Where("id in (?)", []int{3, 5, 6}).Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
nav := []models.Nav{}
models.DB.Where("title like ?", "%特%").Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
nav := []models.Nav{}
models.DB.Where("id between ? and ?", 3, 6).Find(&nav)
c.JSON(http.StatusOK, gin.H{
"success": true,
"result": nav,
})
Or()
方法用于拆分含OR
的Where()
查询。
例如对于以下语句:
nav := []models.Nav{}
models.DB.Where("id=? OR id=?", 2, 3).Find(&nav)
可改写为:
nav := []models.Nav{}
models.DB.Where("id=?", 2).Or("id=?", 3).Or("id=4").Find(&nav)
2.2.2 Select()
Select()
方法对应SQL中的SELECT
,用于选择字段查询:
nav := []models.Nav{}
models.DB.Select("id, title, url").Find(&nav)
2.2.3 Order()、Limit()、Offset()
Order()
、Limit()
方法分别对应SQL中的ORDER
与LIMIT
,用于排序与分页;Offset()
方法用于设置偏移量以跳过若干条记录。
nav1 := []models.Nav{}
models.DB.Where("id>2").Order("id Asc").Find(&nav1)
nav2 := []models.Nav{}
models.DB.Where("id>2").Order("sort Desc").Order("id Asc").Find(&nav2)
nav3 := []models.Nav{}
odels.DB.Where("id>1").Limit(2).Find(&nav3)
【例】跳过2条查询2条
nav := []models.Nav{}
models.DB.Where("id>1").Offset(2).Limit(2).Find(&nav)
2.2.4 Count()
Count()
方法用于统计查询结果中的记录数量。
nav := []models.Nav{}
var num int
models.DB.Where("id > ?", 2).Find(&nav).Count(&num)
2.2.5 Distinct()
Distinct()
方法对应SQL中的DISTINCT
关键字,用于从模型中选择不相同的值。
nav := []models.Nav{}
models.DB.Distinct("title").Order("id desc").Find(&nav)
c.JSON(http.StatusOK, gin.H{
"nav": nav,
})
相当于以下SQL查询语句:
SELECT DISTINCT `title` FROM `nav` ORDER BY id desc
2.2.6 Scan()
Scan()
方法将查询结果映射到指定的变量里,类似于Find()
,主要用于存储部分字段。
type Result struct {
Name string
Age int
}
var result Result
db.Table("users").Select("name", "age").Where("name = ?", "Teresa").Scan(&result)
// 原生SQL:
// db.Raw("SELECT name, age FROM users WHERE name = ?", "Teresa").Scan(&result)
var result []models.User
models.DB.Raw("SELECT * FROM user").Scan(&result) // 此处相当于Find()
fmt.Println(result)
2.2.7 Join()
Join()
用于多表查询(详见后述)
type result struct {
Name string
Email string
}
db.Model(&User{}).Select("users.name, emails.email").Joins("left join emails on emails.user_i d = users.id").Scan(&result{})
相当于以下SQL查询语句:
SELECT users.name, emails.email FROM `users` left join emails on emails.user_id = users.i d
2.3 查看执行的SQL
要想查看执行的SQL,只需连接数据库时在GORM设置项&gorm.Config
中配置QueryFields: true
即可。
package models
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
var err error
func init() {
dsn := "root:123456@tcp(192.168.0.6:3306)/gin?charset=utf8mb4&parseTime=True&loc=L ocal"
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
QueryFields: true,
})
if err != nil {
fmt.Println(err)
}
}
2.4 原生SQL与SQL生成器
Raw()
方法用于执行原生SQL查询,允许直接编写SQL语句并将查询结果映射到Go结构体中。
Exec()
方法用于执行不返回查询结果的SQL语句,如INSERT
、UPDATE
、DELETE
等,返回值通常包括受影响的行数(RowsAffected
成员)和可能出现的错误。
更多使用方式见官方文档-SQL构建器。
【例1】使用原生SQL删除user
表中的一条数据:
result := models.DB.Exec("delete from user where id=?", 3)
fmt.Println(result.RowsAffected)
【例2】使用原生SQL修改user
表中的一条数据:
result := models.DB.Exec("update user set username=? where id=2", "hypluser")
fmt.Println(result.RowsAffected)
【例3】查询uid=2
的数据:
var result models.User
models.DB.Raw("SELECT * FROM user WHERE id = ?", 2).Scan(&result)
fmt.Println(result)
【例4】查询user
表中的所有数据:
var result []models.User
models.DB.Raw("SELECT * FROM user").Scan(&result)
fmt.Println(result)
【例5】统计user
表的数量(Row()
假定结果只有1行,返回单行结果对象):
var count int
row := models.DB.Raw("SELECT count(1) FROM user").Row(&count)
row.Scan(&count)
3 表关联查询
本节以如下图中左、右两幅ER图为例:
3.1 一对一
如图(左)所示,一个文章只有一个分类,article
与article_cate
之间为一对一关系,文章表中的cate_id
保存着文章分类的id。 若想查询文章的同时获取文章分类,则需进行一对一的关联查询。
在结构体tag中,使用foreignkey
指定当前表的外键、references
指定关联表中和外键关联的字段。
因此Article
中应新增ArticleCate
类型的成员,tag为gorm:"foreignKey:CateId;references:Id"
。两个结构体如下所示:
type Article struct {
Id int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
CateId int `json:"cate_id"`
State int `json:"state"`
ArticleCate ArticleCate `gorm:"foreignKey:CateId;references:Id"`
}
func (Article) TableName() string {
return "article"
}
type ArticleCate struct {
Id int `json:"id"`
Title string `json:"title"`
State int `json:"state"`
}
func (ArticleCate) TableName() string {
return "article_cate"
}
多表查询时应使用Preload()
一次性预加载关联数据,需传入预加载表结构体的成员名。
【例1】查询所有文章及文章对应的分类信息:
func (con ArticleController) Index(c *gin.Context) {
var articleList []models.Article
models.DB.Preload("ArticleCate").Limit(2).Find(&articleList) // ArticleCate为成员名
c.JSON(http.StatusOK, gin.H{
"result": articleList,
})
}
【例2】指定条件查询所有文章及文章对应的分类信息:
func (con ArticleController) Index(c *gin.Context) {
var articleList []models.Article
models.DB.Preload("ArticleCate").Where("id>=?", 4).Find(&articleList)
c.JSON(http.StatusOK, gin.H{
"result": articleList,
})
}
3.2 一对多
实际项目中,菜品分类和菜品之间、订单表和订单商品表之间都属于一对多关系。
如图(左)所示,一个分类下有多篇文章,article_cate
与article
之间为一对多的关系。文章表中的cate_id
保存着文章分类的id。 若想查询文章分类的同时获取分类下的文章,则需进行一对多的关联查询。
在3.1的基础上,ArticleCate
新增Article
数组类型的成员,tag为gorm:"foreignKey:CateId"
。两个结构体如下所示:
type ArticleCate struct {
Id int `json:"id"`
Title string `json:"title"`
State int `json:"state"`
Article []Article `gorm:"foreignKey:CateId"`
}
func (ArticleCate) TableName() string {
return "article_cate"
}
type Article struct {
Id int `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
CateId int `json:"cate_id"`
State int `json:"state"`
ArticleCate ArticleCate `gorm:"foreignKey:CateId;references:Id"`
}
func (Article) TableName() string {
return "article"
}
【例1】查找所有分类及分类下的文章信息
func (con ArticleController) Index(c *gin.Context) {
var articleCateList []models.ArticleCate
models.DB.Preload("Article").Find(&articleCateList)
c.JSON(http.StatusOK, gin.H{
"result": articleCateList,
})
}
【例2】指定条件查找所有分类及分类下的文章信息
func (con ArticleController) Index(c *gin.Context) {
var articleCateList []models.ArticleCate
models.DB.Preload("Article").Where("id>0").Offset(1).Limit(1).Find(&articleCateList)
c.JSON(http.StatusOK, gin.H{
"result": articleCateList,
})
}
3.3 多对多
根据图(右)定义学生、课程、学生课程表的model。若想根据课程获取选学本门课程的学生,需在Lesson
中关联Student
。
Lesson
中新增[]*Student
类型成员,tag为gorm:"many2many:lesson_student"
;Student
中新增[]*Lesson
类型成员,tag为gorm:"many2many:lesson_student"
。三个结构体如下所示:
type Lesson struct {
Id int `json:"id"`
Name string `json:"name"`
Student []*Student `gorm:"many2many:lesson_student"`
}
func (Lesson) TableName() string {
return "lesson"
}
type Student struct {
Id int
Number string
Password string
ClassId int
Name string
Lesson []*Lesson `gorm:"many2many:lesson_student"`
}
func (Student) TableName() string {
return "student"
}
type LessonStudent struct {
LessonId int
StudentId int
}
func (LessonStudent) TableName() string {
return "lesson_student"
}
【例1】获取学生信息与课程信息
studentList := []models.Student{}
models.DB.Find(&studentList)
c.JSON(http.StatusOK, studentList)
lessonList := []models.Lesson{}
models.DB.Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
【例2.1】查询全部学生信息的同时获取学生的选课信息
studentList := []models.Student{}
models.DB.Preload("Lesson").Find(&studentList)
c.JSON(http.StatusOK, studentList)
【例2.2】查询id=1
的学生选修了哪些课程
studentList := []models.Student{}
models.DB.Preload("Lesson").Where("id=1").Find(&studentList)
c.JSON(http.StatusOK, studentList)
【例3.1】查询全部课程被哪些学生选修了
lessonList := []models.Lesson{}
models.DB.Preload("Student").Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
【例3.2】查询id=1
的课程被哪些学生选修了
lessonList := []models.Lesson{}
models.DB.Preload("Student").Where("id=1").Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
【例4】制定条件查询数据
lessonList := []models.Lesson{}
models.DB.Preload("Student").Offset(1).Limit(2).Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
3.4 指定子集的筛选条件
Preload()
的可变参数可传入子集的筛选条件,例如Preload("AccessItem", "status=1")
。
一些简单使用例:
access := []models.Access{}
models.DB.Preload("AccessItem", "status=1").Order("sort desc").Where("module_id=?", 0).Fin d(&access)
essonList := []models.Lesson{}
models.DB.Preload("Student", "id!=1").Find(&lessonList)
lessonList := []models.Lesson{}
models.DB.Preload("Student", "id not in (1,2)").Find(&lessonList)
3.5 自定义预加载SQL
Preload()
的可变参数可传入回调函数,用于自定义预加载SQL。
一些简单使用例:
lessonList := []models.Lesson{}
models.DB.Preload("Student", func(db *gorm.DB) *gorm.DB {
return models.DB.Order("id DESC")
}).Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
lessonList := []models.Lesson{}
models.DB.Preload("Student", func(db *gorm.DB) *gorm.DB {
return models.DB.Where("id>3").Order("id DESC")
}).Find(&lessonList)
c.JSON(http.StatusOK, lessonList)
4 事务
事务(Transaction)可以用于维护数据库的完整性,保证成批的SQL语句要么全执行,要么全不执行。
更多事务用法详见官方文档-事务。
4.1 禁用默认事务
为了确保数据一致性,GORM会在事务中执行单个的写入操作(CREATE
、UPDATE
、DELETE
)。若没有这方面的要求,可于初始化时在&gorm.Config
中配置SkipDefaultTransaction: true
来禁用,能获得约30%+的性能提升。
package models
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DB *gorm.DB
var err error
func init() {
dsn := "root:123456@tcp(192.168.0.6:3306)/gin?charset=utf8mb4&parseTime=True&loc=L ocal"
DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
SkipDefaultTransaction: true,
})
if err != nil {
fmt.Println(err)
}
}
4.2 事务执行流程
使用db.Transaction()
方法执行事务,传入的回调函数即为事务中执行的一系列操作(参数为tx *gorm.DB
,返回值为error
);返回任何错误都会回滚事务,返回nil
提交事务。一般流程如下所示:
db.Transaction(func(tx *gorm.DB) error {
// 在事务中执行一些db操作(注意应使用tx)
if err := tx.Create(&Animal{Name: "Giraffe"}).Error; err != nil {
return err // 返回任何错误都会回滚事务
}
if err := tx.Create(&Animal{Name: "Lion"}).Error; err != nil {
return err
}
// 返回nil提交事务
return nil
})
可在事务中嵌套事务:
db.Transaction(func(tx *gorm.DB) error {
tx.Create(&user1)
tx.Transaction(func(tx2 *gorm.DB) error {
tx2.Create(&user2)
return errors.New("rollback user2") // Rollback user2
})
tx.Transaction(func(tx3 *gorm.DB) error {
tx3.Create(&user3)
return nil
})
return nil
})
4.3 手动调用事务
GORM支持直接调用事务控制方法(commit
、rollback
),一般流程如下所示:
// 开始事务
tx := db.Begin()
// 在事务中执行一些db操作(注意与回调函数中一样,应使用tx)
tx.Create(...)
// ...
// 遇到错误时回滚事务
tx.Rollback()
// 否则提交事务
tx.Commit()
【例】A(id=1
)给B(id=2
)转账:
package admin
import (
"fmt"
"gindemo13/models"
"github.com/gin-gonic/gin"
)
type TransitionController struct {
BaseController
}
func (con TransitionController) Index(c *gin.Context) {
tx := models.DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
con.error(c)
}
}()
if err := tx.Error; err != nil {
fmt.Println(err)
con.error(c)
}
// A账户减去100
u1 := models.Bank{Id: 1}
tx.Find(&u1)
u1.Balance = u1.Balance - 100
if err := tx.Save(&u1).Error; err != nil {
tx.Rollback()
con.error(c)
}
// panic("遇到了错误")
// B账户增加100
u2 := models.Bank{Id: 2}
tx.Find(&u2)
u2.Balance = u2.Balance + 100
// panic("失败")
if err := tx.Save(&u2).Error; err != nil {
tx.Rollback()
con.error(c)
}
tx.Commit()
con.success(c)
}
5 go-ini加载.ini配置文件
go-ini是一款极为强大的Go语言.ini
文件操作库。
GitHub:github.com/go-ini/ini
详见使用文档