Skip to content

数据库 GUI 客户端

  • 推荐使用dbgate轻量/开源/可免费使用

介绍

gorm 是go语言中比较受欢迎的 ORM 库, 类似 nodejs 中的 typeorm/sequelize 等, 主要功能是用来操作数据库(包括: sqlite/MySQL/Postgres 等主流数据库), 它功能强大, 提供了类似数据库迁移/数据表数据的CURD/ 关系映射/关联模式等实用功能

安装

这里以 sqlite 为例

sh
# 安装 gorm
go get -u gorm.io/gorm

# 安装数据库链接驱动(只需要安装你需要的数据库驱动即可)
go get -u gorm.io/driver/sqlite
go get -u gorm.io/driver/mysql
go get -u gorm.io/driver/postgres
go get -u gorm.io/driver/sqlserver
go get -u github.com/alifiroozi80/duckdb

快速入门

  1. 链接数据库
  2. 数据库迁移(自动建表)
  3. 数据库数据CURD
  4. 先体验增删改查,后续学习细节
  5. 注意点:使用 gorm 有两种 API: 分别是 传统API泛型API, 由于 GO 支持泛型的编译器版本是 1.18, 所以如果要使用gorm 的 泛型API, 请确保你的go编译器版本大于 1.18, 否则使用泛型 API 会报错

[关于泛型 API 与传统 API 的选择问题]

  1. 统一风格最重要, 要么全部使用 传统API , 要么全部使用 泛型API, 不要混用
  2. 由于Go语言的泛型符号和列表符号用的都是 [] 而不是 <>, 这实在是太反直觉了 所以我还是选择用 传统API 就可以了, 泛型API代码看着太乱了
  3. 使用 传统 API 可以兼容更低版本的 go 语言编译器
go
package main

import (
	"context"
	"fmt"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// 1.链接数据库
func connectDB() *gorm.DB {
	// 如果你使用 mysql 数据库, 你可以这样链接
	// dsn4mysql := "mysql://root:123456@tcp(127.0.0.1:3306)/gorm_demo?charset=utf8mb4
	dsn := "./test.sqlite"
	db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
	if err != nil {
		panic("连接数据库失败")
	}
	return db
}

type User struct {
	gorm.Model
	ID       int
	Email    string
	Password string
}

// 2.自动创建数据表
func migrate(db *gorm.DB) {
	db.AutoMigrate(&User{})
}

// 3.创建数据
func createUsers(ctx context.Context, db *gorm.DB) {
	_ = gorm.G[User](db).Create(ctx, &User{
		ID:       1001,
		Email:    "test@example.com",
		Password: "123456",
	})

	_ = gorm.G[User](db).Create(ctx, &User{
		ID:       1002,
		Email:    "test@test.com",
		Password: "654321",
	})
}

// 4.查询数据
func selectUsers(ctx context.Context, db *gorm.DB) {
	// 查询数据所有数据
	// users, _ := gorm.G[User](db).Find(ctx)

	// 查询第一条数据
	user1, _ := gorm.G[User](db).First(ctx)
	fmt.Println(user1.ID, user1.Email, user1.Password)

	// 查询 id=1002 的数据
	user2, _ := gorm.G[User](db).Where("id = ?", 1002).First(ctx)
	fmt.Println(user2.ID, user2.Email, user2.Password)
}

// 5.更新数据
func updateUsers(ctx context.Context, db *gorm.DB) {
	// 修改一个字段: id=1002 的数据, 将 email 修改为 -> 1002@example.com
	effectRows, _ := gorm.G[User](db).Where("id = ?", 1002).Update(ctx, "email", "1002@example.com")
	fmt.Println("effectRows", effectRows)

	// 修改多个字段: id=1001 的数据, 将 email 修改为 -> 1001@example.com, password 修改为 -> 111111
	effectRows2, _ := gorm.G[User](db).Where("id = ?", 1001).Updates(ctx, User{
		Email:    "1001@example.com",
		Password: "111111",
	})
	fmt.Println("effectRows2", effectRows2)
}

// 6.删除数据
func deleteUsers(ctx context.Context, db *gorm.DB) {
	deleteRows, _ := gorm.G[User](db).Where("id = ?", 1001).Delete(ctx)
	fmt.Println("deleteRows", deleteRows)
}

func main() {
	db := connectDB()
	migrate(db)

	ctx := context.Background()
	createUsers(ctx, db)
	selectUsers(ctx, db)
	updateUsers(ctx, db)
	deleteUsers(ctx, db)
	// 运行代码后, 请用第三方GUI客户端查看效果
}

db package

封装一个包专门用与链接数据库操作, 因为后续的操作都是必须先链接数据库然后再操作的

go
package db

import (
	"fmt"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

// 数据库链接配置
var DSN = "./test.sqlite"

// 数据库链接
var Connection *gorm.DB = nil

// 链接数据库
func ConnectDB() *gorm.DB {
	if Connection != nil {
		return Connection
	}

	db, err := gorm.Open(sqlite.Open(DSN), &gorm.Config{})
	if err != nil {
		panic("连接数据库失败")
	}

	Connection = db
	return Connection
}

// 填充数据库: 给数据库填充一些假数据方便后续查询操作
func SeedDB() {
	var users []UserModel
	for i := 1; i <= 30; i++ {
		users = append(users, UserModel{
			Email:    fmt.Sprintf("test%d@test.com", i),
			Password: fmt.Sprintf("%d", 123456+i),
		})
	}

	Connection.CreateInBatches(&users, len(users))
	fmt.Println("===数据库填充完成===")
}

单表建模

Gorm 遵循约定大于配置的哲学, 所以记录笔记是一方面, 最好还是要多看文档

go
package db

import "gorm.io/gorm"

type UserModel struct {
	ID         int    `gorm:"primaryKey;autoIncrement;comment:主键"` // 添加主键(自增)
	Email      string `gorm:"unique;comment:邮箱"`                   // 添加唯一索引
	Password   string `gorm:"size:128;comment:密码"`                 // 指定长度为128
	gorm.Model        // 会带有约定的配置(id/created_at/updated_at/delete_at)
}

// 自定义表名(如果不实现这个方法表名则会是 user_models)
func (u *UserModel) TableName() string {
	return "users"
}
go
package main

import (
	"gorm_demo/db"
)

func Insert() {
	db.Connection.Create(&db.UserModel{
		Email:    "test@test.com",
		Password: "123456",
	})
}

func main() {
	db.ConnectDB()
	db.Connection.AutoMigrate(&db.UserModel{})
	db.SeedDB(); // 填充数据

	users := []db.UserModel{}
	db.Connection.Debug().Find(&users)

	// Debug(): 可以查看执行的 SQL 语句
	// [0.050ms] [rows:0] SELECT * FROM `users` WHERE `users`.`deleted_at` IS NULL
}

创建

  • create: 会触发 beforeCreate 钩子函数
go
package main

import (
	"fmt"

	"gorm_demo/db"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&db.UserModel{})
	// db.SeedDB()

	newUser := db.UserModel{
		Email:    "new-user@example.com",
		Password: "123456",
	}

	result := db.Connection.Create(&newUser)
	fmt.Println(newUser.ID)          // 回填插入数据的主键 31
	fmt.Println(result.Error)        // 返回 error        <nil>
	fmt.Println(result.RowsAffected) // 返回插入记录的条数 1
}

更新

更新总共有4个方法: Save Update UpdateColumn Updates

  • Save: 有主键字段对应的记录, 就是更新, 否则就创建
  • Update: 必须设置更新条件, 会触发 beforeUpdate 钩子函数, 返回被更新的记录数
  • Updates: 一次更新多个字段可以传入 map/struct, 会触发 beforeUpdate 钩子函数, 返回被更新的记录数
  • UpdateColumn: 不会触发 beforeUpdate 钩子函数
go
package main

import (
	"fmt"

	"gorm_demo/db"
)

// 创建用户(没有 ID 属性)
func createUser() *db.UserModel {
	var user1 db.UserModel
	user1.Email = "test@test.com"

	// 注: 传入 Save 方法的是指针类型(内存地址)
	u1ptr := &user1
	db.Connection.Save(&u1ptr)
	return u1ptr
}

// 更新已经存在的用户信息
func upgradeUser() *db.UserModel {
	// 注: 对应 ID 的数据必须存在, 否则会创建这个数据
	// 在这个例子中, 由于数据迁移的关系, ID=10 的记录是存在的
	var user2 db.UserModel
	user2.ID = 10
	user2.Email = "1010@test.com"
	user2.Password = "101010"

	u2ptr := &user2
	db.Connection.Save(u2ptr)
	return u2ptr
}

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&db.UserModel{})
	// db.SeedDB()

	// u1 := createUser()
	// fmt.Println("u1:", u1)

	u2 := upgradeUser()
	fmt.Println("u2:", u2)
}
go
package main

