Go 语言内置包 database/sql 为所有数据库提供了通用接口,也就是说所有数据库的增删改查操作在 database/sql 中都有对应的方法实现,不同的数据库只需要更换相应的数据库驱动即可,无须修改代码。例如当前使用的SQLite数据库,若要修改为MySQL,则只需要导入MySQL数据库驱动和数据库连接信息即可,而数据库的代码则无须修改。
大部分编程语言中,通过程序操作数据库的步骤都大同小异,如下所示:
- 1.通过数据库驱动连接数据库,生成数据库对象
- 2.使用数据库对象执行SQL语句,数据库收到程序发送的SQL语句后,执行相应的数据操作
- 3.数据库执行完成后,将执行结果返回给程序,程序从执行结果中获取数据或判断执行是否成功
- 4.完成数据操作后,关闭或销毁数据库连接对象
21.1 操作MySQL
以MySQL为例,先安装数据库驱动,执行以下命令
go get -u github.com/go-sql-driver/mysql
示例代码如下所示:
package mainimport (// 是一种接口规范,它并不解决真正的连接和协议"database/sql""fmt""log""os""time"// init 按照名称注册驱动,后期可以使用名称找到对应的驱动_ "github.com/go-sql-driver/mysql"
)var db *sql.DB
var err errorfunc init() {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"// dsn格式:[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]// 示例: username:password@protocol(address)/dbname?param=valuedsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)db, err = sql.Open("mysql", dsn)if err != nil {log.Fatalf("连接数据库出错:%v", err)}// 以下参数参考资料:https://github.com/go-sql-driver/mysqldb.SetConnMaxIdleTime(10 * time.Second)db.SetMaxOpenConns(0)db.SetMaxIdleConns(10)// fmt.Printf("%v\n", db)
}type Person struct {id intname stringlocation string
}// 查询一行
func queryOneRow() {// 返回最多一行数据,不是limit 1,是从查询结果中返回一行,其他的丢弃row := db.QueryRow("SELECT * FROM persons")fmt.Printf("row err:%+v\n", row.Err())// 这里定义与数据库表中对应的字段,需要与数据库中字段名保持一致// 方法一// var (// id int// name string// location string// )var p Personerr = row.Scan(&p.id, &p.name, &p.location)if err == nil {log.Printf("queryOneRow id:%d name:%s location: %s\n", p.id, p.name, p.location)log.Printf("queryOneRow Person is %#v\n", p)}
}// 查询多行
func queryRows() {rows, err := db.Query("SELECT * FROM persons")if err != nil {log.Fatalf("查询数据库出错:%v", err)}for rows.Next() {var p Personerr = rows.Scan(&p.id, &p.name, &p.location)if err == nil {log.Printf("queryRows Person is %#v\n", p)}}
}// 查询带条件多行
func queryRowsByCondition() {// 占位符 ? 相当于参数,但存在注入攻击的风险rows, err := db.Query("SELECT * FROM persons WHERE id > ?", 2)if err != nil {log.Fatalf("查询数据库出错:%v", err)}for rows.Next() {var p Personerr = rows.Scan(&p.id, &p.name, &p.location)if err == nil {log.Printf("queryRowsByCondition Person is %#v\n", p)}}
}// 查询带条件多行
func queryRowsByPrepare(id int) {stmt, err2 := db.Prepare("SELECT * FROM persons WHERE id > ?")if err2 != nil {log.Fatalf("Prepare error: %v", err2)}rows, err := stmt.Query(id)if err != nil {log.Fatalf("查询数据库出错:%v", err)}for rows.Next() {var p Personerr = rows.Scan(&p.id, &p.name, &p.location)if err == nil {log.Printf("queryRowsByPrepare Person is %#v\n", p)}}
}func main() {// 查询一行数据queryOneRow()// 查询多行数据queryRows()// 查询带条件的多行queryRowsByCondition()// 使用prepare避免注入攻击queryRowsByPrepare(3)// 关闭数据库连接db.Close()
}
通过以上示例代码,操作MySQL的大致步骤总结如下所示:
- 驱动安装和导入,
_ "github.com/go-sql-driver/mysql" - 连接数据库并返回数据库句柄,
sql.Open("mysql",dsn),可查看文档:https://github.com/go-sql-driver/mysql - 调用db提供的各种方法,对数据库进行操作
- 可以使用db.Prepare预编译并使用参数化查询,其主要作用为
-
- 对预编译的SQL语句进行缓存,省去每次解析优化该SQL语句的过程
-
- 防止注入攻击
-
- 使用返回的sql.stmt对数据库进行操作
21.2 SQLBuilder
SQLBuilder是一个用于生成SQL语句的库,使用的第三方库为https://github.com/huandu/go-sqlbuilder,示例代码如下所示:
package mainimport ("database/sql""fmt""log""os""time"_ "github.com/go-sql-driver/mysql""github.com/huandu/go-sqlbuilder"
)var db *sql.DB
var err errorfunc init() {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"// dsn格式:[username[:password]@][protocol[(address)]]/dbname[?param1=value1&...¶mN=valueN]// 示例: username:password@protocol(address)/dbname?param=valuedsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)db, err = sql.Open("mysql", dsn)if err != nil {log.Fatalf("连接数据库出错:%v", err)}// 以下参数参考资料:https://github.com/go-sql-driver/mysqldb.SetConnMaxIdleTime(10 * time.Second)db.SetMaxOpenConns(0)db.SetMaxIdleConns(10)// fmt.Printf("%v\n", db)
}type Person struct {id intname stringlocation string
}// 查询多行
func queryRows() {// 用于生成SQL语句querySql, sqlArgs := sqlbuilder.Select("*").From("persons").Where("id > ?").OrderBy("id").Build()log.Printf("SQLBuilder query sql string : %#v sqlArgs : %#v", querySql, sqlArgs)rows, err := db.Query(querySql, 2)if err != nil {log.Fatalf("查询数据库出错:%v", err)}for rows.Next() {var p Personerr = rows.Scan(&p.id, &p.name, &p.location)if err == nil {log.Printf("queryRows Person is %#v\n", p)}}
}func main() {queryRows()
}
关于SQLBuilder更多参考资料可查看https://github.com/huandu/go-sqlbuilder,上述代码运行结果如下所示:
2025/05/05 15:53:00 SQLBuilder query sql string : "SELECT * FROM persons WHERE id > ? ORDER BY id" sqlArgs : []interface {}(nil)
2025/05/05 15:53:00 queryRows Person is main.Person{id:3, name:"Kevin", location:"USA"}
2025/05/05 15:53:00 queryRows Person is main.Person{id:4, name:"Ivy", location:"Hongkong"}
2025/05/05 15:53:00 queryRows Person is main.Person{id:5, name:"Odelia", location:"Serbia"}
21.3 ORM
ORM(Object Relational Mapping)是一种对象关系映射技术,它将代码中的对象与关系型数据库中的表进行映射。代码中的每个对象均代表数据库中表的一条记录或一个实体,通过这种映射使得后端开发能够通过操作编程语言中的对象来执行数据库操作。例如数据检索、存储、更新和删除。而不需要编写复杂的SQL语句。使用ORM的优势如下所示:
- 数据库抽象:ORM在代码和数据库之间建立了一个抽象层,使得后端开发可以专注于业务逻辑,无须处理数据库的底层细节
- 可移植性:通过ORM,开发者可以轻松切换不同的数据库系统,而不需要对应用代码进行大量修改。ORM可以自动处理不同数据库之间的SQL语法差异
- 增强安全性:ORM提供了内置的安全机制,其能够正确处理输入数据,并在代码与数据库之间交互时避免常见的安全漏洞,例如SQL注入攻击。
- 加快开发速度:ORM简化了数据库操作的复杂性,开发者无须手动编写SQL语句,从而提高了程序的开发效率。
关系模型和Go对象之间的映射如下所示:
| 数据库 | Go对象 | 映射对象 |
|---|---|---|
| table | struct | 表映射为结构体 |
| row | object | 行映射为实例 |
| column | property | 字段映射为属性 |
可以认为ORM是一种更高级的抽象,因为对象的操作最终还是会转换为数据库中的SQL语句,数据库操作的结构会被封装为对象。
21.4 GORM
21.4.1 什么是GORM
GORM是一个广泛使用的Go语言ORM库,提供了一种简单而优雅的代码与数据库交互的方式,支持多种数据库系统,如MySQL、SQLite、PostgreSQL、SQLServer等。通过GORM,开发者可以将数据库表映射到Go语言中的对象,从而使用Go代码而非直接使用SQL语句来操作数据库。
GORM采用了约定优先于配置的理念,意味着很多配置已经提前做好,开发者仅需要在必要时进行少量的自定义配置即可。这使得开发过程更加简化,能够更专注于业务逻辑,而无须过多关注数据库细节。GORM包含的主要功能如下所示:
- 与数据库无关:GORM能够与多种数据库系统配合使用,提高了高度的灵活性
- 支持CRUD操作:GORM提供了一整套的方法来执行创建、读取、更新、删除操作,使得通过Go语言结构体与数据库交互变得更加直观和简单
- 支持表间关系:GORM支持定义表之间的关系,如一对一、一对多、多对多等关系
- 支持事务:GORM提供了事务支持,允许开发者将多个数据库操作放在一个事务中,并在发生错误时自动回滚,从而保证数据的一致性
- 数据库迁移:GORM支持数据库模式的自动迁移管理,开发者可以通过Go代码定义数据库架构的更改,GORM会自动生成并执行相应权限的SQL语句来更新数据库结构
- 可扩展性:GORM高度可扩展,拥有可插拔的架构,允许开发者添加新功能或自定义现有功能。
21.4.2 GORM 基础入门
21.4.2.1 GORM 安装
执行安装命令
- 安装MySQL驱动
go get -u github.com/go-sql-driver/mysql
- 安装GORM
这里以MySQL为例
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
gorm.io/driver/mysql依赖于github.com/go-sql-driver/mysql,可以认为它是驱动的再次封装
GORM的官网地址:https://gorm.io/ , 相应的文档地址:https://gorm.io/docs/
21.4.2.2 连接数据库
示例代码如下所示:
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}fmt.Printf("%v\n", db)
}
dsn的组成部分如下所示:
- user:passwd: 数据库用户名和密码
- @tcp:代表所使用的协议
- (host:port):代表数据库的地址和端口
- /dbName: 代表连接数据库的名称
- ?charset=utf8mb4&parseTime=True&loc=Local:代表连接数据库的参数,分别代表指定字符集、启用时间解析、并将时区设置为本地
&gorm.Config{} 为GORM提供的默认配置。
21.4.3 模型定义
21.4.3.1 模型定义
模型是使用普通结构体定义的,这些结构体可以包含具有基本Go类型、指针或这些类型的别名。甚至是自定义类型(需要实现 database/sql 包中的Scanner和Valuer接口)。来看看以下user模型
type User struct {ID uint // Standard field for the primary keyName string // A regular string fieldEmail *string // A pointer to a string, allowing for null valuesAge uint8 // An unsigned 8-bit integerBirthday *time.Time // A pointer to time.Time, can be nullMemberNumber sql.NullString // Uses sql.NullString to handle nullable stringsActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fieldsCreatedAt time.Time // Automatically managed by GORM for creation timeUpdatedAt time.Time // Automatically managed by GORM for update timeignored string // fields that aren't exported are ignored
}
在此模型中:
- 具体数字类型如 uint、string和 uint8 直接使用。
- 指向 *string 和 *time.Time 类型的指针表示可空字段。
- 来自 database/sql 包的 sql.NullString 和 sql.NullTime 用于具有更多控制的可空字段。
- CreatedAt 和 UpdatedAt 是特殊字段,当记录被创建或更新时,GORM 会自动向内填充当前时间。
- 非导出字段(以小定字母开头的字段) 将不会映射导出
GORM倾向于约定优先于配置,如果不遵守约定就要自定义配置。以为为默认的约定:
- 主键:GORM 使用一个名为 ID 的字段作为每个模型的默认主键。
- 表名:默认情况下,GORM 将结构体名称转换为 snake_case 并为表名加上复数形式。 例如,一个 User 结构体在数据库中的表名变为 users 。
- 列名:GORM 自动将结构体字段名称转换为 snake_case 作为数据库中的列名。
- 时间戳字段:GORM使用字段 CreatedAt 和 UpdatedAt 来自动跟踪记录的创建和更新时间。
// 不符合约定的定义,需要自行配置,直接不能使用
type Person struct{id intname string // 首字母大写,需要配置location string
}// 符合约定的定义
type person struct{ // 默认表名personsId intName string // 首字母大写Location string
}
遵循这些约定可以大大减少您需要编写的配置或代码量。 但是,GORM也具有灵活性,允许您根据自己的需求自定义这些设置。
以上可参考文档:https://gorm.io/zh_CN/docs/models.html
21.4.3.2 表名配置
func (p Person) TableName() string {return "persons"
}
21.4.3.3 字段配置
type Person struct {id int `gorm:"primaryKey"` // 设置id为主键FirstName string `gorm:"column:name"`Loc string `gorm:"column:location"`
}
21.4.4 GORM 基本操作
21.4.4.1 创建
21.4.4.1.1 创建单条记录
GORM中的Create()方法用于在数据库表中插入一条记录。传入的参数是结构体指针,GORM会根据该结构体的值创建对应的记录。基本使用方式:
db.Create(&record)
示例代码如下所示:
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}p := Persons{Id: 6,Name: "Bruce",Location: "USA",}db.Create(&p)log.Printf("插入的Id为:%+v\n", p.Id)
}
21.4.4.1.2 创建多条记录
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}// 创建多个对象实例p := []Persons{{Id: 6, Name: "Bruce", Location: "USA"},{Id: 7, Name: "Eva", Location: "Canda"},{Id: 8, Name: "Linda", Location: "USA"},}db.Create(&p)log.Printf("插入的数据:%+v\n", p)
}
21.4.4.1.3 使用Map创建记录
GORM还支持通过Map类型的数据创建记录,适用于动态生成数据或不使用结构体的情况。示例代码如下所示:
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}personData := map[string]any{"Id": 6, "Name": "Linda", "Location": "USA",}var p Personsdb.Model(&Persons{}).Create(personData).Scan(&p)log.Printf("插入的数据:%+v\n", p.Id)
}
使用map时,map的键名必须与结构体字段的名称相匹配,并且值的类型必须正确
21.4.4.2 查询
21.4.4.2.1 查询单个对象
GORM中的First方法用于查询符合条件的第一条记录,即查询单个对象。开发者可以通过Select方法指定要查询的字段。
db.First(&obj,condition)
db.Take(&obj,condition)
db.Last(&obj,condition)
示例代码如下所示:
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}var p Persons// 按主键排序后,从中筛选出符合条件的第一个数据记录,未指定筛选条件,则相当于ORDER BY PRIMARY_KEY LIMIT 1,如果指定筛选条件,则按筛选条件来// result := db.First(&p, 5)// 相当于不指定排序,直接LIMIT 1// result := db.Take(&p)// 未指定筛选条件,相当于ORDER BY PRIMARY_KEY DESC LIMIT 1result := db.Last(&p)if result.Error != nil {log.Fatalf("查询数据异常")}log.Printf("查询到的数据:%+v\n", p)
}
21.4.4.2.2 查询所有对象
GORM中的Find方法用于查询数据库表中所有对象,也可以通过Select方法指定需要查询的字段。
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}// 定义查询容器,即所有人员var p []Persons// 方式一// result := db.Select("id", "name").Find(&p)// 方式二:result := db.Select([]string{"id", "name"}).Find(&p)if result.Error != nil {log.Fatalf("查询数据异常")}for _, v := range p {log.Printf("查询到的名字:%+v\n", v.Name)}
}
21.4.4.2.3 条件查询对象
GORM中的条件查询,可使用Where方法,基本使用方式:
db.Select("fields...").Where("condition").Find(&resutl)
示例代码如下所示:
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}// 定义查询容器,即所有人员var p []Persons// 单条件查询// result := db.Select([]string{"id", "name"}).Where("id > ?", 5).Find(&p)// 多条件查询// result := db.Select([]string{"id", "name"}).Where("id > ?", 2).Where("name LIKE ?", "Linda").Find(&p)// 按结构体查询// result := db.Select("id", "name").Where(&Persons{Name: "Surpass", Id: 1}).Find(&p)// 排序// result:=db.Order("id DESC").Find(&p)// Limit offsetresult := db.Order("id DESC").Limit(3).Offset(2).Find(&p)if result.Error != nil {log.Fatalf("查询数据异常")}for _, v := range p {log.Printf("查询到的名字:%+v\n", v.Id)}
}
条件查询常见用法如下所示:
// 定义查询容器,即所有人员var p []Persons// 单条件查询result := db.Select([]string{"id", "name"}).Where("id > ?", 5).Find(&p)// 多条件查询result := db.Select([]string{"id", "name"}).Where("id > ?", 2).Where("name LIKE ?", "Linda").Find(&p)// 按结构体查询result := db.Select("id", "name").Where(&Persons{Name: "Surpass", Id: 1}).Find(&p)// 排序result:=db.Order("id DESC").Find(&p)// Limit offsetresult := db.Order("id DESC").Limit(3).Offset(2).Find(&p)
21.4.4.2.4 分组
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}// 需要定义一个结构体来保存结果
type Result struct {Location stringCount int
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}var p []Personsvar rets []Resultresult := db.Find(&p)if result.Error != nil {log.Fatalln("查询数据异常")}log.Printf("Query:%+v\n", p)result = db.Model(&p).Select("location,COUNT(location) AS count").Group("location").Find(&rets)if result.Error != nil {log.Fatalln("GROUP数据异常")}log.Printf("Group:%+v\n", rets)
}
运行结果如下所示:
2025/05/06 12:08:27 Query:[{Id:1 Name:Surpass Location:Shanghai} {Id:2 Name:Evan Location:Canda} {Id:3 Name:Kevin Location:USA} {Id:4 Name:Ivy Location:Hongkong} {Id:5 Name:Odelia Location:Serbia} {Id:6 Name:Linda Location:USA}]
2025/05/06 12:08:27 Group:[{Location:Shanghai Count:1} {Location:Canda Count:1} {Location:USA Count:2} {Location:Hongkong Count:1} {Location:Serbia Count:1}]
21.4.4.3 更新
GROM 数据库对象中的Save方法可用于更新数据库表中的现有记录,或插入不存在的新记录。它以一个指针做为输入,使用结构体的值更新表中相应的记录,如果存在错误则返回错误。
db.Save()方法会保存所有字段,对于没有主键的实例相当于INSERT INTO,存在主键的实例相当于UPDATE
21.4.4.3.1 保存单个记录
更新相当于先查再修改,可以对这个实例属性进行修改,然后再调用db.Save()方法保存,保存记录有两种方式,示例代码如下所示:
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}type Salary struct {Id intSalary float64Level string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}// 先查再修改var s Salarydb.Where("id = ?", 1).First(&s)log.Printf("%+v\n", s)s.Salary += 1000s.Level = "T15"db.Save(&s)log.Printf("salary is %+v\n", s)
}
运行结果如下所示:
2025/05/06 00:04:30 {Id:1 Salary:2000 Level:T10}
2025/05/06 00:04:30 salary is {Id:1 Salary:3000 Level:T15}
21.4.4.3.2 保存多个记录
利用更新来保存所有记录,相当于INSERT INTO功能,示例代码如下所示:
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}type Salary struct {Id intSalary float64Level string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}var s []Salary = []Salary{{Id: 2, Salary: 3000, Level: "P6"},{Id: 3, Salary: 5000, Level: "T9"},}for _, v := range s {db.Save(&v)}result := db.Find(&s)if result.Error != nil {log.Fatalln("查询数据异常")}fmt.Printf("s:%+v\n", s)
}
运行结果如下所示:
s:[{Id:1 Salary:3000 Level:T15} {Id:2 Salary:3000 Level:P6} {Id:3 Salary:5000 Level:T9}]
21.4.4.3.3 更新单列
GORM 数据库对象中的 UpdateColumn 方法用于更新数据库中现有记录的特定列,需要传入两个参数 更新的列名 和 列的新值。
UpdateColumn 没有指定ID或Where ,相当于UPDATE TableName SET ColumnName = ?,会全表更新,是非常危险的,日常使用不推荐。
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}type Salary struct {Id intSalary float64Level string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}var s []Salaryresult := db.Find(&s)if result.Error != nil {log.Fatalln("查询数据异常")}fmt.Printf("s:%+v\n", s)result=db.Model(&s).UpdateColumn("salary",8000)if result.Error != nil {log.Fatalln("查询数据异常")}fmt.Printf("s:%+v\n", s)
}
21.4.4.3.4 更新多列
GORM数据库对象中的Updates方法用于更新数据库表中现有记录的一列或多列。Updates方法采用Map或结构体做为输入,键表示要更新的列名称,值表示列的新值
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}type Salary struct {Id intSalary float64Level string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}var s []SalaryresultBeforeUpdate := db.Find(&s)if resultBeforeUpdate.Error != nil {log.Fatalln("查询数据异常")}log.Printf("s:%+v\n", s)// 使用Map更新resultMap := db.Model(&s).Where("id <= ? ", 2).Updates(map[string]any{"Salary": 1234, "Level": "T0"})log.Printf("通过Map更新影响的行数为:%d", resultMap.RowsAffected)// 使用结构体更新resultStruct := db.Model(&s).Where("id > ? ", 2).Updates(Salary{Salary: 5678, Level: "P8"})log.Printf("通过结构体更新影响的行数为:%d", resultStruct.RowsAffected)resultAfterUpdate := db.Find(&s)if resultAfterUpdate.Error != nil {log.Fatalln("查询数据异常")}log.Printf("s:%+v\n", s)
}
运行结果如下所示:
2025/05/06 00:51:52 s:[{Id:1 Salary:1000 Level:T9} {Id:2 Salary:2000 Level:T5} {Id:3 Salary:3000 Level:P1}]
2025/05/06 00:51:52 通过Map更新影响的行数为:2
2025/05/06 00:51:52 通过结构体更新影响的行数为:1
2025/05/06 00:51:52 s:[{Id:1 Salary:1234 Level:T0} {Id:2 Salary:1234 Level:T0} {Id:3 Salary:5678 Level:P8}]
21.4.4.4 删除
在日常操作中,删除属于危险动作,操作时需要慎重执行。
21.4.4.4.1 删除记录
GORM数据库对象中的Delete方法用于根据指定条件从数据库表中删除一条或多条记录。
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}type Salary struct {Id intSalary float64Level string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}var s []Salaryvar salary SalaryresultBeforeDelete := db.Find(&s)if resultBeforeDelete.Error != nil {log.Fatalln("查询数据异常")}log.Printf("s:%+v\n", s)// 使用条件删除 DELETE FROM salaries WHERE level = 'T1'resultDeleteConditional := db.Where("level = ?", "T1").Delete(&salary)log.Printf("通过删除影响的行数为:%d", resultDeleteConditional.RowsAffected)// 根据主键删除 DELETE FROM salaries WHERE id = 5resultDeletePrimaryKey := db.Delete(&Salary{}, 5)log.Printf("通过删除影响的行数为:%d", resultDeletePrimaryKey.RowsAffected)// 使用IN删除 DELETE FROM salaries WHERE id IN (2,3)resultDeleteIn := db.Delete(&Salary{}, []int{2, 3})log.Printf("通过删除影响的行数为:%d", resultDeleteIn.RowsAffected)resultAfterDelete := db.Find(&s)if resultAfterDelete.Error != nil {log.Fatalln("查询数据异常")}log.Printf("s:%+v\n", s)
}
运行结果:
2025/05/06 11:15:34 s:[{Id:1 Salary:3800 Level:T1} {Id:2 Salary:5890 Level:T2} {Id:3 Salary:3000 Level:T3} {Id:4 Salary:5890 Level:T4} {Id:5 Salary:9087 Level:T5}]
2025/05/06 11:15:34 通过删除影响的行数为:1
2025/05/06 11:15:34 通过删除影响的行数为:1
2025/05/06 11:15:34 通过删除影响的行数为:2
2025/05/06 11:15:34 s:[{Id:4 Salary:5890 Level:T4}]
21.4.4.4.21 软删除
在GORM中,可以使用软删除将记录标记为已删除,而不是将其从数据库中物理删除。软删除常用于当开发者想要跟踪已删除的记录并能在需要时恢复它们的场景。如果需要启用软删除功能,需要在模型中包含gorm.DeletedAt字段,则该模型将会自动获取软删除能力,当调用Delete时,GORM并不会从数据库中删除该记录,而是将该记录的DeleteAt设置为当前时间,而后面的一般查询方法将无法查询到该记录。
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"fmt""log""os""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}type Salary struct {Id intSalary float64Level stringDeletedAt gorm.DeletedAt // 添加DeletedAt,启用软删除能力
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}var s []Salaryvar dats []Salaryvar salary SalaryresultBeforeDelete := db.Find(&s)if resultBeforeDelete.Error != nil {log.Fatalln("查询数据异常")}log.Printf("s:%+v\n", s)// 使用条件删除 DELETE FROM salaries WHERE level = 'T1'resultDeleteConditional := db.Where("level = ?", "T1").Delete(&salary)log.Printf("通过删除影响的行数为:%d", resultDeleteConditional.RowsAffected)// 根据主键删除 DELETE FROM salaries WHERE id = 5resultDeletePrimaryKey := db.Delete(&Salary{}, 5)log.Printf("通过删除影响的行数为:%d", resultDeletePrimaryKey.RowsAffected)// 使用IN删除 DELETE FROM salaries WHERE id IN (2,3)resultDeleteIn := db.Delete(&Salary{}, []int{2, 3})log.Printf("通过删除影响的行数为:%d", resultDeleteIn.RowsAffected)resultAfterDelete := db.Find(&s)if resultAfterDelete.Error != nil {log.Fatalln("查询数据异常")}// 查询被软删除的记录resultSoftDelete := db.Unscoped().Find(&dats)if resultSoftDelete.Error != nil {log.Fatalln("查询数据异常")}log.Printf("删除后还剩余的数据::%+v\n", s)log.Printf("软删除后的数据:%+v\n", dats)
}
运行结果如下所示:
2025/05/06 11:47:52 s:[{Id:1 Salary:3800 Level:T1 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} {Id:2 Salary:5890 Level:T2 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} {Id:3 Salary:3000 Level:T3 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} {Id:4 Salary:5890 Level:T4 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} {Id:5 Salary:9087 Level:T5 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}}]
2025/05/06 11:47:52 通过删除影响的行数为:1
2025/05/06 11:47:52 通过删除影响的行数为:1
2025/05/06 11:47:52 通过删除影响的行数为:2
2025/05/06 11:47:55 删除后还剩余的数据::[{Id:4 Salary:5890 Level:T4 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}}]
2025/05/06 11:47:55 软删除后的数据:[{Id:1 Salary:3800 Level:T1 DeletedAt:{Time:2025-05-06 11:47:52 +0800 CST Valid:true}} {Id:2 Salary:5890 Level:T2 DeletedAt:{Time:2025-05-06 11:47:52 +0800 CST Valid:true}} {Id:3 Salary:3000 Level:T3 DeletedAt:{Time:2025-05-06 11:47:52 +0800 CST Valid:true}} {Id:4 Salary:5890 Level:T4 DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} {Id:5 Salary:9087 Level:T5 DeletedAt:{Time:2025-05-06 11:47:52 +0800 CST Valid:true}}]
21.4.4.5 案例
本案例将演示使用GORM从MySQL数据库中导出数据到CSV文件。示例代码如下所示:
package mainimport (// https://pkg.go.dev/gorm.io/driver/mysql"encoding/csv""fmt""log""os""strconv""gorm.io/driver/mysql""gorm.io/gorm"
)func getDsn() string {mysqlHost := os.Getenv("MYSQL_HOST")mysqlPort := os.Getenv("MYSQL_PORT")mysqlUsername := os.Getenv("MYSQL_USERNAME")mysqlPasswd := os.Getenv("MYSQL_PASSWD")dbName := "gorm"dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", mysqlUsername, mysqlPasswd, mysqlHost, mysqlPort, dbName)return dsn
}type Persons struct {Id intName, Location string
}var db *gorm.DB
var err errorfunc main() {dsn := getDsn()db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})if err != nil {log.Fatalf("连接数据库错误:%+v", err)}var p []Personsresult := db.Find(&p)if result.Error != nil {log.Fatalln("查询数据异常")}log.Printf("Query:%+v\n", p)var f *os.Filef, err = os.Create("users.csv")if err != nil {log.Fatal("文件创建出错")}defer f.Close()w := csv.NewWriter(f)w.Write([]string{"id", "name", "location"})for _, v := range p {w.Write([]string{strconv.Itoa(v.Id), v.Name, v.Location})}w.Flush()
}
最终生成的CSV文件如下所示:
id,name,location
1,Surpass,Shanghai
2,Evan,Canda
3,Kevin,USA
4,Ivy,Hongkong
5,Odelia,Serbia
6,Linda,USA
本文同步在微信订阅号上发布,如各位小伙伴们喜欢我的文章,也可以关注我的微信订阅号:woaitest,或扫描下面的二维码添加关注:
