GORM
gorm
Golang 的 ORM 实现 基于 Entity 模型
- go-gorm/gorm
- 执行过程
- 构建 Statement 对象
- 查询 - 执行 query 回调
- 几乎所有实际操作都是通过 callback 串联起来的
- callbacks 管理 processor 组
- processor 下有实际回调处理
- 回调 操作 Statement 上关联的值对象进行查询修改
func(db *gorm.DB)
db.Statement.ReflectValue
- 结果数据
- 入口 processor - 调用后开始实际执行
- create
- gorm:begin_transaction
- gorm:before_create
- gorm:save_before_associations
- gorm:create
- gorm:save_after_associations
- gorm:after_create
- gorm:commit_or_rollback_transaction
- query
- gorm:query
- gorm:preload
- gorm:after_query
- update
- 与 create 类似
- gorm:setup_reflect_value - 在 begin_transaction 后
- delete
- gorm:begin_transaction
- gorm:before_delete
- gorm:delete_before_associations
- gorm:delete
- gorm:after_delete
- gorm:commit_or_rollback_transaction
- row
- gorm:row
- raw
- gorm:raw
- create
- RegisterDefaultCallbacks - 默认 callback 注册
- 能看得出来执行过程
- 关系 - Relationship
- 创建时会自动创建关联
- 其他操作可选 -
Select(clause.Associations)
Select("Profile")
- 也可以直接针对关联进行操作 -
Association("Profile")
- 关系处理方式分为 JoinTable 和 Reference
- gorm:preload
- 多层级 Preload 会按序 - 例如
Preload("Profile.Address")
会分成Profile
和Profile.Address
两次完成 - 如果关系不存在,找不到 Relationship 目前会 NPE
- 多层级 Preload 会按序 - 例如
- 语句构建
clause.Expression{ Build(Builder) }
- 表示任意语句clause.Builder
- WriteString, AddVar, WriteQuoted - 构建上下文clause.Interface
- 带 Name 的 表达式 - 可以被合并和替换 - 例如 LIMIT, SELECTclause.Table
,clause.Column
- Relationship 关联的 Schema 可能和实际 Schema 不同 - 导致无法 Preload
- Embed Struct 也是当作 embedded 来处理的,只不过没有前缀
- 现在嵌套多层解析的 schema 会有问题 - #3964
- 参考
- pilagod/gorm-cursor-paginator
- go-gorm/datatypes - gorm.io/datatypes
- 实现了其他数据类型 - 例如 JSON
go get -u gorm.io/gorm
# 基于 cgo 的 sqlite
go get -u gorm.io/driver/sqlite
# 不需要 cgo
go get -u github.com/glebarez/sqlite
go get -u github.com/glebarez/go-sqlite
go get -u gorm.io/driver/postgres
caution
- 不建议字段设置为 default:null
- OnConflict UpdateAll 不会处理
- Null 无法被读取为 非 Ptr 或 非 sql.Null
tag | mean | e.g. |
---|---|---|
column: | 列名 | |
type: | ||
size: | ||
primaryKey | ||
unique | ||
default: | ||
precision: | ||
scale: | ||
not null | ||
autoIncrement | ||
autoIncrementIncrement: | 自增步长 | |
embedded | ||
embeddedPrefix: | ||
autoCreateTime:nano/milli | ||
autoUpdateTime:nano/milli | ||
index | ||
uniqueIndex | ||
check: | ||
<- | 可权限: create,update,false | gorm:"<-:create" |
-> | 读权限 | gorm:"->:false" |
- | 忽略字段 | |
comment: | ||
foreignKey: | 外建名字 - 默认 {ForeignModel}ID | |
references: | 被关联对象字段 | |
polymorphic: | ||
polymorphicValue: | ||
many2many: | join table | |
joinForeignKey: | ||
joinReferences: | ||
constraint: | OnUpdate,OnDelete | gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;" |
tip
- Tag 不分大小写
- index
- unique
- priority
- class, type, where, comment, expression, sort, collate, option
- composite
- uniqueIndex
特殊值
const (
PrimaryKey string = "~~~py~~~" // primary key
CurrentTable string = "~~~ct~~~" // current table
Associations string = "~~~as~~~" // associations
)
import (
"gorm.io/driver/sqlite"
_ "github.com/glebarez/sqlite"
"gorm.io/gorm"
)
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
// 直接调用 processor
func TestPreloadOnly(t *testing.T){
// 模型只包含主键
var m models.User
m.ID = 1
// 通常逻辑 - 但构造出来的 stmt 包含基础信息
stmt := db.Model(&m).Preload("Profile")
// 填充需要的信息 - 正常逻辑这些字段会被填充
stmt.Statement.Dest = stmt.Statement.Model
stmt.Statement.ReflectValue = reflect.ValueOf(stmt.Statement.Dest).Elem()
assert.NoError(t, stmt.Statement.Parse(stmt.Statement.Model))
// 直接调用 preload
callbacks.Preload(stmt)
assert.NoError(t, stmt.Error)
// 字段被成功 preload
fmt.Println(m.Profile)
}
- 创建、更新、删除 默认在事务中执行
- SkipDefaultTransaction 可关闭 - 约 30% 性能
- Tag 不区分大小写
// 查询条件与数据不同但可以一次操作
db.Where(User{Name: "non_existing"}).Assign(User{Age: 20}).FirstOrCreate(&user)
Model
import (
"github.com/google/uuid"
"github.com/lib/pq"
)
type Post struct {
ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4()"`
Title string
Tags pq.StringArray `gorm:"type:text[]"` // 需要指定类型
}
钩子
- 创建和更新
- BeforeSave
- BeforeCreate/BeforeUpdate
- AfterCreate/AfterUpdate
- AfterSave
- 删除
- BeforeDelete
- AfterDelete
- 查询
- AfterFind
// 操作当前的 Statement 可修改语句
func (u *User) BeforeCreate(tx *gorm.DB) error {
// 通过 tx.Statement 修改当前操作,例如:
tx.Statement.Select("Name", "Age")
tx.Statement.AddClause(clause.OnConflict{DoNothing: true})
// tx 是带有 `NewDB` 选项的新会话模式
// 基于 tx 的操作会在同一个事务中,但不会带上任何当前的条件
err := tx.First(&role, "name = ?", user.Role).Error
// SELECT * FROM roles WHERE name = "admin"
// ...
return err
}
- https://github.com/go-gorm/gorm/issues/3611#issuecomment-729673788
- 使用 BeforeCreate 做 upsert
Upsert
// 更新部分字段
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}),
}).Create(&users)
// 从新映射
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
DoUpdates: clause.Assignments(map[string]interface{}{
"role": "user",
"count": gorm.Expr("GREATEST(count, VALUES(count))"),
}),
}).Create(&users)
// 更新所有
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}},
UpdateAll: true,
}).Create(&users)
DryRun 查看执行语句
stmt := db.Session(&gorm.Session{DryRun: true}).First(&user, 1).Statement
stmt.SQL.String() //=> SELECT * FROM `users` WHERE `id` = $1 ORDER BY `id`
stmt.Vars //=> []interface{}{1}
// 可生成完整 SQL - 不要用于查询,有 SQL 注入风险
db.Dialector.Explain(stmt.SQL.String(), stmt.Vars...)
跳回钩子处理
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)
多态
- 默认多态值为表名
type Cat struct {
ID int
Name string
Toy Toy `gorm:"polymorphic:Owner;"`
}
type Dog struct {
ID int
Name string
Toy Toy `gorm:"polymorphic:Owner;"`
}
type Toy struct {
ID int
Name string
OwnerID int
OwnerType string
}
使用 UID 实现类似多态
type Model struct {
ID int
UID string // uuid
}
type Cat struct {
Model
Name string
Toy Toy `gorm:"polymorphic:Owner;"`
}
type Dog struct {
Model
Name string
Toy Toy `gorm:"polymorphic:Owner;"`
}
type Toy struct {
ID int
Name string
OwnerID int
OwnerType string
OwnerUID string
}
添加 SQL 函数 默认值
type User struct {
ID int `sql:"DEFAULT:myfunction"`
XID int `sql:"type:bigint; DEFAULT:id_generator()"`
CreatedAt time.Time `sql:"DEFAULT:current_timestamp"`
}
避免插入空值
type User struct {
gorm.Model
Email string `gorm:"unique;not null;type:varchar(100);default:null"`
}
ErrRecordNotFound
- First, Last, Take