import (
	"fmt"

	"gorm_demo/db"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&db.UserModel{})
	// db.SeedDB()

	// 1.使用Update方法, 且用结构体设置更新条件, 会触发 beforeUpdate
	user11 := db.UserModel{
		ID: 11,
	}
	result1 := db.Connection.Model(&user11).Update("password", "111111")
	fmt.Println("user11.ID:", user11.ID)
	fmt.Println("user11.Password:", user11.Password)          // 会设置 user11 的 Password 字段
	fmt.Println("result1.Error", result1.Error)               // 更新时错误 nil
	fmt.Println("result1.RowsAffected", result1.RowsAffected) // 更新影响的行数 1

	// 2.使用UpdateColumn方法, 且用结构体设置更新条件, 会触发 beforeUpdate
	user12 := db.UserModel{
		ID: 12,
	}
	result2 := db.Connection.Model(&user12).UpdateColumn("password", "121212")
	fmt.Println("user12.ID:", user12.ID)
	fmt.Println("user12.Password:", user12.Password)          // 会设置 user11 的 Password 字段
	fmt.Println("result2.Error", result2.Error)               // 更新时错误 nil
	fmt.Println("result2.RowsAffected", result2.RowsAffected) // 更新影响的行数 1

	// 3.显示的用 where 方法设置更新条件, 也会触发beforeUpdate
	user13 := db.UserModel{}
	result3 := db.Connection.Model(&user13).Where("id=?", 12).Update("email", "131313@example.com")
	fmt.Println("user13.ID:", user13.ID)
	fmt.Println("user13.Email:", user13.Email)
	fmt.Println("result3.Error", result3.Error)               // 更新时错误 nil
	fmt.Println("result3.RowsAffected", result3.RowsAffected) // 更新影响的行数 1
}
go
package main

import (
	"fmt"

	"gorm_demo/db"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&db.UserModel{})
	// db.SeedDB()

	// 1.使用 Map 更新多个字段
	user13 := db.UserModel{ID: 13}
	result := db.Connection.Model(&user13).Updates(map[string]any{
		"email":    "test13@test13.com",
		"password": "test13test13",
	})
	fmt.Println("user13.ID:", user13.ID)
	fmt.Println("user13.Email:", user13.Email)
	fmt.Println("user13.Password:", user13.Password)
	fmt.Println("result.Error", result.Error)               // nil
	fmt.Println("result.RowsAffected", result.RowsAffected) // 返回受影响的记录的条数 1

	// 2.使用 struct 更新多个字段, 效果是一样的
	user14 := db.UserModel{ID: 14}
	result2 := db.Connection.Model(&user14).Updates(&db.UserModel{
		Email:    "test14@test14.com",
		Password: "test14test14",
	})
	fmt.Println("user14.ID:", user13.ID)
	fmt.Println("user14.Email:", user13.Email)
	fmt.Println("user14.Password:", user13.Password)
	fmt.Println("result.Error", result2.Error)               // nil
	fmt.Println("result.RowsAffected", result2.RowsAffected) // 返回受影响的记录的条数 1
}

删除

  • Delete: 不管是软删除还是硬删除都会触发钩子函数
go
package main

import (
	"fmt"

	"gorm_demo/db"

	"gorm.io/gorm/clause"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&db.UserModel{})
	// db.SeedDB()

	// 1.软删除: 本质是更新 deleted_at 字段
	// Delete 方法会触发 beforeDelete 钩子函数
	result := db.Connection.Delete(&db.UserModel{
		ID: 1, // 删除条件
	})
	fmt.Println("result.Error", result.Error)               // 删除错误 nil
	fmt.Println("result.RowsAffected", result.RowsAffected) // 被删除的函数 1

	// 2.返回被删除行的数据
	deleteItem := db.Connection.Clauses(clause.Returning{}).Delete(&db.UserModel{
		ID: 2,
	})
	fmt.Println("deleteItem", deleteItem)

	// 3.查找软删除的记录
	user1 := db.Connection.Unscoped().Find(&db.UserModel{
		ID: 1,
	})
	fmt.Println("user1", user1)

	// 4.硬删除
	result2 := db.Connection.Unscoped().Delete(&db.UserModel{
		ID: 3,
	})
	fmt.Println("result2.Error", result2.Error)               // 删除错误 nil
	fmt.Println("result2.RowsAffected", result2.RowsAffected) // 被删除的函数 1
}

钩子函数

删除 操作都有对应的钩子函数

  • 每个表都可以定义钩子函数(官方文档上称之为 Hook)
  • 如果 beforeX 系列钩子函数中返回的是 errors.New("xxx") 那么, 操作将会执行失败
go
package db

import (
	"fmt"

	"gorm.io/gorm"
)

type UserModel struct {
	// 请参考文档的字段标签部分:
	// https://gorm.io/zh_CN/docs/models.html#%E5%AD%97%E6%AE%B5%E6%A0%87%E7%AD%BE
	ID         int    `gorm:"primaryKey;autoIncrement;comment:主键"` // 添加主键(自增)
	Email      string `gorm:"unique;comment:邮箱"`                   // 添加唯一索引
	Password   string `gorm:"size:128;comment:密码"`                 // 指定长度为128
	gorm.Model        // 会带有约定的配置(id/created_at/updated_at/delete_at)
}

func (u *UserModel) TableName() string {
	// 自定义表名
	return "users"
}

func (u *UserModel) AfterFind(tx *gorm.DB) (err error) {
	// 执行 Find 方法查询数据时会触发
	fmt.Println("[AfterFind]查询用户数据")
	return nil
}

func (u *UserModel) BeforeCreate(tx *gorm.DB) (err error) {
	// 创建之前执行的钩子函数, 执行 Create/Save 方法会触发
	fmt.Println("[BeforeCreate]创建用户数据")
	return nil
}

func (u *UserModel) AfterCreate(tx *gorm.DB) (err error) {
	// 创建之后执行的钩子函数, 执行 Create/Save 方法会触发
	fmt.Println("[AfterCreate]用户数据创建完成了")
	return nil
}

func (u *UserModel) BeforeSave(tx *gorm.DB) (err error) {
	// 执行 Save 方法会触发
	fmt.Println("[BeforeSave]保存用户数据")
	return nil
}

func (u *UserModel) AfterSave(tx *gorm.DB) (err error) {
	// 执行 Save 方法会触发
	fmt.Println("[AfterCreate]保存数据创建完成了")
	return nil
}

func (u *UserModel) BeforeUpdate(tx *gorm.DB) (err error) {
	// 更新之前执行的钩子函数, 执行 Save/Update 方法会触发
	fmt.Println("[BeforeUpdate]更新用户数据")
	return nil
}

func (u *UserModel) AfterUpdate(tx *gorm.DB) (err error) {
	// 更新之后执行的钩子函数, 执行 Save/Update 方法会触发
	fmt.Println("[AfterUpdate]用户数据创建完成了")
	return nil
}

