fupanc's blog

GORM框架

ORM(Object Relational Mapping,对象关系映射)框架,就是将go中的结构体与数据库表进行映射。通过使用orm框架,可以进行增删改查等操作,无无需编写sql语句,框架会根据对应代码设置生成相应的sql语句并在数据库中执行。这里来学习一下利用GORM框架来操作Mysql数据库的相关代码实现。

需要下先下载对应依赖包以及数据库驱动:

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

前置知识

结构体相关

前面说到了ORM就是结构体与数据库表相互映射,这里来简单了解一下结构体的相关设置。

一个常见的使用:

type User struct {
	gorm.Model
	Name string
	Age int `gorm:"default:18"`
}

这里引入了gorm预定义的结构体,直接跟进其实对应结构如下:

type Model struct {
	ID        uint `gorm:"primarykey"`
	CreatedAt time.Time
	UpdatedAt time.Time
	DeletedAt DeletedAt `gorm:"index"`
}

里面几个字段的定义以及特殊设定:

故前面的结构体其实就相当于如下:

type User struct {
	ID        uint `gorm:"primarykey"`
	Name      string
	Age       int `gorm:"default:18"`
	CreatedAt time.Time `gorm:"autoCreateTime"`
	UpdatedAt time.Time `gorm:"autoUpdateTime"`
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

故总体来说,这里通过标签(tag)来定义了ID为主键,Age的默认值为18,然后设置了auto….Time等标签来让字段的值自动更新,最后还可以看到设置了一个DeletedAt的索引。由此可见,go语言中结构体的tag是非常重要的,有非常多的用处。

当然结构体还可以直接嵌入另一个的结构体,可直接通过结构体名来输出,也可以通过标签embedded来嵌入,后续有需求再继续编写相关代码。

连接方式

连接mysql:

package main

import (
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)
func main() {
	dsn := "root:root@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local)"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

直接通过驱动来连接即可。后续就简单说说GORM框架的增删改查要怎么编写。

数据库操作demo

表创建

使用AutoMigrate()方法来创建表:

package main

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

type User struct {
	gorm.Model
	Name string
	Age  int `gorm:"default:18"`
}

func main() {
	//连接数据库:
	dsn := "root:root@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	//创建结构体对应的表:
	user_table := &User{}
	err1 := db.AutoMigrate(user_table)
	if err1 != nil {
		fmt.Println(err1)
	}
}

注意调用AutoMigrate()方法的对象,是连接数据库后的对象,简单理解一下即可。运行代码可以再数据库中看到成功创建表:

mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| users            |
+------------------+
1 row in set (0.00 sec)

mysql> desc users;
+------------+-----------------+------+-----+---------+----------------+
| Field      | Type            | Null | Key | Default | Extra          |
+------------+-----------------+------+-----+---------+----------------+
| id         | bigint unsigned | NO   | PRI | NULL    | auto_increment |
| created_at | datetime(3)     | YES  |     | NULL    |                |
| updated_at | datetime(3)     | YES  |     | NULL    |                |
| deleted_at | datetime(3)     | YES  | MUL | NULL    |                |
| name       | longtext        | YES  |     | NULL    |                |
| age        | bigint          | YES  |     | 18      |                |
+------------+-----------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)

以及对应的表结构也是都符合的结构体的设置的,这里需要注意的一个点就是gorm默认会将结构体名转小写然后加复数成为新的表名,这里也就会创建名为users的表,想自定义表名就需要在结构体后面加标签,如下demo:

package main

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

type User struct {
	gorm.Model
	Name string
	Age  int `gorm:"default:18"`
}

func (u *User) TableName() string {
	return "user"
}

func main() {
	//连接数据库:
	dsn := "root:root@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	//创建结构体对应的表:
	user_table := &User{}
	err1 := db.Debug().AutoMigrate(user_table)
	if err1 != nil {
		fmt.Println(err1)
	}
}

定义了一个绑定在User结构体方法,这样在创建的时候就会获取这个方法的返回值,而不是按照默认的走,运行后成功创建了user表。

并且在这里的创建语句中,我是加了一个调试模式的:

user_table := &User{}
err1 := db.Debug().AutoMigrate(user_table)

在控制台可以看到对应的使用的sql语句:

image-20260206180408761

可以看到存在创建语句,再运行一次就可以知道如果表中有这个表并且字段对应,那么就不会再调用Create语句来创建表了,只会更新字段(如果字段变化)。

同样的这个调试模式可以用到其他的语句调用中,格式同等替换即可。

添加数据

单条插入数据(Create()方法):

package main

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

type User struct {
	gorm.Model
	Name string
	Age  int `gorm:"default:18"`
	Sex  string
}

func main() {
	//连接数据库:
	dsn := "root:root@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	//创建结构体对应的表:
	//user_table := &User{}
	//err1 := db.Debug().AutoMigrate(user_table)
	//if err1 != nil {
	//	fmt.Println(err1)
	//}
	people := &User{Name: "fupanc", Age: 22, Sex: "boy"} //成功插入后自动修改ID字段的值
	result := db.Create(people)
	if result.Error != nil {
		panic("failed to create user")
	}
	fmt.Printf("User created with ID %d", people.ID)

}

控制台输出预期值,同样的表数据也发生了变化:

image-20260206190817987

插入多条数据,可以使用for循环,比如如下demo:

package main

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

type User struct {
	gorm.Model
	Name string
	Age  int `gorm:"default:18"`
	Sex  string
}

func main() {
	//连接数据库:
	dsn := "root:root@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}
	peoples := []User{{Name: "fupanc1", Age: 23, Sex: "boy"}, {Name: "fupanc2", Age: 24, Sex: "boy"}, {Name: "fupanc3", Age: 25, Sex: "boy"}}
	for _, people := range peoples {
		result := db.Create(&people)
		if result.Error != nil {
			fmt.Println(result.Error)
		}
		fmt.Println("成功插入数据,ID为:", people.ID)
	}
}

输出为:

成功插入数据,ID为: 2
成功插入数据,ID为: 3
成功插入数据,ID为: 4

按照自增要求成功插入数据。

后面发现其实可以直接给数组形式,直接批量插入:

peoples := []User{{Name: "fupanc4", Age: 23, Sex: "boy"}, {Name: "fupanc5", Age: 24, Sex: "boy"}, {Name: "fupanc6", Age: 25, Sex: "boy"}}
result := db.Create(&peoples)

批量插入还可以使用CreateInBatches()来控制分批次多次插入数据:

peoples := []User{{Name: "fupanc5", Age: 23, Sex: "boy"}, {Name: "fupanc6", Age: 24, Sex: "boy"}, {Name: "fupanc7", Age: 25, Sex: "boy"}}
result := db.CreateInBatches(&peoples, 2)

删除数据

数据删除主要分逻辑删除和物理删除:

逻辑删除,通常通过Delete()方法来操作:

package main

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

type User struct {
	gorm.Model
	Name string
	Age  int `gorm:"default:18"`
	Sex  string
}

func main() {
	//连接数据库:
	dsn := "root:root@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}
	//逻辑删除
	db.Delete(&User{}, "id > 1")
}

执行效果:

image-20260206193736076

可以看到deleted_at的值发生了变化。同样的条件还可以这样设置:

db.Where("id > ?", 0).Delete(&User{})

debug一下可以发现执行的sql语句为:

UPDATE `users` SET `deleted_at`='2026-02-06 19:44:47.62' WHERE id > 1 AND `users`.`deleted_at` IS NULL

软删除,一个更新操作。

物理删除,需要Unscoped()+Delete()方法一起才能物理删除:

//物理删除
db.Where("id > ?", 1).Unscoped().Delete(&User{})

执行后可以发现id大于1的都被完全删掉了:

image-20260206194402031

debug一下可以发现执行的sql语句为:

DELETE FROM `users` WHERE id > 1

直接调用delete操作删除数据。

数据查询

获取单条数据(First、last、take)

获取第一条数据(First()):

user := User{}//结构体接收查询到的数据
//即根据id排序然后获取第一条数据
first_data := db.Debug().First(&user)
if first_data.Error != nil {
	panic("failed to connect database")
}
fmt.Println(user)

获取查询到的最后一条数据(Last()):

user := User{}
//即根据id排降序获取第一条数据
last_data := db.Debug().Last(&user, "age < 14")
if last_data.Error != nil {
	panic("failed to connect database")
}
fmt.Println(user)

不指定排序,取第一条(Take()):

user := User{}
//不指定排序获取第一条数据:
last_data := db.Debug().Where("age < ?", 14).Take(&user)
if last_data.Error == gorm.ErrRecordNotFound {
	panic("数据为空")
}
fmt.Println(user)

至于具体是怎么排的,看日志里面的sql语句即可。

如果查询不到数据,那么result.Error就为gorm.ErrRecordNotFound,可通过这个来判断查询结果是否为空。

获取所有数据

users := []User{}
//获取所有数据:
last_data := db.Debug().Where("age < ?", 14).Find(&users)
if last_data.Error == gorm.ErrRecordNotFound {
	panic("数据为空")
}
fmt.Println(users)

注意这里要使用User类型的数组,思考一下就可以知道,由于是映射关系,如果查询结果有多个,需要使用多个结构体来接收对应的值,否则只能获取到第一条数据,由于这里的使用场景,故我们需要设置数组类型来接收查询的结果。

延伸使用:

users := []User{}
//指定字段:
last_data := db.Debug().Select("id,name,age").Where("age < ?", 14).Find(&users)
if last_data.Error == gorm.ErrRecordNotFound {
	panic("数据为空")
}
fmt.Println(users)//注意虽然这里只查询了几个字段,但是映射到结构体上,没查询的值是零值

//排序查询:
last_data := db.Debug().Order("age desc").Find(&users)


//注意都是可以嵌套使用的:
last_data := db.Debug().Select("id,name,age").Order("age desc").Limit(2).Find(&users)


//结果转json格式:
usersJson, _ := json.MarshalIndent(users, "", "  ")
fmt.Println(string(usersJson))

需要的时候直接用就行,不是很难。

数据更改

单字段更新(Update()方法):

package main

import (
	"encoding/json"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Name string
	Age  int `gorm:"default:18"`
	Sex  string
}

func main() {
	//连接数据库:
	dsn := "root:root@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	users := User{}
	//获取一条数据:
	last_data := db.Debug().Select("id,name,age").Take(&users)
	if last_data.Error == gorm.ErrRecordNotFound {
		panic("数据为空")
	}
	fmt.Println(transform_data(users))

	//更新数据
	updating_data := users.ID
	user1 := User{}
	db.Model(&user1).Debug().Where("id = ?", updating_data).Update("name", "fupanc123-updated")

	//获取更新后的数据:
	user2 := User{}
	db.Debug().Where("id = ?", updating_data).Find(&user2)
	fmt.Println(transform_data(user2))
}

func transform_data(users interface{}) string {
	usersJson, _ := json.MarshalIndent(users, "", "  ")
	return string(usersJson)
}

输出效果:

{
  "ID": 11,
  "CreatedAt": "0001-01-01T00:00:00Z",
  "UpdatedAt": "0001-01-01T00:00:00Z",
  "DeletedAt": null,
  "Name": "fupanc1",
  "Age": 12,
  "Sex": ""
}

{
  "ID": 11,
  "CreatedAt": "2026-02-09T11:24:00.783+08:00",
  "UpdatedAt": "2026-02-09T13:45:09.537+08:00",
  "DeletedAt": null,
  "Name": "fupanc123-updated",
  "Age": 12,
  "Sex": "boy"
}

可以看到成功更新了对应字段的值。

多字段更新(Updates()方法):

可通过结构体或者Map更新多字段,但是结构体更新会忽略零值,Map更新支持零值,结构体代码如下:

package main

import (
	"encoding/json"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

type User struct {
	gorm.Model
	Name string
	Age  int `gorm:"default:18"`
	Sex  string
}

func main() {
	//连接数据库:
	dsn := "root:root@tcp(127.0.0.1:3306)/testdb?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	users := User{}
	//获取一条数据:
	last_data := db.Debug().Where("id = 13").Select("id,name,age").Find(&users)
	if last_data.Error == gorm.ErrRecordNotFound {
		panic("数据为空")
	}
	fmt.Println(transform_data(users))

	//更新数据
	updating_data := users.ID
	user1 := User{}
	db.Model(&user1).Debug().Where("id = ?", updating_data).Updates(User{Name: "fupanc1234-updated", Age: 0})
	//获取更新后的数据:
	user2 := User{}
	db.Debug().Select("id,name,age").Where("id = ?", updating_data).Find(&user2)
	fmt.Println(transform_data(user2))
}

func transform_data(users interface{}) string {
	usersJson, _ := json.MarshalIndent(users, "", "  ")
	return string(usersJson)
}

效果如下:

{
  "ID": 13,
  "CreatedAt": "0001-01-01T00:00:00Z",
  "UpdatedAt": "0001-01-01T00:00:00Z",
  "DeletedAt": null,
  "Name": "fupanc3",
  "Age": 14,
  "Sex": ""
}

{
  "ID": 13,
  "CreatedAt": "0001-01-01T00:00:00Z",
  "UpdatedAt": "0001-01-01T00:00:00Z",
  "DeletedAt": null,
  "Name": "fupanc1234-updated",
  "Age": 14,
  "Sex": ""
}

可以看到age的值并没有改变,这是因为结构体初始化后就会以零值代替,虽然这里想将Age设置为0,但是会被当成零值,也就是不需要更新,不能达到想要的效果。于是使用Map来解决这个问题。

Map代码如下:

db.Model(&user1).Debug().Where("id = ?", updating_data).Updates(map[string]interface{}{"Name": "fupanc", "Age": 0})//只需要修改一行代码如上

最后运行效果如下:

{
  "ID": 13,
  "CreatedAt": "0001-01-01T00:00:00Z",
  "UpdatedAt": "0001-01-01T00:00:00Z",
  "DeletedAt": null,
  "Name": "fupanc1234-updated",
  "Age": 14,
  "Sex": ""
}

{
  "ID": 13,
  "CreatedAt": "0001-01-01T00:00:00Z",
  "UpdatedAt": "0001-01-01T00:00:00Z",
  "DeletedAt": null,
  "Name": "fupanc",
  "Age": 0,
  "Sex": ""
}

可以看到json和age的值都改变了。后续可以多用。

当然GORM还有一些内置方法可以使用,后续再挖掘吧,先到这里了。

参考文章:

https://gorm.io/zh_CN/docs/models.html

https://cloud.tencent.com/developer/article/2627941