Go Web学习笔记(五)- 数据库ORM
的今天开始学习数据库操作:
- 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 | package db |
sql.DB并不是一个链接,而是数据库连接池的句柄,内部管理多个链接,应该在应用启动时创建一次并复用,而不是每次请求都创建。
创建表结构
1 | CREATE TABLE users ( |
编写repository
internal/repository/user_repository.go(重新调整了下目录)
1 | package repository |
注意,请使用带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 | package service |
修改handler
1 | package handler |
修改router.go
修改router.go:
1 | userRepo := repository.NewUserRepository(db) |
修改main.go
1 | dsn := "root:123456@tcp(127.0.0.1:3306)/goweb?charset=utf8mb4&parseTime=True&loc=Local" |
GORM
应该是类似于Java的Hibernate。
安装依赖
1 | go get gorm.io/gorm |
修改Model
model/user_dto.go
1 | type User struct { |
修改repository
internal/repository/user_repository
1 | // 实际用下来,应该新建一个user_gorm_resp.go好一点 |
修改service
internal/service/user_service.go
1 | type UserService struct { |
修改db.go
pkg/db/db.go
1 | // 新增 |
修改router.go
routers/router.go
1 | userGormRepo := repository.NewGormUserRepository(gorm) |
方法签名修改为
1 | func SetupRouter(db *sql.DB, gorm *gorm.DB) *gin.Engine |
修改main.go
1 | gormInstance, err := db.NewGormDB(dsn) |
使用GORM可以明显感觉到开发效率高很多,不需要自己去写SQL,然后做映射,像Hibernate那样,CRUD很快、自动迁移方便、关联查询方便、Hook / Transaction / Preload功能完整。
当然有优点,那就有缺点:
- 复杂SQL可读性差
- 需要理解ORM生成的SQL
- 很难做到性能调优
选择database/sql的理由:
- 需要自己掌控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 | package repository |
对于事务,需要特别注意如下三个问题:
避免混用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 | type Profile struct { |
手动事务
适合精细化控制的方式,如果不想在闭包、或者事务链路非常长、需要根据复杂的跨系统业务接过来人为决定何时提交,GORM也保留了类似原生database/sql的手动事务机制。
手动事务的核心步骤:
- 开启:
tx := db.Begin() - 回滚:
tx.Rollback() - 提交:
tx.Commit()
下面是一段手动事务的模板代码:
1 | func (r *GormUserRepository) RegisterUserManual(ctx context.Context, name string) (err error) { |
全局禁用事务
GORM为了保证单条增删改语句的数据安全,默认情况下哪怕只调用一次db.Create(&user),它也会在底层隐式的开启BEGIN和COMMIT。
- 如果追求极致的并发性能,可以通过在初始化数据库时,配置
SkipDefaultTransaction来关掉这个隐式特性。
1 | func NewGormDB(dsn string) (*gorm.DB, error) { |
事务传递
我查了下GORM不支持像Spring那样的事务传递(@Transactional(propagation=Propagation.REQUIRED)),但也可以依靠*gorm.DB上下文对象的显示传递和Transaction闭包机制,可以实现媲美于Spring的事务传播效果。
下面模拟一个PROPAGATION_REQUIRED
internal/service/user_service.go
1 | func (s *UserService) UpdateUser(ctx context.Context, u model.User) (err error) { |
internal/repository/user_repository.go
1 | func (r *GormUserRepository) UpdateInfo(ctx context.Context, tx *gorm.DB, user *model.User) error { |