func (u *UserModel) BeforeDelete(tx *gorm.DB) (err error) {
	// 删除数据之前执行的钩子函数, 执行 Delete 方法会触发
	fmt.Println("[BeforeDelete]删除用户数据")
	return nil
}

func (u *UserModel) AfterDelete(tx *gorm.DB) (err error) {
	// 删除数据之后执行的钩子函数, 执行 Delete 方法会触发
	fmt.Println("[After]用户数据删除完成了")
	return nil
}

查询

这是最常用的功能

基础查询

go
package main

import (
	"fmt"

	"gorm_demo/db"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&db.UserModel{})
	// db.SeedDB()

	//// 1.First查询单个数据(主键升序)
	// select * from users order by id asc limit 1
	user1 := db.UserModel{}
	result := db.Connection.First(&user1)
	fmt.Println("user1.id:", user1.ID)
	fmt.Println("user1.email:", user1.Email)
	fmt.Println("result.error:", result.Error)
	fmt.Println("result.RowsAffected:", result.RowsAffected)

	//// 2.First查询单个数据并指定主键
	// select * from users where id = 10
	fmt.Println("========")
	user10 := db.UserModel{}
	db.Connection.First(&user10, 10)
	fmt.Println("user10.id:", user10.ID)
	fmt.Println("user10.email:", user10.Email)

	//// 3.Last查询单个数据(主键降序)
	// select * from users order by id desc limit 1
	fmt.Println("========")
	user30 := db.UserModel{}
	db.Connection.Last(&user30)
	fmt.Println("user30.id:", user30.ID)
	fmt.Println("user30.email:", user30.Email)

	//// 4.Last查询单个数据并指定主键
	// select * from users where id = 22
	fmt.Println("========")
	user22 := db.UserModel{}
	db.Connection.Last(&user22, 22)
	fmt.Println("user22.id:", user22.ID)
	fmt.Println("user22.email:", user22.Email)

	//// 5.Take 查询单个数据(不排序其实就是 First 别名)
	// select * from users limit 1
	fmt.Println("========")
	firstUser := db.UserModel{}
	db.Connection.Take(&firstUser)
	fmt.Println("firstUser.id:", firstUser.ID)
	fmt.Println("firstUser.email:", firstUser.Email)

	//// 6.Take 查询单个数据并指定主键
	// select * from users where id = 15
	fmt.Println("========")
	user15 := db.UserModel{}
	db.Connection.Take(&user15, 15)
	fmt.Println("user15.id:", user15.ID)
	fmt.Println("user15.email:", user15.Email)
}
go
package main

import (
	"encoding/json"
	"fmt"

	"gorm_demo/db"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&db.UserModel{})
	// db.SeedDB()

	//// 1.Find查询所有字段所有数据(不带条件)
	// select * from users
	// users := []db.UserModel{}
	// db.Connection.Find(&users)
	// userList, err := json.Marshal(users)
	// if err != nil {
	// 	fmt.Println("json.Marshal error:", err)
	// }
	// fmt.Println(string(userList)) // json array

	//// 2.Find查询指定字段所有数据: select id,email from users
	users2 := []db.UserModel{}
	//// db.Connection.Select([]string{"id", "email"}).Find(&users2)
	db.Connection.Select("id", "email").Find(&users2)
	userList, err := json.Marshal(users2)
	if err != nil {
		fmt.Println("json.Marshal error:", err)
	}
	fmt.Println(string(userList)) // json array
	// 两种写法没有作用, 虽然官方文档是这么写的, 但是然并卵
	// https://gorm.io/zh_CN/docs/query.html#%E9%80%89%E6%8B%A9%E7%89%B9%E5%AE%9A%E5%AD%97%E6%AE%B5
}

条件查询

go
package main

import (
	"fmt"

	"gorm_demo/db"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&UserModel{})
	// db.SeedDB()

	// 1.单个条件
	//// 1.0相等 id=1
	user1 := db.UserModel{}
	db.Connection.Where("id = ?", 1).Find(&user1)
	fmt.Println("user1.ID:", user1.ID)
	fmt.Println("user1.Email:", user1.Email)

	//// 1.1不等 id<>1 / not id=1
	var userList []db.UserModel
	db.Connection.Where("id <> ?", 1).Find(&userList)
	fmt.Println("userList:", len(userList)) // 29

	var userList2 []db.UserModel
	db.Connection.Not("id = ?", 1).Find(&userList2)
	fmt.Println("userList2:", len(userList2)) // 29

	//// 1.2范围 id>=10 / id<=10 / id between 1 and 10 / id in 1,2,3
	var userList3 []db.UserModel
	db.Connection.Where("id >= ?", 10).Find(&userList3)
	fmt.Println("userList3:", len(userList3)) // 21

	var userList4 []db.UserModel
	db.Connection.Where("id <= ?", 10).Find(&userList4)
	fmt.Println("userList4:", len(userList4)) // 10

	var userList5 []db.UserModel
	db.Connection.Where("id between ? and ?", 1, 10).Find(&userList5)
	fmt.Println("userList5:", len(userList5)) // 10

	var userList6 []db.UserModel
	db.Connection.Where("id in ?", []int{1, 2, 3}).Find(&userList6)
	fmt.Println("userList6:", len(userList6)) // 3

	//// 1.3模糊匹配 email like '%@test.com'
	var userList7 []db.UserModel
	db.Connection.Where("email like ?", "%@test.com").Find(&userList7)
	fmt.Println("userList7:", len(userList7)) // 30
}
go
package main

import (
	"fmt"

	"gorm_demo/db"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&UserModel{})
	// db.SeedDB()

	// 2.多个条件 and

	// 字符串作为条件
	// 2.1: id=2 and email="test2@test.com"
	var user1 db.UserModel
	db.Connection.Where("id = ? and email = ?", 2, "test2@test.com").First(&user1)
	fmt.Println("user1.id:", user1.ID)
	fmt.Println("user1.email:", user1.Email)

	// map 数据作为条件
	// 2.2: id=3 and email="test3@test.com"
	var user2 db.UserModel
	db.Connection.Where(map[string]string{
		"id":    "3",
		"email": "test3@test.com",
	}).Take(&user2)
	fmt.Println("user2.id:", user2.ID)
	fmt.Println("user2.email:", user2.Email)

	// struct 数据作为条件
	// 2.3: id=4 and email="test4@test.com"
	var user3 db.UserModel
	db.Connection.Where(&db.UserModel{
		ID:    4,
		Email: "test4@test.com",
	}).Last(&user3)
	fmt.Println("user3.id:", user3.ID)
	fmt.Println("user3.email:", user3.Email)
}
go
package main

import (
	"fmt"

	"gorm_demo/db"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&UserModel{})
	// db.SeedDB()

	// 2.多个条件 or

	// 使用字符串形式拼接sql参数
	// 2.1: id=1 or email="test2@test.com"
	var userList1 []db.UserModel
	db.Connection.Where("id = ? or email = ?", 1, "test2@test.com").Find(&userList1)
	user1 := userList1[0]
	fmt.Println("user1.id:", user1.ID)
	fmt.Println("user1.email:", user1.Email)

	user2 := userList1[1]
	fmt.Println("user2.id:", user2.ID)
	fmt.Println("user2.email:", user2.Email)

	// 使用 Or 方法拼接sql参数
	// 2.2 id=2 or email="test3@test.com"
	var userList2 []db.UserModel
	db.Connection.Where("id = ?", 1).Or("email = ?", "test3@test.com").Find(&userList2)
	user3 := userList2[0]
	fmt.Println("user3.id:", user3.ID)
	fmt.Println("user3.email:", user3.Email)

	user4 := userList2[1]
	fmt.Println("user4.id:", user4.ID)
	fmt.Println("user4.email:", user4.Email)
}

分页查询

go
package main

