⬆︎
×
TOC
CHAT

Go语言Web开发(2):GORM

GORM是Go语言的一个ORM框架,使用ORM框架能够更方便地操作各种数据库。GORM提供了丰富的API来简化与数据库的交互,通过数据库方言屏蔽了各个数据库的差异、利用反射获取结构体的字段和tag与数据库做映射,

GORM官方支持的数据库类型:MySQL、PostgreSQL、SQlite、SQL Server等

官网:gorm.io

官方文档:gorm.io/zh_CN/docs

相关阅读:Gin框架基础教程SQL基本操作

1 Gin中使用GORM

安装GORM包与MySQL(或其他数据库)的驱动:

go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

ORM

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)映射到数据库表来简化数据库交互,定义方式详见官方文档-模型定义

在实际项目中定义数据库模型注意以下几点:

  1. 结构体名称必须首字母大写,并与数据库表名对应。【例】表名为user,结构体名称定义为User;表名为article_cate,结构体名称定义为ArticleCate
  2. 结构体中的字段名称首字母必须大写,并与数据库表中的字段一一对应。【例】以下结构体中的Id与数据库中的id对应,Username与数据库中的username对应,Age与数据库中的age对应,Email与数据库中的email对应,AddTime与数据库中的add_time字段对应。
  3. 默认情况表名是结构体名称的复数形式,即若结构体名称为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 NULLIS NULLBETWEEN ... AND ...NOT BETWEEN ... AND ...INORANDNOTLIKE

可在语句中使用?占位符以插值,如以下数例所示:

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()方法用于拆分含ORWhere()查询。

例如对于以下语句:

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中的ORDERLIMIT,用于排序与分页;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语句,如INSERTUPDATEDELETE等,返回值通常包括受影响的行数(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 一对一

如图(左)所示,一个文章只有一个分类,articlearticle_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_catearticle之间为一对多的关系。文章表中的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会在事务中执行单个的写入操作(CREATEUPDATEDELETE)。若没有这方面的要求,可于初始化时在&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支持直接调用事务控制方法(commitrollback),一般流程如下所示:

// 开始事务
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

官网:ini.unknwon.io

详见使用文档

发表评论