Skip to content

Gorm框架简明笔记

介绍

Gorm 是一个用于 Go 语言的 ORM(Object-Relational Mapping)库,它简化了与数据库的交互,通过结构体映射数据库表,使开发者能够使用 Go 代码进行数据库操作。以下是 Gorm 的一些主要特性和一个简单示例。

安装

bash
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

连接到数据库

配置DSN

什么是DSN

gorm库使用dsn作为连接数据库的参数,dsn翻译过来就叫数据源名称,用来描述数据库连接信息。一般都包含数据库连接地址,账号,密码之类的信息。

DSN格式

[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...&paramN=valueN]

MySQL

go
package main

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main()  {
    //配置MySQL连接参数
	username := "root"  //账号
	password := "123456" //密码
	host := "127.0.0.1" //数据库地址,可以是Ip或者域名
	port := 3306 //数据库端口
	Dbname := "tizi365" //数据库名
	timeout := "10s" //连接超时,10秒
	
	//拼接下dsn参数, dsn格式可以参考上面的语法,这里使用Sprintf动态拼接dsn参数,因为一般数据库连接参数,我们都是保存在配置文件里面,需要从配置文件加载参数,然后拼接dsn。
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
	//连接MYSQL, 获得DB类型实例,用于后面的数据库读写操作。
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("连接数据库失败, error=" + err.Error())
	}
	//延时关闭数据库连接
	defer db.Close()
}

设立连接池

什么是数据库连接池

数据库连接池是一个用于管理数据库连接的容器。它维护着一组数据库连接,可以重复使用这些连接以应对多个请求。连接池可以减少应用程序创建和销毁数据库连接的开销,优化数据库访问的性能。

go
package main

import (
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "time"
    "fmt"
)

func main() {
    dsn := "user:password@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 {
        panic("failed to connect database")
    }

    // 获取底层的 sql.DB 对象
    sqlDB, err := db.DB()
    if err != nil {
        panic("failed to get database connection")
    }

    // 设置数据库连接池参数
    sqlDB.SetMaxOpenConns(100)          // 设置最大打开连接数
    sqlDB.SetMaxIdleConns(10)           // 设置最大空闲连接数
    sqlDB.SetConnMaxLifetime(time.Hour) // 设置连接的最大存活时间

    fmt.Println("Database connection pool configured successfully")
}

gorm数据库连接池在设置后连接会自动复用,无需特别设置.

GORM 数据操作

数据表迁移

AutoMigrate

迁移Schema,根据对应的数据模型结构体,创建或更新相应的数据库表结构。

go
err = db.AutoMigrate(&User{})
    if err != nil {
        log.Fatal(err)
    }

插入数据

gorm查询数据本质上就是提供一组函数,帮我们快速拼接sql语句,尽量减少编写sql语句的工作量。 gorm查询结果我们一般都是保存到结构体(struct)变量,所以在执行查询操作之前需要根据自己想要查询的数据定义结构体类型。

示例表

sql
CREATE TABLE `foods` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '商品id',
  `title` varchar(100) NOT NULL COMMENT '商品名',
  `price` float DEFAULT '0' COMMENT '商品价格',
  `stock` int(11) DEFAULT '0' COMMENT '商品库存',
  `type` int(11) DEFAULT '0' COMMENT '商品类型',
  `create_time` datetime NOT NULL COMMENT '商品创建时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

对应Go结构体

go
//商品
type Food struct {
	Id         int
	Title      string
	Price      float32
	Stock      int
	Type       int
	//mysql datetime, date类型字段,可以和golang time.Time类型绑定, 详细说明请参考:gorm连接数据库章节。
	CreateTime time.Time
}

//为Food绑定表名
func (v Food) TableName() string {
	return "foods"
}

Gorm插入数据

Create

使用db.Create(&u)插入记录.

go
//定义一个用户,并初始化数据
u := User{...忽略初始化代码...} 
//插入记录
db.Create(&u)

u.ID           // 返回主键id,默认主键名为ID,也可以通过gorm标签定义,请参考前面的模型定义章节
u.Error        // 返回 error
u.RowsAffected // 返回插入记录的条数

Gorm查询数据

query

Take

查询一条记录

go
//定义接收查询结果的结构体变量
food := Food{}

//等价于:SELECT * FROM `foods`   LIMIT 1  
db.Take(&food)
First

查询一条记录,根据主键ID排序(正序),返回第一条记录

go
//等价于:SELECT * FROM `foods`   ORDER BY `foods`.`id` ASC LIMIT 1    
db.First(&food)
Last

查询一条记录, 根据主键ID排序(倒序),返回第一条记录

go
//等价于:SELECT * FROM `foods`   ORDER BY `foods`.`id` DESC LIMIT 1   
//语义上相当于返回最后一条记录
db.Last(&food)
Find

查询多条记录,Find函数返回的是一个数组

go
//因为Find返回的是数组,所以定义一个商品数组用来接收结果
var foods []Food

//等价于:SELECT * FROM `foods`
db.Find(&foods)
Pluck

查询一列值

go
//商品标题数组
var titles []string