import (
	"fmt"

	"gorm_demo/db"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&UserModel{})
	// db.SeedDB()

	page := 2   // 第2页
	limit := 10 // 每页10条数据

	var count int64
	var items []db.UserModel

	// 1.统计数据条数
	db.Connection.Model(&db.UserModel{}).Where("id >= ?", 5).Count(&count)

	// 2.偏移和限制条数计算
	db.Connection.Where("id >= ?", 5).Offset((page - 1) * limit).Limit(limit).Find(&items)

	fmt.Println("count:", count)      // 10
	fmt.Println("items:", len(items)) // 26
}

作用域

go
package main

import (
	"gorm_demo/db"

	"gorm.io/gorm"
)

func main() {
	db.ConnectDB()
	// db.Connection.AutoMigrate(&UserModel{})
	// db.SeedDB()

  // 2.使用作用域: 其实就是更方便的添加 where 条件
	// select * from users where is_active = 1 and id >= 5
	var users1 []db.UserModel
	db.Connection.Scopes(IsActived).Where("id >= ", 5).Find(&users1)

	// select * from users where is_active = 1 and email in (test1@test.com, test2@test.com) and id >= 5
	var users2 []db.UserModel
	emails := []string{"test1@test.com", "test2@test.com"}
	db.Connection.Scopes(IsActived, EmailIn(emails)).Where("id >= ", 5).Find(&users2)
}

// 1.定义作用域: 其实就是定义复用的 where 条件
// 用户账户必须是激活状态(防止机器人注册)
func IsActived(db *gorm.DB) *gorm.DB {
	return db.Where("is_active = ?", 1)
}

// 用于按状态筛选订单的 Scope
func EmailIn(status []string) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		return db.Where("email IN (?)", status)
	}
}


// PageInfo 分页信息结构体
type PageInfo struct {
	Page     int   // 页码,从1开始
	PageSize int   // 每页数量
	Total    int64 // 总记录数
	TotalPage int  // 总页数
}

// Paginate Scope函数 - 用于分页查询
// 使用示例: db.Scopes(Paginate(&pageInfo)).Find(&users)
func Paginate(pageInfo *PageInfo) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		// 参数校验
		if pageInfo.Page <= 0 {
			pageInfo.Page = 1
		}
		if pageInfo.PageSize <= 0 {
			pageInfo.PageSize = 10
		}
		if pageInfo.PageSize > 100 { // 防止过大查询
			pageInfo.PageSize = 100
		}

		// 计算偏移量应用分页
		offset := (pageInfo.Page - 1) * pageInfo.PageSize
		return db.Offset(offset).Limit(pageInfo.PageSize)
	}
}

// Count 统计总数的Scope
// 使用示例: db.Scopes(Count(&pageInfo)).Find(&users)
func Count(pageInfo *PageInfo, model any) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		// 统计总数
		db.Model(model).Count(&pageInfo.Total)

		// 计算总页数
		if pageInfo.PageSize > 0 {
			pageInfo.TotalPage = int((pageInfo.Total + int64(pageInfo.PageSize) - 1) / int64(pageInfo.PageSize))
		}

		return db
	}
}

// OrderBy 排序 Scope
func OrderBy(column string, desc bool) func(db *gorm.DB) *gorm.DB {
	return func(db *gorm.DB) *gorm.DB {
		if desc {
			return db.Order(column + " DESC")
		}
		return db.Order(column + " ASC")
	}
}

高级查询

多表关联

  • 一对一
  • 一对多
  • 多对多

一对一

一个用户只有一个身份证信息

go
// 为了方便做笔记, 将两个表放到一个文件中方便观察
package db

import "gorm.io/gorm"

// 用户表
type User struct {
	gorm.Model
	Email      string      `gorm:"unique;not null"`    // 用户邮箱
	Password   string      `gorm:"not null"`           // 用户密码
	Nickname   string      `gorm:"not null"`           // 用户昵称
	Avatar     string      `gorm:"not null"`           // 用户头像
	UserIDCard *UserIDCard `gorm:"foreignKey:UserID;"` // 用户身份证信息,foreignKey:设置实体外键
}

// 用户身份证信息表
type UserIDCard struct {
	gorm.Model
	UserID      uint   `gorm:"unique;not null"`   // 用户ID(外键字段)
	CardID      string `gorm:"not null"`          // 身份证号
	CardName    string `gorm:"not null"`          // 身份证姓名
	CardAddress string `gorm:"not null"`          // 身份证详细地址
	CardPic1    string `gorm:"not null"`          // 身份证(正面)图片
	CardPic2    string `gorm:"not null"`          // 身份证(反面)图片
	User        *User  `gorm:"foreignKey:UserID"` // 身份证信息所属用户(反向引用)
}
go
package main

import (
	"gorm_demo/db"
)

func main() {
	// db.Migrate()

	// 同时创建用户表和用户身份证信息表
	db.ConnectDB().Create(&db.User{
		Email:    "test@example.com",
		Password: "123456",
		Nickname: "张三疯",
		Avatar:   "https://avatars.githubusercontent.com/u/29266093",
		UserIDCard: &db.UserIDCard{
			CardID:      "43042120000112417X",
			CardName:    "张三",
			CardAddress: "湖南省衡阳县西渡镇解放路幸福小区25号楼1803室",
			CardPic1:    "https://avatars.githubusercontent.com/u/29266093",
			CardPic2:    "https://avatars.githubusercontent.com/u/29266093",
		},
	})
}
go
package main

import (
	"encoding/json"
	"fmt"

	"gorm_demo/db"
)

func main() {
	// db.Migrate()

	// 正向查询: 通过用户查询用户身份证信息
	var userWithCard db.User
	db.ConnectDB().Model(&db.User{}).Preload("UserIDCard").Take(&userWithCard, 1)
	bytes, err := json.Marshal(userWithCard)
	if err != nil {
		fmt.Println("JSON 序列化错误:", err)
		return
	}
	fmt.Println("用户信息:", string(bytes))
	// 用户信息:
	//	{
	//	  "ID": 1,
	//	  "CreatedAt": "2026-05-14T11:20:42.401218+08:00",
	//	  "UpdatedAt": "2026-05-14T11:20:42.401218+08:00",
	//	  "DeletedAt": null,
	//	  "Email": "test@example.com",
	//	  "Password": "123456",
	//	  "Nickname": "张三疯",
	//	  "Avatar": "https://avatars.githubusercontent.com/u/29266093",
	//	  "UserIDCard": {
	//	    "ID": 1,
	//	    "CreatedAt": "2026-05-14T11:20:42.402095+08:00",
	//	    "UpdatedAt": "2026-05-14T11:20:42.402095+08:00",
	//	    "DeletedAt": null,
	//	    "User": null,
	//	    "UserID": 1,
	//	    "CardID": "43042120000112417X",
	//	    "CardName": "张三",
	//	    "CardAddress": "湖南省衡阳县西渡镇解放路幸福小区25号楼1803室",
	//	    "CardPic1": "https://avatars.githubusercontent.com/u/29266093",
	//	    "CardPic2": "https://avatars.githubusercontent.com/u/29266093"
	//	  }
	//	}

	// 反向查询:通过身份证信息查询用户信息
	var cardWithUser db.UserIDCard
	db.ConnectDB().Model(&db.UserIDCard{}).Preload("User").Take(&cardWithUser, 1)
	bytes, err = json.Marshal(cardWithUser)
	if err != nil {
		fmt.Println("JSON 序列化错误:", err)
		return
	}
	fmt.Println("身份证信息:", string(bytes))
	// 身份证信息:
	//	{
	//	  "ID": 1,
	//	  "CreatedAt": "2026-05-14T11:20:42.402095+08:00",
	//	  "UpdatedAt": "2026-05-14T11:20:42.402095+08:00",
	//	  "DeletedAt": null,
	//	  "User": {
	//	    "ID": 1,
	//	    "CreatedAt": "2026-05-14T11:20:42.401218+08:00",
	//	    "UpdatedAt": "2026-05-14T11:20:42.401218+08:00",
	//	    "DeletedAt": null,
	//	    "Email": "test@example.com",
	//	    "Password": "123456",
	//	    "Nickname": "张三疯",
	//	    "Avatar": "https://avatars.githubusercontent.com/u/29266093",
	//	    "UserIDCard": null
	//	  },
	//	  "UserID": 1,
	//	  "CardID": "43042120000112417X",
	//	  "CardName": "张三",
	//	  "CardAddress": "湖南省衡阳县西渡镇解放路幸福小区25号楼1803室",
	//	  "CardPic1": "https://avatars.githubusercontent.com/u/29266093",
	//	  "CardPic2": "https://avatars.githubusercontent.com/u/29266093"
	//	}
}

