Golang Gin框架(六):Gin ORM
参考资料
ORM(入门)
安装和配置
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite # 数据库驱动快速上手
Gorm有两种API,一种是Gorm低于1.30.0版本的传统API,另一种是在这个版本之后的泛型API
放个传送门
连接数据库
Gorm目前支持下面这些数据库:
- SQLite
- Mysql
- PostgreSQL
- GaussDB
- Oracle Database
- SQL Server
- TiDB
- ClickHouse
其中,Mysql、PostgreSQL、GaussDB都支持使用自定义驱动
这里只演示(搬运)Mysql和SQLite的连接方式:
// 连接SQLite
import (
"gorm.io/driver/sqlite" // 基于 CGO 的 Sqlite 驱动
// "github.com/glebarez/sqlite" // 纯 Go 实现的 SQLite 驱动, 详情参考:https://github.com/glebarez/sqlite
"gorm.io/gorm"
)
// github.com/mattn/go-sqlite3
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})// 连接Mysql
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func main() {
// 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}Gorm还提供了一些高级设置,这里直接把示例搬过来:
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source name
DefaultStringSize: 256, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}), &gorm.Config{})本地运行测试:
func init() {
var err error
dsn := config.Config.DB.Serialize()
fmt.Printf("dsn: %v\n", dsn)
// user:pass@tcp(localhost:3306)/app?charset=utf8mb4&parseTime=true&loc=Local
DBConn, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
}
声明模型
模型定义
感觉官方文档不是给初学者看的,因为文档的编排顺序是先教声明模型才教数据库连接……byd
模型是使用普通结构体定义的。 这些结构体可以包含具有基本Go类型、指针或这些类型的别名,甚至是自定义类型(只需要实现
database/sql包中的Scanner和Valuer接口)。
// 示例
type User struct {
ID uint // Standard field for the primary key
Name string // A regular string field
Email *string // A pointer to a string, allowing for null values
Age uint8 // An unsigned 8-bit integer
Birthday *time.Time // A pointer to time.Time, can be null
MemberNumber sql.NullString // Uses sql.NullString to handle nullable strings
ActivatedAt sql.NullTime // Uses sql.NullTime for nullable time fields
CreatedAt time.Time // Automatically managed by GORM for creation time
UpdatedAt time.Time // Automatically managed by GORM for update time
ignored string // fields that aren't exported are ignored
}- 具体数字类型如
uint、string和uint8直接使用。 - 指向
*string和*time.Time类型的指针表示可空字段。 - 来自
database/sql包的sql.NullString和sql.NullTime用于具有更多控制的可空字段。 CreatedAt和UpdatedAt是特殊字段,当记录被创建或更新时,GORM 会自动向内填充当前时间。- 以小写字母开头的私有变量不会被映射
除了 GORM 中模型声明的基本特性外,强调下通过 serializer 标签支持序列化也很重要。 此功能增强了数据存储和检索的灵活性,特别是对于需要自定义序列化逻辑的字段。详细说明请参见 Serializer。
注意一下声明约定:
- 主键:GORM 使用一个名为
ID的字段作为每个模型的默认主键。 - 表名:默认情况下,GORM 将结构体名称转换为
snake_case并为表名加上复数形式。 例如,如果我们有一个叫User的结构体,那么在数据库中它会变成users,同样地,GormUserName会变成gorm_user_names - 列名:GORM 自动将结构体字段名称转换为
snake_case作为数据库中的列名。 - 时间戳字段:GORM使用字段
CreatedAt和UpdatedAt来自动跟踪记录的创建和更新时间。
官方文档的声明模型没有提到如何自定义表名,这里补充一下:
- Gorm会访问接收者类型为结构体值类型 的
TableName方法来获取表名 - 对于Psql数据库,可以用
TableName方法显式指定表所在的结构名
type User struct {
gorm.Model // 里面自带ID主键、创建时间、更新时间和删除时间
Username string `gorm:"uniqueindex;type:varchar(255)"`
Password string `gorm:"not null;"`
Active bool `gorm:not null;default=false`
LastLoginAt time.Time
LastLogoutAt time.Time
}
func (u User) TableName() string {
return "public.users" // 显示指定schema名为public
}高级选项 (只是占个地方)
放个传送门
迁移 && 一键建表
定义一个User表用于测试:
type User struct {
ID uint // 无符号整型uint uint8可直接使用 // 对应列名id
Username string // 不加*号的字段都默认非空 // 对应列名username
Password string // 不加*号的字段都默认非空 // 对应列名password
Age uint8 // 年龄字段 // 对应列名age
Nickname sql.NullString // 昵称字段
CreatedAt time.Time // 对应字段 created_at, 下同updated_at
UpdatedAt time.Time
// CreatedAt和UpdatedAt字段由Gorm自动管理, 用于追踪记录的创建和更新时间
}再在db-init.go的init函数中初始化数据库连接并建表,到main.go里import一下就行了:
func init() {
var err error
dsn := config.Config.DB.Serialize()
fmt.Printf("dsn: %v\n", dsn)
// user:pass@tcp(localhost:3306)/app?charset=utf8mb4&parseTime=true&loc=Local
DBConn, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect database")
}
if err = DBConn.AutoMigrate(&models.User{}); err != nil {
panic("failed to migrate database")
}
}运行效果如下:
模型定义约定再放送
主键:GORM 使用一个名为
ID的字段作为每个模型的默认主键。表名:默认情况下,GORM 将结构体名称转换为
snake_case并为表名加上复数形式。 For instance, aUserstruct becomesusersin the database, and aGormUserNamebecomesgorm_user_names.列名:GORM 自动将结构体字段名称转换为
snake_case作为数据库中的列名。时间戳字段:GORM使用字段
CreatedAt和UpdatedAt来自动跟踪记录的创建和更新时间。
Migrator接口
Migrator接口看上去是个迁移接口,但其实还集成了查看表结构的诸多API——放个传送门
比如建表、看表、查表是否存在,甚至是重命名表:
// 为 `User` 创建表
db.Migrator().CreateTable(&User{})
// 将 "ENGINE=InnoDB" 添加到创建 `User` 的 SQL 里去
db.Set("gorm:table_options", "ENGINE=InnoDB").Migrator().CreateTable(&User{})
// 检查 `User` 对应的表是否存在
db.Migrator().HasTable(&User{})
db.Migrator().HasTable("users")
// 如果存在表则删除(删除时会忽略、删除外键约束)
db.Migrator().DropTable(&User{})
db.Migrator().DropTable("users")
// 重命名表
db.Migrator().RenameTable(&User{}, &UserInfo{})
db.Migrator().RenameTable("users", "user_infos")哦,还有个看数据库呢!
db.Migrator().CurrentDatabase()查库 【都是占位的,知道就好】
查表/建表
查列/加列
查视图/定义视图
查约束、索引
我是CRUD Boy!
注
Gorm在 1.30版本后启用了泛型API,提供更简洁的API,而传统API仍能使用
下面将主要使用新版的泛型API作为演示
增(创建记录)
官方示例:
user := User{Name: "Jinzhu", Age: 18, Birthday: time.Now()}
// Create a single record
ctx := context.Background()
err := gorm.G[User](db).Create(ctx, &user) // pass pointer of data to Create
// Create with result
result := gorm.WithResult()
err := gorm.G[User](db, result).Create(ctx, &user)
user.ID // returns inserted data's primary key
result.Error // returns error
result.RowsAffected // returns inserted records count示例:
不知道是不是因为Go会自动解引用,新的表实例哪怕已经是指针,在Create的时候仍然要取地址&:
// services/user.go
// UserRegister 创建用户
//
// 参数: user *models.User 调用方自己序列化表单
// 返回: 错误信息
func UserRegister(user *models.User) (err error) {
ctx := context.Background()
return gorm.G[*models.User](DBConn).Create(ctx, &user)
}
func UserPasswordVerify(username string, password string) (err error) {
// 占位
return nil
}
// controllers/user.go
func UserRegisterAPI(c *gin.Context) {
var userReq models.User
if err := c.ShouldBind(&userReq); err != nil {
c.JSON(400, gin.H{
"msg": "参数错误",
"err": err.Error(),
})
return
}
if err := services.UserRegister(&userReq); err != nil {
c.JSON(400, gin.H{
"msg": "注册失败",
"err": err.Error(),
})
return
}
c.JSON(200, gin.H{
"msg": "注册成功",
})
}
// routers/user.go
func UserRouterInit(r *gin.Engine) {
routerGroup := r.Group("/api/v1/user")
{
routerGroup.POST("/register", controllers.UserRegisterAPI)
}
}批量Insert
users := []*User{
{Name: "Jinzhu", Age: 18, Birthday: time.Now()},
{Name: "Jackson", Age: 19, Birthday: time.Now()},
}
result := db.Create(users) // pass a slice to insert multiple row
result.Error // returns error
result.RowsAffected // returns inserted records count更多用法 【占位】