//返回所有商品标题
//等价于:SELECT title FROM `foods`
//Pluck提取了title字段,保存到titles变量
//这里Model函数是为了绑定一个模型实例,可以从里面提取表名。
db.Model(&Food{}).Pluck("title", &titles)

where

上面的例子都没有指定where条件,这里介绍下如何设置where条件,主要通过db.Where函数设置条件. 函数说明:

go
db.Where(query interface{}, args ...interface{})

参数说明:

参数名说明
querysql语句的where子句, where子句中使用问号(?)代替参数值,则表示通过args参数绑定参数
argswhere子句绑定的参数,可以绑定多个参数

select

设置select子句, 指定返回的字段

go
//等价于: SELECT id,title FROM `foods`  WHERE `foods`.`id` = '1' AND ((id = '1')) LIMIT 1  
db.Select("id,title").Where("id = ?", 1).Take(&food)

//这种写法是直接往Select函数传递数组,数组元素代表需要选择的字段名
db.Select([]string{"id", "title"}).Where("id = ?", 1).Take(&food)


//例子2:
//可以直接书写聚合语句
//等价于: SELECT count(*) as total FROM `foods`
total := []int{}

//Model函数,用于指定绑定的模型,这里生成了一个Food{}变量。目的是从模型变量里面提取表名,Pluck函数我们没有直接传递绑定表名的结构体变量,gorm库不知道表名是什么,所以这里需要指定表名
//Pluck函数,主要用于查询一列值
db.Model(&Food{}).Select("count(*) as total").Pluck("total", &total)

fmt.Println(total[0])

order

设置排序语句,order by子句

go
//例子:
//等价于: SELECT * FROM `foods`  WHERE (create_time >= '2018-11-06 00:00:00') ORDER BY create_time desc
db.Where("create_time >= ?", "2018-11-06 00:00:00").Order("create_time desc").Find(&foods)

limit&Offset

设置limit和Offset子句,分页的时候常用语句。

go
//等价于: SELECT * FROM `foods` ORDER BY create_time desc LIMIT 10 OFFSET 0 
db.Order("create_time desc").Limit(10).Offset(0).Find(&foods)

count

Count函数,直接返回查询匹配的行数。

go
//例子:
var total int64 = 0
//等价于: SELECT count(*) FROM `foods` 
//这里也需要通过model设置模型,让gorm可以提取模型对应的表名
db.Model(Food{}).Count(&total)
fmt.Println(total)

Gorm更新数据

Save

用于保存模型变量的值。

go
food := Food{}
//先查询一条记录, 保存在模型变量food
//等价于: SELECT * FROM `foods`  WHERE (id = '2') LIMIT 1
db.Where("id = ?", 2).Take(&food)

//修改food模型的值
food.Price = 100

//等价于: UPDATE `foods` SET `title` = '可乐', `type` = '0', `price` = '100', `stock` = '26', `create_time` = '2018-11-06 11:12:04'  WHERE `foods`.`id` = '2'
db.Save(&food)

Update

更新单个字段值

go
//例子1:
//更新food模型对应的表记录
//等价于: UPDATE `foods` SET `price` = '25'  WHERE `foods`.`id` = '2'
db.Model(&food).Update("price", 25)
//通过food模型的主键id的值作为where条件,更新price字段值。


//例子2:
//上面的例子只是更新一条记录,如果我们要更全部记录怎么办?
//等价于: UPDATE `foods` SET `price` = '25'
db.Model(&Food{}).Update("price", 25)
//注意这里的Model参数,使用的是Food{},新生成一个空白的模型变量,没有绑定任何记录。
//因为Food{}的id为空,gorm库就不会以id作为条件,where语句就是空的

//例子3:
//根据自定义条件更新记录,而不是根据主键id
//等价于: UPDATE `foods` SET `price` = '25'  WHERE (create_time > '2018-11-06 20:00:00') 
db.Model(&Food{}).Where("create_time > ?", "2018-11-06 20:00:00").Update("price", 25)

Updates

go
/例子1:
//通过结构体变量设置更新字段
updataFood := Food{
		Price:120,
		Title:"柠檬雪碧",
	}

//根据food模型更新数据库记录
//等价于: UPDATE `foods` SET `price` = '120', `title` = '柠檬雪碧'  WHERE `foods`.`id` = '2'
//Updates会忽略掉updataFood结构体变量的零值字段, 所以生成的sql语句只有price和title字段。
db.Model(&food).Updates(&updataFood)

//例子2:
//根据自定义条件更新记录,而不是根据模型id
updataFood := Food{
		Stock:120,
		Title:"柠檬雪碧",
	}
	
//设置Where条件,Model参数绑定一个空的模型变量
//等价于: UPDATE `foods` SET `stock` = '120', `title` = '柠檬雪碧'  WHERE (price > '10') 
db.Model(&Food{}).Where("price > ?", 10).Updates(&updataFood)

//例子3:
//如果想更新所有字段值,包括零值,就是不想忽略掉空值字段怎么办?
//使用map类型,替代上面的结构体变量