[注意]

关于多表写入(创建/删除/修改), 应该用手动控制事物的方式来操作比较合理, 确保数据的正确性

一对多

  • 一个用户可以有多个社交账号
  • 一个用户可以有多个收货地址
go
package db

import "gorm.io/gorm"

// 用户表
type User struct {
	gorm.Model
	Email          string              `gorm:"unique;not null"`    // 用户邮箱
	Password       string              `gorm:"not null"`           // 用户密码
	Nickname       string              `gorm:"not null"`           // 用户昵称
	Avatar         string              `gorm:"not null"`           // 用户头像
	SocialAccounts []UserSocialAccount `gorm:"foreignKey:UserID;"` // 用户社交平台账号
}

// 用户社交账号信息表
type UserSocialAccount struct {
	gorm.Model
	UserID   uint   `gorm:"not null"`           // 用户ID
	Platform string `gorm:"not null"`           // 社交平台(qq/wx/dy/bilibili)
	Account  string `gorm:"not null"`           // 社交账号
	Nickname string `gorm:"not null"`           // 社交账号昵称
	Avatar   string `gorm:"not null"`           // 社交账号头像
	Moments  uint   `gorm:"deafult:0"`          // 社交账号动态数
	Follows  uint   `gorm:"deafult:0"`          // 关注数
	Fans     uint   `gorm:"deafult:0"`          // 粉丝数
	User     *User  `gorm:"foreignKey:UserID;"` // 社交账号所属用户信息(如果不需要反向查找可以移除关系定义)
}
go
package main

import "gorm_demo/db"

func main() {
	// db.Migrate()

  // 插入一个用户数据的同时插入多个社交账户信息数据
	db.ConnectDB().Model(&db.User{}).Create(&db.User{
		Email:    "test@example.com",
		Password: "123456",
		Nickname: "张三疯",
		Avatar:   "https://avatars.githubusercontent.com/u/29266093",
		SocialAccounts: []db.UserSocialAccount{
			{
				Platform: "qq",
				Account:  "qq123456789",
				Nickname: "张三QQ",
				Avatar:   "https://avatars.githubusercontent.com/u/29266093",
			},
			{
				Platform: "wx",
				Account:  "wx123456789",
				Nickname: "张三微信",
				Avatar:   "https://avatars.githubusercontent.com/u/29266093",
			},
		},
	})
}
go
package main

import (
	"encoding/json"
	"fmt"

	"gorm_demo/db"
)

func main() {
	// db.Migrate()

	//// 1.正向查找: 通过 User 查找用户社交账号信息
	// 注: Preload 中的参数是模型中定义的字段, 而不是目标模型的名称
	var userWithSocial db.User
	db.ConnectDB().Model(&db.User{}).Preload("SocialAccounts").First(&userWithSocial, "Nickname = ?", "张三疯")
	jsonBytes, err := json.Marshal(userWithSocial)
	if err != nil {
		fmt.Println("JSON 序列化错误:", err)
		return
	}
	fmt.Println("用户信息:", string(jsonBytes))
	// 用户信息:
	// {
	//   "ID": 1,
	//   "CreatedAt": "2026-05-15T11:49:12.699197+08:00",
	//   "UpdatedAt": "2026-05-15T11:49:12.699197+08:00",
	//   "DeletedAt": null,
	//   "Email": "test@example.com",
	//   "Password": "123456",
	//   "Nickname": "张三疯",
	//   "Avatar": "https://avatars.githubusercontent.com/u/29266093",
	//   "SocialAccounts": [
	//     {
	//       "ID": 1,
	//       "CreatedAt": "2026-05-15T11:49:12.699643+08:00",
	//       "UpdatedAt": "2026-05-15T11:49:12.699643+08:00",
	//       "DeletedAt": null,
	//       "UserID": 1,
	//       "Platform": "qq",
	//       "Account": "qq123456789",
	//       "Nickname": "张三QQ",
	//       "Avatar": "https://avatars.githubusercontent.com/u/29266093",
	//       "Moments": 0,
	//       "Follows": 0,
	//       "Fans": 0,
	//       "User": null
	//     },
	//     {
	//       "ID": 2,
	//       "CreatedAt": "2026-05-15T11:49:12.699643+08:00",
	//       "UpdatedAt": "2026-05-15T11:49:12.699643+08:00",
	//       "DeletedAt": null,
	//       "UserID": 1,
	//       "Platform": "wx",
	//       "Account": "wx123456789",
	//       "Nickname": "张三微信",
	//       "Avatar": "https://avatars.githubusercontent.com/u/29266093",
	//       "Moments": 0,
	//       "Follows": 0,
	//       "Fans": 0,
	//       "User": null
	//     }
	//   ]
	// }

	//// 2.反向查找: 通过社交账户信息查询所属 User 信息
	var socialWithUser db.UserSocialAccount
	db.ConnectDB().Model(&db.UserSocialAccount{}).Preload("User").First(&socialWithUser, "Account = ?", "qq123456789")
	bytes, err := json.Marshal(socialWithUser)
	if err != nil {
		fmt.Println("JSON 序列化错误:", err)
		return
	}
	fmt.Println("社交账号信息:", string(bytes))
	// 社交账号信息:
	// {
	//   "ID": 1,
	//   "CreatedAt": "2026-05-15T11:49:12.699643+08:00",
	//   "UpdatedAt": "2026-05-15T11:49:12.699643+08:00",
	//   "DeletedAt": null,
	//   "UserID": 1,
	//   "Platform": "qq",
	//   "Account": "qq123456789",
	//   "Nickname": "张三QQ",
	//   "Avatar": "https://avatars.githubusercontent.com/u/29266093",
	//   "Moments": 0,
	//   "Follows": 0,
	//   "Fans": 0,
	//   "User": {
	//     "ID": 1,
	//     "CreatedAt": "2026-05-15T11:49:12.699197+08:00",
	//     "UpdatedAt": "2026-05-15T11:49:12.699197+08:00",
	//     "DeletedAt": null,
	//     "Email": "test@example.com",
	//     "Password": "123456",
	//     "Nickname": "张三疯",
	//     "Avatar": "https://avatars.githubusercontent.com/u/29266093",
	//     "SocialAccounts": null
	//   }
	// }
}

多对多

  • 一个老师可以有多个学生,一个学生也可以有多个老师
go
package db

import "gorm.io/gorm"

// 学生表
type Student struct {
	gorm.Model
	Name     string    `gorm:"not null"`                          // 姓名
	Gender   string    `gorm:"not null"`                          // 性别
	Subject  string    `gorm:"not null"`                          // 专业
	Class    string    `gorm:"not null"`                          // 班级
	Teachers []Teacher `gorm:"many2many:student_teacher_relates"` // 多对多:中间关系表
}

// 学生&老师关系表(老师&学生多对多中间表)
type StudentTeacherRelate struct {
	StudentID uint `gorm:"not null"` // 学生ID
	TeacherID uint `gorm:"not null"` // 教师ID
}

// 教师表
type Teacher struct {
	gorm.Model
	Name     string    `gorm:"not null"`                          // 姓名
	Gender   string    `gorm:"not null"`                          // 性别
	Course   string    `gorm:"not null"`                          // 教的课程
	Students []Student `gorm:"many2many:student_teacher_relates"` // 多对多:中间关系表
}
go
package main

import "gorm_demo/db"

func main() {
	// db.Migrate()

	// 学生:
	// 张三 -> 计算机科学与技术
	// 李四 -> 数学与应用数学
	// 王五 -> 中国语言文学
	// 赵六 -> 数字媒体艺术
	// 钱七 -> 政治与行政学
	// 孙八 -> 哲学
	students := []db.Student{
		{
			Name:    "张三",
			Gender:  "男",
			Subject: "计算机科学与技术",
			Class:   "1班",
		},
		{
			Name:    "李四",
			Gender:  "女",
			Subject: "数学与应用数学",
			Class:   "2班",
		},
		{
			Name:    "王五",
			Gender:  "男",
			Subject: "中国语言文学",
			Class:   "3班",
		},
		{
			Name:    "赵六",
			Gender:  "男",
			Subject: "数字媒体艺术",
			Class:   "4班",
		},
		{
			Name:    "钱七",
			Gender:  "女",
			Subject: "政治与行政学",
			Class:   "5班",
		},
		{
			Name:    "孙八",
			Gender:  "男",
			Subject: "哲学",
			Class:   "6班",
		},
	}

	// 创建学生数据
	db.ConnectDB().Model(&db.Student{}).CreateInBatches(&students, len(students))
	zs := students[0]
	ls := students[1]
	ww := students[2]
	zl := students[3]
	qq := students[4]
	sb := students[5]

	// 老师:
	// 司马老师 -> 数学
	// 宇文老师 -> 语文
	// 欧阳老师 -> 音乐
	// 慕容老师 -> 地理
	// 独孤老师 -> 化学
	// 令狐老师 -> 生物
	teachers := []db.Teacher{
		{
			Name:     "司马老师",
			Gender:   "男",
			Course:   "数学",
			Students: []db.Student{zs, ls, ww, zl, qq, sb},
		},
		{
			Name:     "宇文老师",
			Gender:   "女",
			Course:   "语文",
			Students: []db.Student{ww, zl},
		},
		{
			Name:     "欧阳老师",
			Gender:   "女",
			Course:   "音乐",
			Students: []db.Student{qq, sb},
		},
		{
			Name:     "慕容老师",
			Gender:   "男",
			Course:   "地理",
			Students: []db.Student{zl, sb},
		},
		{
			Name:     "独孤老师",
			Gender:   "男",
			Course:   "地理",
			Students: []db.Student{zs, ls},
		},
		{
			Name:     "令狐",
			Gender:   "男",
			Course:   "生物",
			Students: []db.Student{ls, qq},
		},
	}

	// 创建老师数据
	db.ConnectDB().Model(&db.Teacher{}).CreateInBatches(&teachers, len(teachers))
}
go
package main

import (
	"encoding/json"
	"fmt"

	"gorm_demo/db"
)

func main() {
	// db.Migrate()

	// 查询出所有的老师及其所教的学生
	var teachersWithStudents []db.Teacher
	db.ConnectDB().Model(&db.Teacher{}).Preload("Students").Find(&teachersWithStudents)
	bytes, err := json.Marshal(teachersWithStudents)
	if err != nil {
		fmt.Println("数据序列化错误", err)
	}
	fmt.Println(string(bytes))

	// 查询的数据与插入数据完全一致, 说明没有问题, 反之, 查询所有学生及其老师也是一样:
	// [
	//
	//	{
	//	  "ID": 1,
	//	  "CreatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "UpdatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "DeletedAt": null,
	//	  "Name": "司马老师",
	//	  "Gender": "男",
	//	  "Course": "数学",
	//	  "Students": [
	//	    {
	//	      "ID": 1,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "张三",
	//	      "Gender": "男",
	//	      "Subject": "计算机科学与技术",
	//	      "Class": "1班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 2,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "李四",
	//	      "Gender": "女",
	//	      "Subject": "数学与应用数学",
	//	      "Class": "2班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 3,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "王五",
	//	      "Gender": "男",
	//	      "Subject": "中国语言文学",
	//	      "Class": "3班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 4,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "赵六",
	//	      "Gender": "男",
	//	      "Subject": "数字媒体艺术",
	//	      "Class": "4班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 5,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "钱七",
	//	      "Gender": "女",
	//	      "Subject": "政治与行政学",
	//	      "Class": "5班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 6,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "孙八",
	//	      "Gender": "男",
	//	      "Subject": "哲学",
	//	      "Class": "6班",
	//	      "Teachers": null
	//	    }
	//	  ]
	//	},
	//	{
	//	  "ID": 2,
	//	  "CreatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "UpdatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "DeletedAt": null,
	//	  "Name": "宇文老师",
	//	  "Gender": "女",
	//	  "Course": "语文",
	//	  "Students": [
	//	    {
	//	      "ID": 3,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "王五",
	//	      "Gender": "男",
	//	      "Subject": "中国语言文学",
	//	      "Class": "3班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 4,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "赵六",
	//	      "Gender": "男",
	//	      "Subject": "数字媒体艺术",
	//	      "Class": "4班",
	//	      "Teachers": null
	//	    }
	//	  ]
	//	},
	//	{
	//	  "ID": 3,
	//	  "CreatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "UpdatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "DeletedAt": null,
	//	  "Name": "欧阳老师",
	//	  "Gender": "女",
	//	  "Course": "音乐",
	//	  "Students": [
	//	    {
	//	      "ID": 5,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "钱七",
	//	      "Gender": "女",
	//	      "Subject": "政治与行政学",
	//	      "Class": "5班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 6,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "孙八",
	//	      "Gender": "男",
	//	      "Subject": "哲学",
	//	      "Class": "6班",
	//	      "Teachers": null
	//	    }
	//	  ]
	//	},
	//	{
	//	  "ID": 4,
	//	  "CreatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "UpdatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "DeletedAt": null,
	//	  "Name": "慕容老师",
	//	  "Gender": "男",
	//	  "Course": "地理",
	//	  "Students": [
	//	    {
	//	      "ID": 4,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "赵六",
	//	      "Gender": "男",
	//	      "Subject": "数字媒体艺术",
	//	      "Class": "4班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 6,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "孙八",
	//	      "Gender": "男",
	//	      "Subject": "哲学",
	//	      "Class": "6班",
	//	      "Teachers": null
	//	    }
	//	  ]
	//	},
	//	{
	//	  "ID": 5,
	//	  "CreatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "UpdatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "DeletedAt": null,
	//	  "Name": "独孤老师",
	//	  "Gender": "男",
	//	  "Course": "地理",
	//	  "Students": [
	//	    {
	//	      "ID": 1,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "张三",
	//	      "Gender": "男",
	//	      "Subject": "计算机科学与技术",
	//	      "Class": "1班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 2,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "李四",
	//	      "Gender": "女",
	//	      "Subject": "数学与应用数学",
	//	      "Class": "2班",
	//	      "Teachers": null
	//	    }
	//	  ]
	//	},
	//	{
	//	  "ID": 6,
	//	  "CreatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "UpdatedAt": "2026-05-15T12:25:30.466389+08:00",
	//	  "DeletedAt": null,
	//	  "Name": "令狐",
	//	  "Gender": "男",
	//	  "Course": "生物",
	//	  "Students": [
	//	    {
	//	      "ID": 2,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "李四",
	//	      "Gender": "女",
	//	      "Subject": "数学与应用数学",
	//	      "Class": "2班",
	//	      "Teachers": null
	//	    },
	//	    {
	//	      "ID": 5,
	//	      "CreatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "UpdatedAt": "2026-05-15T12:25:30.465538+08:00",
	//	      "DeletedAt": null,
	//	      "Name": "钱七",
	//	      "Gender": "女",
	//	      "Subject": "政治与行政学",
	//	      "Class": "5班",
	//	      "Teachers": null
	//	    }
	//	  ]
	//	}
	// ]
}

禁止实体外键

  • 仅使用逻辑外键

这是链接数据库时的一个选项 DisableForeignKeyConstraintWhenMigrating

go
package db

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

// 数据库链接配置
var DSN = "./test.sqlite"

// 数据库链接对象
var Connection *gorm.DB = nil

// ConnectDB 链接数据库
func ConnectDB() *gorm.DB {
	if Connection != nil {
		return Connection
	}

	db, err := gorm.Open(sqlite.Open(DSN), &gorm.Config{
		DisableForeignKeyConstraintWhenMigrating: true, // 禁止使用实体外键
	})
	if err != nil {
		panic("连接数据库失败")
	}

	Connection = db
	return Connection
}

// Migrate 迁移数据库表
func Migrate() {
	ConnectDB().AutoMigrate(&User{}, &UserIDCard{})
}

自定义数据类型

要存储一些不确定数据大小/个数/字段的数据,无法很好的建立表格,此时就应该动态存储一个JSON字符串,在读取的时候自动序列化

  • 比如: 用户爱好(每个人爱好数量不一样, 不可能使用固定的列来存储)
go
package db

import (
	"database/sql/driver"
	"encoding/json"
	"errors"

	"gorm.io/gorm"
)

type (
	AnyJSON      json.RawMessage // 任意结构的 json 字符串(如果不需要强类型提示/或者不确定结构用这个即可)
	Hobbies      []string        // 兴趣爱好
	PrivateInfos struct {
		RealName string `json:"realName"` // 用户真实姓名
		Address  string `json:"address"`  // 用户地址
	}
)

// User 用户表模型
type User struct {
	gorm.Model
	Email        string       `gorm:"unique;not null"` // 用户邮箱
	Password     string       `gorm:"not null"`        // 用户密码
	Nickname     string       `gorm:"not null"`        // 用户昵称
	Avatar       string       `gorm:"not null"`        // 用户头像
	Hobbies      Hobbies      `json:"hobbies"`         // 用户爱好(确定数据结构: json 字符串数组)
	PrivateInfos PrivateInfos `json:"privateInfos"`    // 用户私密信息(确定结构: json对象)
	ExtraData    AnyJSON      `json:"extraData"`       // 额外需要存储的数据(不确定结构: 任意 JSON 格式的字符串)
}

// 像这种实现了 Scan 方法的, 数据库存储类型是 `Blob` 这个是字符二进制序列
// 可以理解为Go的[]byte类型, 如果要直接存明文字符串需设置 `gorm:"type:text"`
// Scan 方法实现: 将 struct 转换为 jsonb(二进制 json 字符串数据)
func (hs *Hobbies) Scan(value any) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New("failed to Unmarshal JSONB field(hobbies) data")
	}

	res := Hobbies{}
	err := json.Unmarshal(bytes, &res)
	*hs = res
	return err
}

// Value 方法实现 driver.Valuer 接口, 将 jsonb(二进制 json 字符串数据)转为 struct
func (hs Hobbies) Value() (driver.Value, error) {
	return json.Marshal(hs)
}

// Scan 同理: 如果有多个字段需要转换(则需要给每个字段都实现一次, 否则 migrate 会报错)
func (pi *PrivateInfos) Scan(value any) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New("failed to Unmarshal JSONB field(PrivateInfos) data")
	}
	res := PrivateInfos{}
	err := json.Unmarshal(bytes, &res)
	*pi = res
	return err
}

// Value 同理
func (pi PrivateInfos) Value() (driver.Value, error) {
	return json.Marshal(pi)
}

// MarshalJSON 直接返回原始 JSON 字节, 避免 base64 编码
func (aj AnyJSON) MarshalJSON() ([]byte, error) {
	if len(aj) == 0 {
		return []byte("null"), nil
	}
	// 如果已经是合法 JSON,直接返回;否则加上引号作为字符串
	if json.Valid(aj) {
		return aj, nil
	}
	return json.Marshal(string(aj))
}

// UnmarshalJSON 原始字节直接赋值
func (aj *AnyJSON) UnmarshalJSON(data []byte) error {
	*aj = AnyJSON(data)
	return nil
}

// Scan 任意结构的 json 字符串
func (aj *AnyJSON) Scan(value any) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New("failed to Unmarshal JSONB field(AnyJSON) data")
	}
	*aj = AnyJSON(bytes)
	return nil
}

// Value 同理 存入数据库时仍需要 Marshal
func (aj AnyJSON) Value() (driver.Value, error) {
	if len(aj) == 0 {
		return nil, nil
	}
	return json.Marshal(aj)
}
go
package main

import (
	"gorm_demo/db"
)

func main() {
	// db.Migrate()

	// 插入数据2条数据
	db.ConnectDB().Create(&db.User{
		Email:    "test@example.com",
		Password: "123456",
		Nickname: "张三疯",
		Avatar:   "https://avatars.githubusercontent.com/u/29266093",
		Hobbies:  db.Hobbies{"唱", "跳", "Rap", "篮球"},
		PrivateInfos: db.PrivateInfos{
			RealName: "张三", // 确定结构的字段, 就必须按照结构填写
			Address:  "北京市海淀区解放路幸福小区5号楼2单元2306",
		},
		// 不确定结构的, 可以填写任意json字符串, 但是没有强类型提示
		ExtraData: db.AnyJSON(`{"tags":["tag3", "test", "example"]}`),
	})
	db.ConnectDB().Create(&db.User{
		Email:    "test222@example.com",
		Password: "123456",
		Nickname: "张三疯",
		Avatar:   "https://avatars.githubusercontent.com/u/29266093",
		Hobbies:  db.Hobbies{"唱", "跳"},
		PrivateInfos: db.PrivateInfos{
			RealName: "李四",
			Address:  "北京市海淀区解放路幸福小区5号楼2单元2307",
		},
		ExtraData: db.AnyJSON(`{"a":"1","b":2}`),
	})
}
go
package main

import (
	"encoding/json"
	"fmt"

	"gorm_demo/db"
)

func main() {
	// db.Migrate()

	// 查询
	var users []db.User
	db.ConnectDB().Find(&users)
	bytes, err := json.Marshal(users)
	if err != nil {
		fmt.Println("数据序列化失败", err)
	}
	fmt.Println(string(bytes))

	// [
	//
	//	{
	//	  "ID": 1,
	//	  "CreatedAt": "2026-05-15T14:15:40.335404+08:00",
	//	  "UpdatedAt": "2026-05-15T14:15:40.335404+08:00",
	//	  "DeletedAt": null,
	//	  "Email": "test@example.com",
	//	  "Password": "123456",
	//	  "Nickname": "张三疯",
	//	  "Avatar": "https://avatars.githubusercontent.com/u/29266093",
	//	  "hobbies": [
	//	    "唱",
	//	    "跳",
	//	    "Rap",
	//	    "篮球"
	//	  ],
	//	  "privateInfos": {
	//	    "realName": "张三",
	//	    "address": "北京市海淀区解放路幸福小区5号楼2单元2306"
	//	  },
	//	  "extraData": {
	//	    "tags": [
	//	      "tag3",
	//	      "test",
	//	      "example"
	//	    ]
	//	  }
	//	},
	//	{
	//	  "ID": 2,
	//	  "CreatedAt": "2026-05-15T14:15:40.336556+08:00",
	//	  "UpdatedAt": "2026-05-15T14:15:40.336556+08:00",
	//	  "DeletedAt": null,
	//	  "Email": "test222@example.com",
	//	  "Password": "123456",
	//	  "Nickname": "张三疯",
	//	  "Avatar": "https://avatars.githubusercontent.com/u/29266093",
	//	  "hobbies": [
	//	    "唱",
	//	    "跳"
	//	  ],
	//	  "privateInfos": {
	//	    "realName": "李四",
	//	    "address": "北京市海淀区解放路幸福小区5号楼2单元2307"
	//	  },
	//	  "extraData": {
	//	    "a": "1",
	//	    "b": 2
	//	  }
	//	}
	//
	// ]
}

