今天开始学习数据库操作:
database/sql:标准库,偏底层,显示SQL
GORM: ORM 开发效率高,适合快速CRUD
database/sql database/sql是Go标准库提供的通用SQL数据库接口,需要配合具体数据库driver使用。
安装依赖 1 go get github.com/go-sql-driver/mysql
创建pkg/db/db.go文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package dbimport ( "database/sql" "log" "time" _ "github.com/go-sql-driver/mysql" ) func NewDB (dsn string ) (*sql.DB, error ) { db, err := sql.Open("mysql" , dsn) if err != nil { return nil , err } db.SetMaxOpenConns(20 ) db.SetMaxIdleConns(10 ) db.SetConnMaxLifetime(time.Hour) if err := db.Ping(); err != nil { return nil , err } log.Println("pong...." ) return db, nil }
sql.DB并不是一个链接,而是数据库连接池的句柄,内部管理多个链接,应该在应用启动时创建一次并复用,而不是每次请求都创建。
创建表结构 1 2 3 4 5 6 7 8 9 10 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR (50 ) NOT NULL , email VARCHAR (100 ) NOT NULL UNIQUE , age INT NOT NULL , password VARCHAR (255 ) NOT NULL , status VARCHAR (20 ) NOT NULL , created_at DATETIME NOT NULL , updated_at DATETIME NOT NULL );
编写repository internal/repository/user_repository.go(重新调整了下目录)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 package repositoryimport ( "context" "database/sql" "errors" "log" "sync" "dev.net.cn/goweb/errs" "dev.net.cn/goweb/model" ) type UserRepository struct { mu sync.RWMutex users map [int ]*model.User db *sql.DB } func NewUserRepository (db *sql.DB) *UserRepository { return &UserRepository{ users: make (map [int ]*model.User), db: db, } } func (r *UserRepository) FindByID(ctx context.Context, id int ) (*model.User, error ) { query := ` select id,name,age,email,password,create_at,update_at from users where id = ? ` var user model.User err := r.db.QueryRowContext(ctx, query, id).Scan( &user.ID, &user.Name, &user.Age, &user.Email, &user.Status, &user.CreateAt, &user.UpdateAt, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil , errs.ErrUserNotFound } return nil , err } return &user, nil } func (r *UserRepository) Create(ctx context.Context, user *model.User) (*model.User, error ) { query := ` insert into users(name,email,age,password,status,create_at,update_at) values (?,?,?,?,?,?,?) ` result, err := r.db.ExecContext( ctx, query, user.Name, user.Email, user.Age, user.Password, user.Status, user.CreateAt, user.UpdateAt, ) if err != nil { return nil , err } id, err := result.LastInsertId() if err != nil { return nil , err } user.ID = int (id) log.Println("mysql ...." ) return user, nil } func (r *UserRepository) ListUser(ctx context.Context) ([]*model.User, error ) { query := `select id,name,email,age,password,status,create_at,update_at from users` rows, err := r.db.QueryContext(ctx, query) if err != nil { return nil , err } defer func () { err := rows.Close() if err != nil { log.Fatalf("查询关闭失败 : %v" , err) } }() var users []*model.User for rows.Next() { var u model.User err := rows.Scan( &u.ID, &u.Name, &u.Email, &u.Age, &u.Password, &u.Status, &u.CreateAt, &u.UpdateAt, ) if err != nil { return nil , err } users = append (users, &u) } if err = rows.Err(); err != nil { return nil , err } return users, nil }
注意,请使用带Context的方法:ExecContext、QueryContext、QueryRowContext,这样请求取消或者超时时,数据库也有机会取消。
QueryRow用于查询单行结果,通常配合Scan;Query用于查询多行结果,需要遍历rows.Next(),最后检查rows.Err()
注意点1:
对于QueryContext,查询完成后需要rows.Close(),因为它执行后会从底层得连接池sql.DB中独占一个有效的数据库链接,知道rows的数据全部读完。如果你得代码在rows.Next()循环中因为某种原因提前return,或者逻辑问题没处理完且没有调用Close(),那么这个TCP连接将会被永远挂起,无法放回连接池中。一单达到SetMaxOpenConns得上线就会彻底卡死。
注意点2:
循环完成后,还需要补一个rows.Err()检查。避免读取某一行是因网络波动发生异常,为了区分是读取完数据,还是发生异常,这里就需要在循环外再调用rows.Err()进行二次确认。避免将不完整得数据返回给上层。
注意点3:
还需要注意的是NULL值,Go语言的int、string等类型不能接收数据库得NULL值,所以建表的时候必须加上NOT NULL DEFAULT ''或者DEFAULT 0,如果表里就是有NULL,那就需要标准库自带的sql.NullInt32、sql.NullString,或者将字段定义为指针类型(Age *int),这样遇到NULL时,Go会自动赋值为nil
修改service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package serviceimport ( "context" "log" "time" "dev.net.cn/goweb/internal/repository" "dev.net.cn/goweb/model" ) type UserService struct { userRepo *repository.UserRepository } func NewUserService (userRepo *repository.UserRepository) *UserService { return &UserService{ userRepo: userRepo, } } type CreateUserInput struct { Name string Email string Age int Password string } func (s *UserService) CreateUser(ctx context.Context, input CreateUserInput) (*model.User, error ) { user := &model.User{ Name: input.Name, Email: input.Email, Age: input.Age, Password: input.Password, Status: model.UserStatusActive, CreateAt: time.Now(), UpdateAt: time.Now(), } log.Println("service ...." ) return s.userRepo.Create(ctx, user) } func (s *UserService) FindUserByID(ctx context.Context, id int ) (*model.User, error ) { user, err := s.userRepo.FindByID(ctx, id) if err != nil { log.Fatalf("查询用户失败: %v" , err) return nil , err } return user, nil } func (s *UserService) ListUser(ctx context.Context) ([]*model.User, error ) { users, err := s.userRepo.ListUser(ctx) if err != nil { return nil , err } return users, nil }
修改handler 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 package handlerimport ( "strconv" "strings" "dev.net.cn/goweb/errs" "dev.net.cn/goweb/internal/service" "dev.net.cn/goweb/model" "dev.net.cn/goweb/response" "github.com/gin-gonic/gin" ) type UserHandler struct { userService *service.UserService } func NewUserHandler (userService *service.UserService) *UserHandler { return &UserHandler{ userService: userService, } } func (h *UserHandler) GetUserListHandler(c *gin.Context) { page, err := strconv.Atoi(c.DefaultQuery("page" , "1" )) if err != nil { response.BadRequest(c, err.Error()) return } pageSize, err := strconv.Atoi(c.DefaultQuery("page_size" , "10" )) if err != nil { response.BadRequest(c, err.Error()) return } users, err := h.userService.ListUser(c.Request.Context()) if err != nil { response.HandleError(c, err) return } response.PageOK(c, users, page, pageSize) } func (h *UserHandler) GetUserInfoByIdHandler(c *gin.Context) { id, err := strconv.Atoi(c.Param("id" )) if err != nil { response.BadRequest(c, "id must be number" ) return } if id < 0 { response.BadRequest(c, "id is null" ) return } user, err := h.userService.FindUserByID(c.Request.Context(), id) response.OK(c, user) } func (h *UserHandler) CreateUserHandler(c *gin.Context) { var user model.CreateUserReq if err := c.ShouldBindJSON(&user); err != nil { response.BadRequest(c, err.Error()) return } if err := validateEmail(user.Email); err != nil { response.HandleError(c, err) return } user1, err := h.userService.CreateUser(c.Request.Context(), service.CreateUserInput{ Name: user.Name, Age: user.Age, Email: user.Email, Password: user.Password, }) if err != nil { response.BadRequest(c, err.Error()) return } res := model.UserResponse{ ID: 1 , Name: user1.Name, Age: user1.Age, Email: user1.Email, } response.OK(c, res) } func UpdateUserHandler (c *gin.Context) { id := c.Param("id" ) if id == "" { response.BadRequest(c, "id is null" ) return } var user model.CreateUserReq if err := c.ShouldBindJSON(&user); err != nil { response.BadRequest(c, err.Error()) } res := model.UserResponse{ ID: 1 , Name: user.Name, Age: user.Age, Email: user.Email, } response.OK(c, res) } func DeleteUserByIdHandler (c *gin.Context) { id := c.Param("id" ) if id == "" { response.BadRequest(c, "id is null" ) return } response.OK(c, "用户已被删除" ) } func FormLogin (c *gin.Context) { username := c.PostForm("username" ) password := c.PostForm("password" ) response.OK(c, gin.H{ "username" : username, "password" : password, }) } func validateEmail (email string ) error { if strings.HasSuffix(email, "@qq.com" ) { return nil } return errs.ErrEmailExists }
修改router.go 修改router.go:
1 2 3 4 5 6 7 8 9 10 userRepo := repository.NewUserRepository(db) d1Group := r.Group("/api/d1/" ) { d1Group.GET("/users" , userHandler.GetUserListHandler) d1Group.GET("/users/:id" , userHandler.GetUserInfoByIdHandler) d1Group.POST("/users" , userHandler.CreateUserHandler) d1Group.PUT("/users/:id" , handler.UpdateUserHandler) d1Group.DELETE("users/:id" , handler.DeleteUserByIdHandler) }
修改main.go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 dsn := "root:123456@tcp(127.0.0.1:3306)/goweb?charset=utf8mb4&parseTime=True&loc=Local" dbInstance, err := db.NewDB(dsn) if err != nil { log.Fatalf("数据库连接失败: %v" , err) } log.Println("数据库连接池初始化成功..." ) defer func () { err := dbInstance.Close() if err != nil { log.Fatalf("数据库关闭失败: %v" , err) } }() r := routers.SetupRouter(dbInstance)
GORM 应该是类似于Java的Hibernate。
安装依赖 1 2 go get gorm.io/gorm go get gorm.io/driver/mysql
GORM VS database/sql
操作
原生
GORM
插入
db.Exec(“INSERT …”)
db.Create(&user)
查询
db.QueryRow(“SELECT …”)
db.First(&user,1)
更新
db.Exec(“UPDATE ….”)
db.Model(&user).Update(…)
删除
db.Exec(“DELETE …”)
db.Delete(&user)
就像Hibernate一样,不用写SQL就可以完成绝大多数的CRUD场景。
定义模型 通过结构体定义表结构(Java通过类定义表结构)
1 2 3 4 5 6 7 8 9 10 type User struct { ID int `gorm:"primaryKey"` Name string `gorm:"size:50;not null"` Email string `gorm:"size:100;not null"` Age int `gorm:"not null"` Password string `gorm:"type:varchar(255);not null"` Status string `gorm:"size:20;not null"` CreateAt time.Time UpdateAt time.Time }
User结构体名称默认会被转换为 数据库的表名 users,这里会带个s。
等同于如下SQL
1 2 3 4 5 6 7 8 9 10 CREATE TABLE users ( id INT PRIMARY KEY , name varchar (50 ) NOT NULL , email varchar (100 ) NOT NULL , age int NOT NULL , password varchar (255 ) NOT NULL , status varchar (20 ) NOT NULL , create_at DATETIME, update_at DATATIME );
从这里可以注意到,GORM使用了和JSON一样的方式,结构体标签:gorm:"primaryKey"。
结构体标签 常用的标签有:
标签
含义
示例
SQL
type
指定类型
gorm:"type:varchar(100)"
email varchar(100)
not null
非空约束
gorm::"not null"
status varchar(20) NOT NULL
uniqueIndex
唯一索引
gorm:"uniqueIndex"
UNIQUE KEY idx_users_mobile (mobile)
index
普通索引/联合索引
gorm:"index"
KEY idx_users_name (name),
default
默认值
gorm:"default:aaa
xxx varchar NOT NULL DEFAULT aaa
column
自定义列名
gorm:"column:age"
age INT
primaryKey
指定主键
gorm:"primaryKey"
ID uint gorm:"primaryKey"
autoIncrement
自增
gorm:”primaryKey;autoIncrement”
NOT NULL AUTO_INCREMENT
-
忽略字段
gorm:”-“
该字段不体现在SQL中
size
大小限制
gorm:”size:255”
varchar(255)
其他的还有foreignKey,references,包括嵌入结构体embedded等。
下面再来一个完整的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package modelimport ( "time" "gorm.io/gorm" ) type User struct { ID int64 `gorm:"primaryKey;autoIncrement;comment:主键ID"` Username string `gorm:"type:varchar(50);not null;uniqueIndex;comment:用户名"` Password string `gorm:"type:varchar(255);not null;comment:密码哈希"` Nickname string `gorm:"type:varchar(50);not null;default:'';comment:昵称"` Age int `gorm:"type:int;not null;default:0;index;comment:年龄"` Email string `gorm:"type:varchar(100);not null;default:'';comment:邮箱"` Status tinyint `gorm:"type:tinyint;not null;default:1;comment:状态:1=正常,2=禁用"` CreatedAt time.Time `gorm:"column:created_at;not null;comment:创建时间"` UpdatedAt time.Time `gorm:"column:updated_at;not null;comment:更新时间"` DeletedAt gorm.DeletedAt `gorm:"index;comment:删除时间(软删除标记)"` LoginToken string `gorm:"-"` }
执行db.AutoMigrate(&User{}),生成如下语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 CREATE TABLE `users` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID' , `username` varchar (50 ) NOT NULL COMMENT '用户名' , `password` varchar (255 ) NOT NULL COMMENT '密码哈希' , `nickname` varchar (50 ) NOT NULL DEFAULT '' COMMENT '昵称' , `age` int NOT NULL DEFAULT '0' COMMENT '年龄' , `email` varchar (100 ) NOT NULL DEFAULT '' COMMENT '邮箱' , `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1=正常,2=禁用' , `created_at` datetime(3 ) NOT NULL COMMENT '创建时间' , `updated_at` datetime(3 ) NOT NULL COMMENT '更新时间' , `deleted_at` datetime(3 ) DEFAULT NULL COMMENT '删除时间(软删除标记)' , PRIMARY KEY (`id`), UNIQUE KEY `idx_users_username` (`username`), KEY `idx_users_age` (`age`), KEY `idx_users_deleted_at` (`deleted_at`) ) ENGINE= InnoDB DEFAULT CHARSET= utf8mb4 COLLATE = utf8mb4_0900_ai_ci;
自定义表名 GORM默认使用结构体的名(s)称作为表名,如果想自定义表名,需要在定义结构体的地方添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 type User struct { ID int `gorm:"primaryKey"` Name string `gorm:"size:50;not null"` Email string `gorm:"size:100;not null"` Age int `gorm:"not null"` Password string `gorm:"type:varchar(255);not null"` Status string `gorm:"size:20;not null"` CreateAt time.Time UpdateAt time.Time } func (User) TableName() string { return "user_info" }
除了表名规则,还有列名规则:
Name -> name
Email -> email
CreateAt -> create_at
时间戳 从上面的izi可以看到,对于时间字段,并没有使用结构体标签,因为GORM会自动管理这种字段。
软删除 如果结构体中定义有如下字段:
1 DeletedAt gorm.DeletedAt `gorm:"index"`
此时GORM就会支持软删除,所谓软删除就是不真删除数据,只是给deleted_at字段设个时间戳,查询的时候GORM会自动过略掉已删除的记录。
自动建表 代码执行db.AutoMigrate(&User{})就会自动创建user的表:
检测表是否存在,不存在就创建
检查字段是否完整,缺少的列自动补充
检测索引是否正确
AutoMigrate 只会增加列和索引,不会删除或者修改已存在的列。
还可以通过如下方式检测表是否存在
1 2 3 db.Migrator().HasTable(&User{}) { }
对于生产环境,更推荐migration工具:golang-migrate、goose、atlas。 因为正式环境的数据库变更需要可审计、可回滚、可灰度,不能依赖启动时自动修改表结构。
增删改查
Create : 新增
First : 查询单条数据
Find : 查询多条数据
Where : 条件查询
Updates : 更新多个字段
Update : 更新单个字段
Delete : 删除
Unscoped : 物理删除/查询软删除数据
Transaction : 事务
Create 1 2 3 func (r *goodsRepository) Create(ctx context.Context, goods *model.Goods) error { return r.db.WithContext(ctx).Create(goods).Error }
还可以批量保存:
1 2 3 4 5 6 7 8 9 10 11 12 13 func (r *goodsRepository) BatchCreate(ctx context.Context, goods []*model.Goods) error { if len (goods) == 0 { return nil } if err := r.db.WithContext(ctx).Create(&goods).Error; err != nil { return fmt.Errorf("批量保存商品失败: %w" , err) } return nil }
First 1 2 3 4 5 6 7 8 func (r *goodsRepository) FindGoodsByID(ctx context.Context, id int64 ) (*model.Goods, error ) { var goods model.Goods if err := r.db.WithContext(ctx).First(&goods, id).Error; err != nil { return nil , err } return &goods, nil }
对于Service层
1 2 3 4 5 6 7 8 9 goods, err := s.goodsRepository.FindGoodsByID(ctx, 1001 ) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil , errors.New("商品不存在" ) } return nil , err }
Find 1 2 3 4 5 6 7 func (r *goodsRepository) ListGoods(ctx context.Context) ([]*model.Goods, error ) { goods := make ([]*model.Goods, 0 ) if err := r.db.WithContext(ctx).Find(&goods).Error; err != nil { return nil , err } return goods, nil }
Where 带条件查询
1 2 3 4 5 6 7 func (r *goodsRepository) FindGoodsByIDAndPrice(ctx context.Context, id int64 , price int ) (*model.Goods, error ) { var goods model.Goods if err := r.db.WithContext(ctx).Where("price = ?" , price).First(&goods, id).Error; err != nil { return nil , err } return &goods, nil }
复杂条件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 type ListGoodsInput struct { Keyword string Status string Page int PageSize int } func (r *goodsRepository) FindGoodsByMultiCondition(ctx context.Context, input ListGoodsInput) ([]model.Goods, int64 , error ) { goods := make ([]model.Goods, 0 ) var total int64 if input.Page <= 0 { input.Page = 1 } if input.PageSize <= 0 || input.PageSize > 100 { input.PageSize = 20 } query := r.db.WithContext(ctx).Model(&model.Goods{}) if input.Keyword != "" { keyword := "%" + input.Keyword + "%" query = query.Where("name LIKE ? OR code LIKE ?" , keyword, keyword) } if input.Status != "" { query = query.Where("status = ?" , input.Status) } if err := query.Session(&gorm.Session{}).Count(&total).Error; err != nil { return nil , 0 , fmt.Errorf("统计商品数失败 : %w" , err) } if total == 0 { return goods, 0 , nil } offset := (input.Page - 1 ) * input.PageSize if err := query.Order("id DESC" ).Limit(input.PageSize).Offset(offset).Find(&goods).Error; err != nil { return nil , 0 , fmt.Errorf("查询商品失败 : %w" , err) } return goods, total, nil }
Updates 更新多个字段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func (r *goodsRepository) Update(ctx context.Context, id int , input UpdateGoodsInput) (*model.Goods, error ) { var goods model.Goods err := r.db.WithContext(ctx).Transaction(func (tx *gorm.DB) error { if err := tx.First(&goods, id).Error; err != nil { return err } update := map [string ]any{} if input.Name != "" { update["name" ] = input.Name } if input.Status != "" { update["status" ] = input.Status } if len (update) == 0 { return nil } if err := tx.Model(&goods).Updates(update).Error; err != nil { return nil } return tx.First(&goods, id).Error }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil , gorm.ErrRecordNotFound } return nil , fmt.Errorf("update user failed : %w" , err) } return &goods, nil }
Update 更新单个字段
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func (r *goodsRepository) UpdateGoods(ctx context.Context, id int , price int ) (*model.Goods, error ) { var goods model.Goods err := r.db.WithContext(ctx).Transaction(func (tx *gorm.DB) error { if err := tx.First(&goods, id).Error; err != nil { return err } if err := tx.Model(&goods).Update("price" , price).Error; err != nil { return err } return tx.First(&goods, id).Error }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil , gorm.ErrRecordNotFound } return nil , fmt.Errorf("update goods failed : %w" , err) } return &goods, nil }
Delete 软删除
1 2 3 4 5 6 7 8 9 10 func (r *goodsRepository) DeleteByID(ctx context.Context, id int64 ) error { result := r.db.WithContext(ctx).Delete(&model.Goods{}, id) if result.Error != nil { return fmt.Errorf("delete user failed : %w" , result.Error) } if result.RowsAffected == 0 { return gorm.ErrRecordNotFound } return nil }
Unscoped 物理删除:Unscoped().Delete()
1 2 3 4 5 6 7 8 9 10 func (r *goodsRepository) unScopedDeleteByID(ctx context.Context, id int64 ) error { result := r.db.WithContext(ctx).Unscoped().Delete(&model.Goods{}, id) if result.Error != nil { return fmt.Errorf("hard delete user failed : %w" , result.Error) } if result.RowsAffected == 0 { return gorm.ErrRecordNotFound } return nil }
GORM Hook GORM Hook是模型生命周期回调函数,会在创建、查询、更新、删除前后自动执行;Hook方法签名必须是func(*grom.DB) error。如果Hook返回error,GORM会停止后续操作并回滚当前事务。
例如:
1 2 3 4 func (u *model.User) BeforeCreate(tx *gorm.DB) error { return nil }
通常场景为:
创建前:校验字段、规范化email、生成UUID、设置默认状态
创建后:写审计字段、初始化用户配置
更新前:禁止修改敏感字段、校验状态流转
更新后:写变更日志、同步冗余字段
删除前:阻止删除系统管理员
删除后:写删除日志
查询后:设置默认展示字段、脱敏部分字段
Hook生命周期顺序 Create生命周期 创建对象时,GORM的Hook顺序时:
1 2 3 4 5 6 7 8 BeforeSave BeforeCreate 保存前置关联 INSERT INTO database 保存后之关联 AfterCreate AfterSave commit或 rollback
Update生命周期 更新对象时,GORM的Hook顺序是:
1 2 3 4 5 6 7 8 BeforeSave BeforeUpdate 保存前置关联 UPDATE database 保存后置关联 AfterUpdate AfterSave commit 或 rollback
Delete 生命周期 删除对象时,Hook顺序是:
1 2 3 4 BeforeDelete DELETE FROM database AfterDelete commit 或 rollback
如果模型里有gorm.DeletedAt字段,GORM默认执行软删除,也就是更新deleted_at字段,而不是物理删除记录。
Query生命周期 查询对象时,主要Hook是:
就是数据从数据库加载出来后自动执行。
Hook案例 在internal/model/user_dto.go中,添加如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 func (u *User) BeforeSave(tx *gorm.DB) error { fmt.Println("[Hook] BeforeSave" ) u.Email = strings.ToLower(strings.TrimSpace(u.Email)) u.Name = strings.TrimSpace(u.Name) if u.Name == "" { return errors.New("name is required" ) } if u.Email == "" { return errors.New("email is required" ) } return nil } func (u *User) BeforeCreate(tx *gorm.DB) error { fmt.Println("[Hook] BeforeCreate" ) if u.Status == "" { u.Status = UserStatusActive } if u.Password == "" { return errors.New("password is required" ) } return nil } func (u *User) AfterCreate(tx *gorm.DB) error { fmt.Println("[Hook] AfterCreate" ) log := AuditLog{ Action: "CREATE" , Entity: "users" , EntityID: u.ID, Message: fmt.Sprintf("created user :%s" , u.Name), } if err := tx.Create(&log).Error; err != nil { return err } return nil } func (u *User) BeforeUpdate(tx *gorm.DB) error { fmt.Println("[Hook] BeforeUpdate" ) if u.ID == 1 && tx.Statement.Changed("Password" ) { return errors.New("superadmin password cannot be changed" ) } if tx.Statement.Changed("Status" ) { if u.Status != UserStatusDisable && u.Status != UserStatusActive { return errors.New("invalid user status" ) } } return nil } func (u *User) AfterUpdate(tx *gorm.DB) error { fmt.Println("[Hook] AfterUpdate" ) log := AuditLog{ Action: "UPDATE" , Entity: "users" , EntityID: u.ID, Message: fmt.Sprintf("updated user : %s" , u.Name), } if err := tx.Create(&log).Error; err != nil { return err } return nil } func (u *User) BeforeDelete(tx *gorm.DB) error { fmt.Println("[Hook] BeforeDelete" ) if u.ID == 1 { return errors.New("superadmin cannot be deleted" ) } return nil } func (u *User) AfterDelete(tx *gorm.DB) error { fmt.Println("[Hook] AfterDelete" ) log := AuditLog{ Action: "DELETE" , Entity: "users" , EntityID: u.ID, Message: fmt.Sprintf("deleted user : %s" , u.Name), } if err := tx.Create(&log).Error; err != nil { return err } return nil } func (u *User) AfterFind(tx *gorm.DB) error { fmt.Println("[Hook] AfterFind" ) if u.Email == "" { u.Email = u.Name + "@dev.net.cn" } return nil }
此时,执行一次创建用户,就会在控制台打印:
1 2 3 4 5 6 7 [30.607ms] [rows:1] INSERT INTO `users ` (`name`,`email`,`age`,`password`,`status`,`create_at`,`update_at`) VALUES ('devid' ,'devid@qq.com' ,19,'123456' ,'active' ,'2026-06-17 16:27:22.092' ,'2026-06-17 16:27:22.092' ) [GIN] 2026/06/17 - 16:27:22 | 400 | 31.75ms | 127.0.0.1 | POST "/api/d1/users" 2026/06/17 16:27:49 service .... [Hook] BeforeSave [Hook] BeforeCreate [Hook] AfterCreate [GIN] 2026/06/17 - 16:27:49 | 200 | 30.19ms | 127.0.0.1 | POST "/api/d1/users"
执行一次查询:
1 2 3 4 5 6 [GIN] 2026/06/17 - 16:27:49 | 200 | 30.19ms | 127.0.0.1 | POST "/api/d1/users" [Hook] AfterFind [Hook] AfterFind [Hook] AfterFind [Hook] AfterFind [GIN] 2026/06/17 - 16:31:12 | 200 | 9.81ms | 127.0.0.1 | GET "/api/d1/users?page=1&page_size=15"
每查出一条数据,就会调用一次AfterFind。
BeforeSave和BeforeCreate的区别是:BeforeSave在创建和更新前都会执行;BeforeCreate只在创建前执行。如果逻辑只适用于新增、应该放在BeforeCreate,如果创建和更新都需要,例如:trim去掉空字符等,可以放在BeforeSave。
根据案例还发现,所有的参数都是tx *grom.DB,原因就是
tx表示当前GORM操作所在的事务上下文。Hook里使用tx可以保证著操作和Hook里的副操作在同一事务内,要么都成功、要么都回滚。如果使用db,可能会导致数据不一致。
对于Updates和Update,还需要监控字段值是否发生变化,可以使用tx.Statment.Changed("FieldName")判断字段是否发生变化。
SetColumn 如果模型中,存在updated_at字段,在执行Updates或者Update时,更新这个updated_at字段的值,可以使用如下方式:
1 2 3 4 5 6 func (u *User) BeforeUpdate(tx *gorm.DB) error { if tx.Statement.Changed("Status" ){ tx.Statment.SetColumn("UpdateAt" ,time.Now()) } return nil }
对于这个字段的更新,也是推荐使用这种方式,能够确保该字段进入更新语句。
跳过Hook 如果不想触发Hook,可以通过Session(&gorm.Session(SkipHooks:true))跳过Hook。
1 2 3 err := db.Session(&gorm.Session{ SkipHook: true , }).Create(&user).Error
案例 修改Model model/user_dto.go
1 2 3 4 5 6 7 8 9 10 type User struct { ID int `gorm:"primaryKey"` Name string `gorm:"size:50;not null"` Email string `gorm:"size:100;not null"` Age int `gorm:"not null"` Password string `gorm:"size:255;not null"` Status string `gorm:"size:20;not null"` CreateAt time.Time UpdateAt time.Time }
修改repository internal/repository/user_repository
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 func NewUserRepository (db *sql.DB) *UserRepository { return &UserRepository{ users: make (map [int ]*model.User), db: db, } } type GormUserRepository struct { db *gorm.DB } func NewGormUserRepository (db *gorm.DB) *GormUserRepository { return &GormUserRepository{db: db} } func (r *GormUserRepository) Create(ctx context.Context, user *model.User) (*model.User, error ) { if err := r.db.WithContext(ctx).Create(user).Error; err != nil { return nil , err } return user, nil } func (r *GormUserRepository) FindByID(ctx context.Context, id int ) (*model.User, error ) { var user model.User err := r.db.WithContext(ctx).First(&user, id).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil , errs.ErrUserNotFound } return nil , err } return &user, nil }
修改service internal/service/user_service.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type UserService struct { userRepo *repository.UserRepository userGormRepo *repository.GormUserRepository } func NewUserService (userRepo *repository.UserRepository, gormRepo *repository.GormUserRepository) *UserService { return &UserService{ userRepo: userRepo, userGormRepo: gormRepo, } } func (s *UserService) FindUserByID(ctx context.Context, id int ) (*model.User, error ) { user, err := s.userGormRepo.FindByID(ctx, id) if err != nil { log.Fatalf("查询用户失败: %v" , err) return nil , err } return user, nil }
修改db.go pkg/db/db.go
1 2 3 4 5 6 7 8 9 10 11 12 13 func NewGormDB (dsn string ) (*gorm.DB, error ) { db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { return nil , err } if err := db.AutoMigrate(&model.User{}); err != nil { return nil , err } return db, nil }
修改router.go routers/router.go
1 2 userGormRepo := repository.NewGormUserRepository(gorm) userService := service.NewUserService(userRepo, userGormRepo)
方法签名修改为
1 func SetupRouter (db *sql.DB, gorm *gorm.DB) *gin.Engine
修改main.go 1 2 3 4 5 gormInstance, err := db.NewGormDB(dsn) if err != nil { log.Fatalf("Gorm数据库连接失败: %v" , err) } r := routers.SetupRouter(dbInstance, gormInstance)
使用GORM可以明显感觉到开发效率高很多,不需要自己去写SQL,然后做映射,像Hibernate那样,CRUD很快、自动迁移方便、关联查询方便、Hook / Transaction / Preload功能完整。
当然有优点,那就有缺点:
复杂SQL可读性差
需要理解ORM生成的SQL
很难做到性能调优
选择database/sql的理由:
除此之外,GORM开发效率更高。
事务 谈到DB,就绕不开事务。
database/sql事务 在Go语言中,事物的生命周期由sql.Tx对象管理。一个标准得事务包含以下四个阶段:
开启事务:调用db.BeginTx(ctx,opts)。此时,底层连接池会独占一格固定的TCP连接,并向MySQL发送BEGIN指令。
执行业务:后续所有的SQL语句(CRUD)必须调用tx.ExecContext或者tx.QueryContext,而不是db.ExceContext。
异常回滚:如果中途任何一步报错,或者程序发生Panic,必须执行tx.Rollback()
成功提交: 所有步骤完美通过后,执行tx.Commit(),底层连接释放,重归连接池。
下面以一个比较通用的事务模板代码为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 package repositoryimport ( "context" "database/sql" "errors" "log" ) type AccountRepository struct { db *sql.DB } func (r *AccountRepository) Transfer(ctx context.Context, fromID, toID int , amount float64 ) (err error ) { tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelDefault}) if err != nil { return err } defer func () { if p := recover (); p != nil { _ = tx.Rollback() panic (p) } else if err != nil { _ = tx.Rollback() log.Printf("事务回滚成功,原因:%v" , err) } }() query1 := `UPDATE accounts SET balance=balance - ? WHERE id = ? AND balance >= ?` res1, err := tx.ExecContext(ctx, query1, amount, fromID, amount) if err != nil { return err } rowsAffected1, _ := res1.RowsAffected() if rowsAffected1 == 0 { err = errors.New("扣款失败,余额不足或账户不存在" ) return err } query2 := `UPDATE accounts SET balance=balance + ? WHERE id = ?` res2, err := tx.ExecContext(ctx, query2, amount, toID) if err != nil { return err } rowsAffected2, _ := res2.RowsAffected() if rowsAffected2 == 0 { err = errors.New("收款账号不存在" ) return err } if err = tx.Commit(); err != nil { return err } return nil }
对于事务,需要特别注意如下三个问题:
避免混用db和tx:
开启事务后,中间得更新语句如果写成r.db.ExecContext(),那么就会导致r.db从数据库连接池再去获取一个新的连接,这条语句就在事务之外独立运行。而tx连接依然在等待。不仅破坏了原子性,还极其容易导致数据库死锁(Deadlock),因为两个TCP连接可能在互相等待对方释放同一行数据得锁。
忘记写defer tx.Rollback()导致连接池枯竭:
如果执行中途报错,你没有写defer,也没有在if err != nil里手动调用Rollback(),那么这个事务在MySQL里保持PENDING状态,它占用得TCP连接永远不会释放。高并发下,几秒钟就能把项目的数据库连接池(SetMaxOpenConns)全部耗尽,导致整个系统瘫痪。
在事务里操作其他事情
事务开启后,MySQL会对相关的行加排他锁(X锁)。如果中间请求了第三方网络接口,就意味着请求的时间就加到MySQL的锁的时间。所以,事务内只做纯粹的数据库增删改查,其他的操作必须在开启事务之前。或者提交事务之后。
GORM事务 GORM提供了两种事务处理机制:自动事务(闭包机制:推荐) 和手动事务。可以类比于Spring框架中的@Transcational注解。
自动事务 自动事务的核心规则:
如果闭包函数返回了nil,GORM会自动提交事务。
如果闭包函数返回了任何error,或者内部发生了Panic,GORM会自动回滚事务,并把对应的错误或者Panic往外抛。
下面是一段比较模板化的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 type Profile struct { UserID int `gorm:"uniqueIndex"` Hobby string } func (r *GormUserRepository) RegisterUser(ctx context.Context, name, hobby string ) error { err := r.db.WithContext(ctx).Transaction(func (tx *gorm.DB) error { user := model.User{Name: name, Status: model.UserStatusActive} if err := tx.Create(&user).Error; err != nil { return err } if name == "hack" { return errors.New("违规用户名,拒绝注册" ) } profile := Profile{UserID: user.ID, Hobby: hobby} if err := tx.Create(&profile).Error; err != nil { return err } return nil }) return err }
手动事务 适合精细化控制的方式,如果不想在闭包、或者事务链路非常长、需要根据复杂的跨系统业务接过来人为决定何时提交,GORM也保留了类似原生database/sql的手动事务机制。
手动事务的核心步骤:
开启:tx := db.Begin()
回滚:tx.Rollback()
提交:tx.Commit()
下面是一段手动事务的模板代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func (r *GormUserRepository) RegisterUserManual(ctx context.Context, name string ) (err error ) { tx := r.db.WithContext(ctx).Begin() if tx.Error != nil { return tx.Error } defer func () { if p := recover (); p != nil { tx.Rollback() panic (p) } else if err != nil { tx.Rollback() } }() user := model.User{Name: name} if err = tx.Create(&user).Error; err != nil { return err } if err = tx.Commit().Error; err != nil { return err } return nil }
全局禁用事务 GORM为了保证单条增删改语句的数据安全,默认情况下哪怕只调用一次db.Create(&user),它也会在底层隐式的开启BEGIN和COMMIT。
如果追求极致的并发性能,可以通过在初始化数据库时,配置SkipDefaultTransaction来关掉这个隐式特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func NewGormDB (dsn string ) (*gorm.DB, error ) { db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{ SkipDefaultTransaction: true , }) if err != nil { return nil , err } if err := db.AutoMigrate(&model.User{}); err != nil { return nil , err } return db, nil }
事务传递 我查了下GORM不支持像Spring那样的事务传递(@Transactional(propagation=Propagation.REQUIRED)),但也可以依靠*gorm.DB上下文对象的显示传递和Transaction闭包机制,可以实现媲美于Spring的事务传播效果。
下面模拟一个PROPAGATION_REQUIRED
internal/service/user_service.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 func (s *UserService) UpdateUser(ctx context.Context, u model.User) (err error ) { return s.userGormRepo.Db.Transaction(func (tx *gorm.DB) error { if err := s.userGormRepo.UpdateInfo(ctx, tx, &u); err != nil { return err } if err := s.userGormRepo.Rename(ctx, tx, "Jerry" , 1 ); err != nil { return err } return nil }) }
internal/repository/user_repository.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func (r *GormUserRepository) UpdateInfo(ctx context.Context, tx *gorm.DB, user *model.User) error { return tx.WithContext(ctx).Transaction(func (nestedTX *gorm.DB) error { updateSql := "update users set name=? where id=?" if err := nestedTX.Exec(updateSql, user.Name, user.ID).Error; err != nil { return err } return nil }) } func (r *GormUserRepository) Rename(ctx context.Context, tx *gorm.DB, name string , id int ) error { return tx.WithContext(ctx).Transaction(func (nestedTx *gorm.DB) error { updateSql := "update users set name=? where id=?" if err := nestedTx.Exec(updateSql, name, id).Error; err != nil { return err } return nil }) }