//定义map类型,key为字符串,value为interface{}类型,方便保存任意值
data := make(map[string]interface{})
data["stock"] = 0 //零值字段
data["price"] = 35

//等价于: UPDATE `foods` SET `price` = '35', `stock` = '0'  WHERE (id = '2')
db.Model(&Food{}).Where("id = ?", 2).Updates(data)

Gorm删除数据

删除模型数据一般用于删除之前查询出来的模型变量绑定的记录。

Delete

用法:db.Delete(模型变量)

go
//例子:
food := Food{}
//先查询一条记录, 保存在模型变量food
//等价于: SELECT * FROM `foods`  WHERE (id = '2') LIMIT 1
db.Where("id = ?", 2).Take(&food)

//删除food对应的记录,通过主键Id标识记录
//等价于: DELETE from `foods` where id=2;
db.Delete(&food)

Where

用法:db.Where(条件表达式).Delete(空模型变量指针)

go
//等价于:DELETE from `foods` where (`type` = 5);
db.Where("type = ?", 5).Delete(&Food{})

这里Delete函数需要传递一个空的模型变量指针,主要用于获取模型变量绑定的表名。 不能传递一个非空的模型变量,否则就变成删除指定的模型数据,自动在where语句加上类似id = 2这样的主键约束条件。


Gorm数据库事务

什么是事务

据库的事务(Transaction)是指一组操作,要么全部执行成功,要么全部撤销,确保数据的一致性和完整性。事务有四个重要属性,简称为ACID:

1.原子性(Atomicity):事务中的所有操作要么全部完成,要么全部不完成。

2.一致性(Consistency):事务执行前后,数据库保持一致状态。

3.隔离性(Isolation):并发事务之间互不干扰。

4.持久性(Durability):事务提交后,其结果是永久的。

事务管理包括开始(BEGIN)、**提交(COMMIT)回滚(ROLLBACK)**操作。事务的使用可以确保数据库操作在遇到错误或系统故障时仍能保持数据的正确性和一致性。

示例

sql
-- 开始事务
BEGIN TRANSACTION;

-- 从账户A中扣除100元
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 'A';

-- 向账户B中增加100元
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 'B';

-- 检查操作是否成功
IF @@ERROR = 0
BEGIN
    -- 提交事务
    COMMIT;
END
ELSE
BEGIN
    -- 回滚事务
    ROLLBACK;
END;

自动事务和手动事务

自动事务

对于自动事务,每个独立的 SQL 语句都是一个事务,执行后立即自动提交或回滚。适合简单的操作。

示例(以 MySQL 为例):

sql
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'B';

每条语句都是独立事务,自动提交或回滚。

手动事务

需要显式地开始、提交和回滚事务,适合复杂的、多步操作。

示例(以 MySQL 为例):

sql
-- 开始事务
BEGIN;

-- 从账户A中扣除100元
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';

-- 向账户B中增加100元
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'B';

-- 提交事务
COMMIT;

-- 或发生错误时回滚事务
ROLLBACK;

自动事务的Gorm实现

Transaction

通过db.Transaction函数实现事务,如果闭包函数返回错误,则回滚事务。

go
db.Transaction(func(tx *gorm.DB) error {
  // 在事务中执行一些 db 操作(从这里开始,您应该使用 'tx' 而不是 'db')
  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
})

手动事务的Gorm实现

go
// 开启事务
tx := db.Begin()

//在事务中执行数据库操作,使用的是tx变量,不是db。

//库存减一
//等价于: UPDATE `foods` SET `stock` = stock - 1  WHERE `foods`.`id` = '2' and stock > 0
//RowsAffected用于返回sql执行后影响的行数
rowsAffected := tx.Model(&food).Where("stock > 0").Update("stock", gorm.Expr("stock - 1")).RowsAffected
if rowsAffected == 0 {
    //如果更新库存操作,返回影响行数为0,说明没有库存了,结束下单流程
    //这里回滚作用不大,因为前面没成功执行什么数据库更新操作,也没什么数据需要回滚。
    //这里就是举个例子,事务中可以执行多个sql语句,错误了可以回滚事务
    tx.Rollback()
    return
}
err := tx.Create(保存订单).Error

//保存订单失败,则回滚事务
if err != nil {
    tx.Rollback()
} else {
    tx.Commit()
}

GORM错误处理

错误处理

如果在执行SQL查询的时候,出现错误,GORM 会将错误信息保存到 *gorm.DB 的Error字段,我们只要检测Error字段就可以知道是否存在错误。

go
if err := db.Where("name = ?", "tizi365").First(&user).Error; err != nil {
  // 错误处理
}

或者使用

go
if result := db.Where("name = ?", "jinzhu").First(&user); result.Error != nil {
  // 错误处理
}

记录找不到

当 First、Last、Take 方法找不到记录时,GORM 会返回 ErrRecordNotFound 错误。如果发生了多个错误,你可以通过 errors.Is 判断错误是否为 ErrRecordNotFound

go
// 检查错误是否为 RecordNotFound
err := db.First(&user, 100).Error
errors.Is(err, gorm.ErrRecordNotFound)

参考文献

https://www.tizi365.com/archives