自定义序列化器

其实前面为了存个JSON格式的字符串费了那么大精力, 其实完全可以使用内置的 序列化器 就可以完美解决问题

[自定义序列化器有什么用呢?]

有些敏感私密数据, 我们并不希望明文存储到数据库, 比如用户的密码/银行卡号/收货地址/电话/邮箱等

可以序列化之后在存储到数据库, 当然可以在插入数据的时候手动处理, 也可以直接使用序列化器自动处理

达到 抽离和复用序列化明文字段值 这一部分代码的目的

但是通过观察可以发现, 以上字段虽然都需要密文存储但又各有不同, 比如密码, 只需要 加密存储&对比密文和原文

即可但是电话/邮箱等只是为了用户的隐私但是在程序执行过程中可能需要反序列化获得原文来进行查找筛选等操作

go
package db

import (
	"context"
	"encoding/base64"
	"fmt"
	"reflect"

	"gorm.io/gorm/schema"
)

// 1.使用简单的 Base64 模拟 AES/DES 等加密算法

type EncryptSerializer struct{}

func (EncryptSerializer) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) (err error) {
	if dbValue == nil {
		return nil
	}

	// 判断数据库字段的类型
	var base64str string
	switch v := dbValue.(type) {
	case []byte:
		base64str = string(v)
	case string:
		base64str = v
	default:
		return fmt.Errorf("failed to decode base64: unsupported db value type %T", dbValue)
	}

	// 尝试 Base64 解码
	decoded, err := base64.StdEncoding.DecodeString(base64str)
	if err != nil {
		return fmt.Errorf("failed to decode base64: %w", err)
	}

	// 根据字段类型设置解码后的值
	fieldType := field.FieldType
	switch fieldType.Kind() {
	case reflect.String: // string 类型
		field.ReflectValueOf(ctx, dst).SetString(string(decoded))
	case reflect.Slice: // []byte 类型
		if fieldType.Elem().Kind() == reflect.Uint8 {
			field.ReflectValueOf(ctx, dst).SetBytes(decoded)
		} else {
			return fmt.Errorf("unsupported slice element type: %v", fieldType.Elem().Kind())
		}
	default:
		return fmt.Errorf("unsupported field type for EncryptSerializer: %v", fieldType.Kind())
	}

	return nil
}

func (EncryptSerializer) Value(ctx context.Context, field *schema.Field, dst reflect.Value, fieldValue interface{}) (interface{}, error) {
	if fieldValue == nil {
		return nil, nil
	}

	var bytes []byte
	switch v := fieldValue.(type) {
	case string:
		bytes = []byte(v) // 强制类型转换
	case []byte:
		bytes = v
	default:
		return nil, fmt.Errorf("failed to encode base64: unsupported field value type %T", fieldValue)
	}

	if len(bytes) == 0 {
		return nil, nil
	}

	// base64 编码
	encoded := base64.StdEncoding.EncodeToString(bytes)
	return encoded, nil
}
go
package db

import (
	"gorm.io/gorm/schema"
)

func init() {
	// 2.在定义模型(使用序列器)之前注册序列器
	schema.RegisterSerializer("encrypt", EncryptSerializer{})
}
go
package db

import (
	"gorm.io/gorm"
)

// User 用户表模型
type User struct {
	gorm.Model
	Email     string         `gorm:"unique;not null"`             // 用户邮箱
	Password  string         `gorm:"not null"`                    // 用户密码
	Nickname  string         `gorm:"not null"`                    // 用户昵称
	Telephone string         `gorm:"serializer:encrypt;not null"` // 用户电话(使用自定义序列化器实现密文存储手机号码)
	ExtraData map[string]any `gorm:"serializer:json"`             // 额外数据(使用默认的序列化器来存储 JSON 数据)
}
go
package main

import (
	"gorm_demo/db"
)

func main() {
	// db.Migrate()

	db.ConnectDB().Create(&db.User{
		Email:     "test@example.com",
		Password:  "123456",
		Nickname:  "张三",
		Telephone: "18888888888",
		ExtraData: map[string]any{
			"a": 1,
			"b": "234",
		},
	})
}
go
package main

import (
	"encoding/json"
	"fmt"

	"gorm_demo/db"
)

func main() {
	// db.Migrate()

	var user db.User
	db.ConnectDB().Find(&user)
	bytes, _ := json.Marshal(user)

	fmt.Println(string(bytes))
	//	{
	//	  "ID": 1,
	//	  "CreatedAt": "2026-05-15T15:46:24.956559+08:00",
	//	  "UpdatedAt": "2026-05-15T15:46:24.956559+08:00",
	//	  "DeletedAt": null,
	//	  "Email": "test@example.com",
	//	  "Password": "123456",
	//	  "Nickname": "张三",
	//	  "Telephone": "18888888888",
	//	  "ExtraData": {
	//	    "a": 1,
	//	    "b": "234"
	//	  }
	//	}
}
sh
$ sqlite3 test.sqlite
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .tables
users
sqlite> select id,email,nickname,telephone,extra_data from users;
1|test@example.com|张三|MTg4ODg4ODg4ODg=|{"a":1,"b":"234"}

# 原始数据中 telephone 字段的数据被 Base64 了
# 使用 MTg4ODg4ODg4ODg= 解密可得: 18888888888
# 而 json 数据也正确存储了

事物操作

基础概念回顾

以转账为例: 张三给李四转 100 块

  1. 张三的账户余额减100
  2. 李四的账户余额加100
  3. 这是一个整体的操作(应该具有原子性/隔离性)
  4. 如果其中任意一个操作失败都应该整个事务失败
  5. 必须确保都成功才能保证数据的正确性和一致性
go
package main

import (
	"fmt"

	"gorm_demo/db"

	"gorm.io/gorm"
)

func main() {
	// db.Migrate()

	err := db.ConnectDB().Transaction(func(t *gorm.DB) error {
		// 1.扣除张三余额100元
		// update users set money = money - 100 where name = '张三'
		result := t.Model(&db.User{}).Where("name = ?", "张三").Update("money", gorm.Expr("money - ?", 100))
		if result.Error != nil {
			return result.Error
		}

		// 如果任意一个步骤出错, 则回滚事务
		// 由于这里不会出错, 所以需要手动模拟出错了来查看回滚效果
		// err := errors.New("手动模拟任务出错了")
		// if err != nil {
		// 	return err
		// }

		// 2.增加李四余额100元
		// update users set money = money + 100 where name = '李四'
		result2 := t.Model(&db.User{}).Where("name = ?", "李四").Update("money", gorm.Expr("money + ?", 100))
		if result2.Error != nil {
			return result2.Error
		}

		// 3.创建转账记录
		// t.Model(&MoneyLog{}).Create(...)
		return nil
	})

	if err != nil {
		fmt.Println("执行事务出错:", err)
		return
	}

	fmt.Println("执行事务成功")
}
go
package db

import (
	"gorm.io/gorm"
)

// User 用户表模型
type User struct {
	gorm.Model
	Telephone string  `gorm:"unique;not null"` // 用户电话
	Password  string  `gorm:"not null"`        // 用户密码
	Money     float64 `gorm:"default:0"`       // 账户余额(默认0)
}
go
package main

import (
	"gorm_demo/db"
)

func main() {
	// db.Migrate()

	db.ConnectDB().Create(&db.User{
		Name:      "张三",
		Telephone: "13812345678",
		Password:  "123456",
		Money:     100,
	})

	db.ConnectDB().Create(&db.User{
		Name:      "李四",
		Telephone: "13912345678",
		Password:  "123456",
		Money:     100,
	})
}

Released under the MIT License.