查(查询记录)
查询单条记录
ctx := context.Background()
// Get the first record ordered by primary key
user, err := gorm.G[User](db).First(ctx)
// SELECT * FROM users ORDER BY id LIMIT 1;
// Get one record, no specified order
user, err := gorm.G[User](db).Take(ctx)
// SELECT * FROM users LIMIT 1;
// Get last record, ordered by primary key desc
user, err := gorm.G[User](db).Last(ctx)
// SELECT * FROM users ORDER BY id DESC LIMIT 1;
// check error ErrRecordNotFound
errors.Is(err, gorm.ErrRecordNotFound)查询多条记录
// Get all records
result := db.Find(&users)
// SELECT * FROM users;
result.RowsAffected // returns found records count, equals `len(users)`
result.Error // returns error带条件查询
结构体交集查询
db.Where(&User{Name: "jinzhu"}, "name", "Age").Find(&users)
// SELECT * FROM users WHERE name = "jinzhu" AND age = 0;
db.Where(&User{Name: "jinzhu"}, "Age").Find(&users)
// SELECT * FROM users WHERE age = 0;Group By 聚合查询
根据某个字段对查询结果进行分组聚合,重点是聚合
注意事项
子查询中的Group By
products = &[]models.CategorifiedProduct{}
DBConn.Model(&models.Product{}).
Where("id IN (?)", IDs).
Group("category").
Omit("deleted_at").
Find(products)
你说得对,但这是Mysql 5.7+的only_full_group_by特性……
更多内容

高级查询
智能选择字段
子查询
带多个列的In查询
更新 Update
保存所有字段
Save会保存所有的字段,即使字段是零值
db.First(&user)
user.Name = "jinzhu 2"
user.Age = 100
db.Save(&user)
// UPDATE users SET name='jinzhu 2', age=100, birthday='2016-01-01', updated_at = '2013-11-17 21:34:10' WHERE id=111;只有老API才有这个操作,新API则专注于精准更新(同时仍然以空值覆盖旧值)Save is an upsert function:
- If the value contains no primary key, it performs
Create - If the value has a primary key, it first executes Update (all fields, by
Select(*)). - If
rows affected = 0after Update, it automatically falls back toCreate.
更多内容
删除 Delete
删除一条记录时,删除对象需要指定主键,否则会触发 批量删除,例如:
删除一条记录
ctx := context.Background()
// Delete by ID
err := gorm.G[Email](db).Where("id = ?", 10).Delete(ctx)
// DELETE from emails where id = 10;
// Delete with additional conditions
err := gorm.G[Email](db).Where("id = ? AND name = ?", 10, "jinzhu").Delete(ctx)
// DELETE from emails where id = 10 AND name = "jinzhu";删除多条记录
如果指定的值不包括主属性,那么 GORM 会执行批量删除,它将删除所有匹配的记录
ctx := context.Background()
// Batch delete with conditions
err := gorm.G[Email](db).Where("email LIKE ?", "%jinzhu%").Delete(ctx)
// DELETE from emails where email LIKE "%jinzhu%";软删除
传送门
如果你的模型包含了gorm.DeletedAt字段(该字段也被包含在gorm.Model中),那么该模型将会自动获得软删除的能力!
当调用Delete时,GORM并不会从数据库中删除该记录,而是将该记录的DeleteAt设置为当前时间,而后的一般查询方法将无法查找到此条记录。
// user's ID is `111`
db.Delete(&user)
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE id = 111;
// Batch Delete
db.Where("age = ?", 20).Delete(&User{})
// UPDATE users SET deleted_at="2013-10-29 10:23" WHERE age = 20;
// Soft deleted records will be ignored when querying
db.Where("age = 20").Find(&user)
// SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;事务
会话认证
哈希校验
golang.org/x/crypto/bcrypt
bcrypt包很精简,只提供了三个参数:
func CompareHashAndPassword(hashedPassword, password []byte) error- 计算并比较哈希;可能返回下面两个错误
variablesvar ErrHashTooShort = errors.New("crypto/bcrypt: hashedSecret too short to be a bcrypted password")
顾名思义,哈希值太短,不符合bcrypt的特征var ErrMismatchedHashAndPassword = errors.New("crypto/bcrypt: hashedPassword is not the hash of the given password")
哈希值不匹配
- 计算并比较哈希;可能返回下面两个错误
func GenerateFromPassword(password []byte, cost int) ([]byte, error)- 根据明文字节序列计算哈希值;可能抛出下面一个错误变量
var ErrPasswordTooLong = errors.New("bcrypt: password length exceeds 72 bytes")
明文字节序列长度大于72字节——Go中的UTF8码点一个是3字节,也就是说至多接受24个字符长的UTF8密码
- 根据明文字节序列计算哈希值;可能抛出下面一个错误变量
func Cost(hashedPassword []byte) (int, error)- 用于辅助开发者判断哪些密码需要更新
看描述感觉没什么用
包内有三个常量,用于设置哈希花费:
const (
MinCost int = 4 // the minimum allowable cost as passed in to GenerateFromPassword
MaxCost int = 31 // the maximum allowable cost as passed in to GenerateFromPassword
DefaultCost int = 10 // the cost that will actually be set if a cost below MinCost is passed into GenerateFromPassword
)示例:
package scratch
import (
"bytes"
"fmt"
"golang.org/x/crypto/bcrypt")
func bcryptTest() {
passwd := bytes.NewBufferString("12345678") // 短密码测试
fmt.Printf("消息 %v 长度为%v\n", passwd.String(), passwd.Len())
hash, err := bcrypt.GenerateFromPassword(passwd.Bytes(), bcrypt.DefaultCost)
if err != nil {
fmt.Printf("哈希摘要处理消息%v时发生错误: %v\n", passwd.String(), err)
} else {
fmt.Printf("对明文%v进行哈希摘要得到: %v\n", passwd.String(), string(hash))
}
passwd = bytes.NewBufferString("你好世界这是一个二十四个字符长的明文啦啦啦啦啦啦啦") // 长密码测试
fmt.Printf("消息 %v 长度为%v\n", passwd.String(), passwd.Len())
hash, err = bcrypt.GenerateFromPassword(passwd.Bytes(), bcrypt.DefaultCost)
if err != nil {
fmt.Printf("哈希摘要处理消息%v时发生错误: %v\n", passwd.String(), err)
} else {
fmt.Printf("对明文%v进行哈希摘要得到: %v\n", passwd.String(), string(hash))
}
/*
消息 12345678 长度为8
对明文12345678进行哈希摘要得到: $2a$10$TQJa.ic21XUneaM0vlt9rOcOB9pwiQ3baPDYUYr0E5YOGBQmidkqC
消息 你好世界这是一个二十四个字符长的明文啦啦啦啦啦啦啦 长度为75
哈希摘要处理消息你好世界这是一个二十四个字符长的明文啦啦啦啦啦啦啦时发生错误: bcrypt: password length exceeds 72 bytes
*/}