英文:
Golang GORM Implement archived_at similar to soft delete
问题
我正在使用 GORM V1 进行工作。我们有一个需求,希望添加一个类似于 GORM 的 deleted_at
的 archived_at
列。实体可以被归档和取消归档,默认情况下,我们希望查询 archived_at
为 NULL 的记录。
我的当前想法是利用 GORM 回调,在注册回调之前:
- 注册一个在 gorm:query 回调之前的回调函数。
- 检查模式/模型是否具有所需的 archived_at 字段。
- 如果有该字段,则添加必要的条件。
到这里听起来不错,但如何高效地复制 archived 的 Unscoped() 等效功能呢?
- 获取归档记录的惯用方式是什么?如何指示在必要的回调函数中是否应添加
archived_at
列? - 我还将遇到这样一种情况,我只想获取已归档的记录(
archived_at
不为 NULL 的记录)。
编辑 - 我的用例是同时使用 deleted_at
和 archived_at
字段,而不是将一个字段替换为另一个字段。我希望保留软删除的能力,并添加归档实体的能力。用户可能只是被归档,然后可能被删除(软删除)。
英文:
I am working with GORM V1. I have a requirement where we would want to add an archived_at
column similar to GORMs deleted_at
. The entities could be archived and unarchived and by default we would want to query records with archived_at
is NULL.
My current thought is to leverage GORM callbacks, to register a callback
- To register a callback before gorm:query callback.
- Check if the schema/model has required archived_at field.
- If it does have the field add necessary condition.
This sounds good until here but how do I efficiently replicate the Unscoped() equivalent of archived.
- What would be the idiomatic way of fetching archived records as well? How do I indicate whether the
archived_at
column should be added inside the necessary callback? - I am also going to have a case where I want to fetch just the archived records (where
archived_at
IS NOT NULL).
EDIT - my use case is to use both deleted_at
and archived_at
fields and not use one in place of another. I want to retain the ability of soft deleting along with adding the ability to archive an entity. A user could be just archived, and then may be deleted (soft delete).
答案1
得分: 2
邀请反馈。这是我目前想出来的。
callback.go
package db
import (
"fmt"
"reflect"
"gorm.io/gorm"
)
const (
GormSettingKeyUnscopeArchive = "unscope_archive"
StructFieldNameArchivedAt = "ArchivedAt"
)
// ArchivedQueryCallback - 如果被查询的模型满足以下条件,有条件地添加 "WHERE archived_at IS NULL"
// 1. 由 StructFieldNameArchivedAt 表示的结构字段
// 2. GORM 实例设置 GormSettingKeyUnscopeArchive,请参阅 UnscopeArchive
func ArchivedQueryCallback(db *gorm.DB) {
// 检查模型是否为指针并具有间接的结构类型
if db.Statement.Model != nil &&
reflect.TypeOf(db.Statement.Model).Kind() == reflect.Ptr &&
reflect.Indirect(reflect.ValueOf(db.Statement.Model)).Kind() == reflect.Struct {
stmt := &gorm.Statement{DB: db}
parseErr := stmt.Parse(db.Statement.Model)
if parseErr != nil {
panic(parseErr)
}
if _, archivedAtExists := stmt.Schema.FieldsByName[StructFieldNameArchivedAt]; archivedAtExists {
v, ok := db.InstanceGet(GormSettingKeyUnscopeArchive)
if ok {
if v == true {
return
}
}
db.Where(fmt.Sprintf("%s IS NULL", stmt.Schema.FieldsByName[StructFieldNameArchivedAt].DBName))
}
}
}
scopes.go
// UnscopeArchive - 为键 GormSettingKeyUnscopeArchive 设置为 true
func UnscopeArchive(db *gorm.DB) *gorm.DB {
db = db.InstanceSet(GormSettingKeyUnscopeArchive, true)
return db
}
main.go
type User {
ID string `gorm:"primary_key" json:"id" valid:"uuidv4, optional"`
CreatedAt time.Time `valid:"-" json:"created_at"`
UpdatedAt time.Time `valid:"-" json:"-"`
DeletedAt gorm.DeletedAt `sql:"index" valid:"-" json:"-"`
ArchivedAt time.Time
}
var user []User
ctx := context.Background()
dbClient := InitializeGORM() //helper
_ := dbClient.WithContext(ctx).Find(&user).Error // SELECT * FROM users WHERE deleted_at IS NULL AND archived_at IS NULL;
_ := dbClient.WithContext(ctx).Scopes(UnscopeArchive).Find(&user).Error // SELECT * FROM users WHERE deleted_at IS NULL;
英文:
Inviting feedback. This is what I have currently come up with.
callback.go
package db
import (
"fmt"
"reflect"
"gorm.io/gorm"
)
const (
GormSettingKeyUnscopeArchive = "unscope_archive"
StructFieldNameArchivedAt = "ArchivedAt"
)
// ArchivedQueryCallback - conditionally adds "WHERE archived_at IS NULL" if the Model being queried has the following
// 1. Struct field represented by StructFieldNameArchivedAt
// 2. GORM instance setting GormSettingKeyUnscopeArchive, See UnscopeArchive
func ArchivedQueryCallback(db *gorm.DB) {
// Check if Model is a pointer and has an indirect struct type
if db.Statement.Model != nil &&
reflect.TypeOf(db.Statement.Model).Kind() == reflect.Ptr &&
reflect.Indirect(reflect.ValueOf(db.Statement.Model)).Kind() == reflect.Struct {
stmt := &gorm.Statement{DB: db}
parseErr := stmt.Parse(db.Statement.Model)
if parseErr != nil {
panic(parseErr)
}
if _, archivedAtExists := stmt.Schema.FieldsByName[StructFieldNameArchivedAt]; archivedAtExists {
v, ok := db.InstanceGet(GormSettingKeyUnscopeArchive)
if ok {
if v == true {
return
}
}
db.Where(fmt.Sprintf("%s IS NULL", stmt.Schema.FieldsByName[StructFieldNameArchivedAt].DBName))
}
}
}
scopes.go
// UnscopeArchive - sets a true value for the key GormSettingKeyUnscopeArchive
func UnscopeArchive(db *gorm.DB) *gorm.DB {
db = db.InstanceSet(GormSettingKeyUnscopeArchive, true)
return db
}
main.go
type User {
ID string `gorm:"primary_key" json:"id" valid:"uuidv4, optional"`
CreatedAt time.Time `valid:"-" json:"created_at"`
UpdatedAt time.Time `valid:"-" json:"-"`
DeletedAt gorm.DeletedAt `sql:"index" valid:"-" json:"-"`
ArchivedAt time.Time
}
var user []User
ctx := context.Background()
dbClient := InitializeGORM() //helper
_ := dbClient.WithContext(ctx).Find(&user).Error // SELECT * FROM users WHERE deleted_at IS NULL AND archived_at IS NULL;
_ := dbClient.WithContext(ctx).Scopes(UnscopeArchive).Find(&user).Error // SELECT * FROM users WHERE deleted_at IS NULL;
答案2
得分: 0
没有必要使用回调函数,只需将ArchivedAt
定义为DeletedAt
,然后使用Unscoped方法。
以下是一个示例:
type User struct {
Name string
Age int
ArchivedAt gorm.DeletedAt `gorm:"index"`
}
func main() {
db := config.CreateMysql()
db.AutoMigrate(User{})
us := []User{}
db.Debug().Unscoped().Find(&us)
//SELECT * FROM `users`
fmt.Printf("%v\n", us)
us2 := []User{}
db.Debug().Find(&us2)
//SELECT * FROM `users` WHERE `users`.`archived_at` IS NULL
fmt.Printf("%v\n", us)
}
如果你想要查询archived_at IS NOT NULL
,可以使用以下回调函数:
type User struct {
Name string
Age int
ArchivedAt gorm.DeletedAt `gorm:"index"`
}
func MyQuery(db *gorm.DB) {
if db.Statement.Unscoped {
// archived_at is NULL
fmt.Printf("append archived_at is not NULL\n")
db = db.Statement.Where("archived_at is not NULL")
} else {
// default select all
fmt.Printf("MyQuery\n")
}
}
func main() {
db := config.CreateMysql()
db.Callback().Query().Before("gorm:query").Register("my_plugin:after_query", MyQuery)
db.AutoMigrate(User{})
us := []User{}
db.Debug().Unscoped().Find(&us)
// SELECT * FROM `users` WHERE archived_at is not NULL
fmt.Printf("%v\n", us)
us2 := []User{}
db.Debug().Find(&us2)
// SELECT * FROM `users` WHERE `users`.`archived_at` IS NULL
fmt.Printf("%v\n", us)
}
如果你想要同时查询archived_at
和deleted_at
,可以使用以下代码:
const (
fc_all = iota // 0
fc_null
fc_notnull
)
const FindCondition = "FindCondition"
type User struct {
Name string
Age int
ArchivedAt sql.NullTime `gorm:"index"`
}
func MyQuery(db *gorm.DB) {
if c, ok := db.Get(FindCondition); ok {
switch c {
case fc_null:
db = db.Statement.Where("archived_at is NULL")
case fc_notnull:
db = db.Statement.Where("archived_at is not NULL")
}
}
}
func main() {
db := config.CreateMysql()
db.Callback().Query().Before("gorm:query").Register("my_plugin:after_query", MyQuery)
db.AutoMigrate(User{})
us := []User{}
db.Debug().Set(FindCondition, fc_notnull).Find(&us)
// SELECT * FROM `users` WHERE `users`.`archived_at` IS NOT NULL
fmt.Printf("%v\n", us)
us2 := []User{}
db.Debug().Set(FindCondition, fc_null).Find(&us2)
// SELECT * FROM `users` WHERE `users`.`archived_at` IS NULL
fmt.Printf("%v\n", us)
}
提示:hooks比回调函数更好用。
英文:
there is no need callbacks...
just defined the ArchivedAt
as DeletedAt
Unscoped will used
follow is a demo:
type User struct {
Name string
Age int
ArchivedAt gorm.DeletedAt `gorm:"index"`
}
func main() {
db := config.CreateMysql()
//db.Callback().Query().Before("gorm:query").Register("my_plugin:after_query", MyQuery)
db.AutoMigrate(User{})
us := []User{}
db.Debug().Unscoped().Find(&us)
//SELECT * FROM `users`
fmt.Printf("%v\n", us)
us2 := []User{}
db.Debug().Find(&us2)
//SELECT * FROM `users` WHERE `users`.`archived_at` IS NULL
fmt.Printf("%v\n", us)
}
if you want where archived_at IS NOT NULL, you can use callbacks like follow:
type User struct {
Name string
Age int
ArchivedAt gorm.DeletedAt `gorm:"index"`
}
func MyQuery(db *gorm.DB) {
if db.Statement.Unscoped {
//archived_at is NULL
fmt.Printf("append archived_at is not NULL\n")
db = db.Statement.Where("archived_at is not NULL")
} else {
//default select all
fmt.Printf("MyQuery\n")
}
}
func main() {
db := config.CreateMysql()
db.Callback().Query().Before("gorm:query").Register("my_plugin:after_query", MyQuery)
db.AutoMigrate(User{})
us := []User{}
db.Debug().Unscoped().Find(&us)
//SELECT * FROM `users` WHERE archived_at is not NULL
fmt.Printf("%v\n", us)
us2 := []User{}
db.Debug().Find(&us2)
//SELECT * FROM `users` WHERE `users`.`archived_at` IS NULL
fmt.Printf("%v\n", us)
}
ps: hooks is better than callbacks
if want both archived_at and deleted_at
const (
fc_all = iota //0
fc_null
fc_notnull
)
const FindCondition = "FindCondition"
type User struct {
Name string
Age int
ArchivedAt sql.NullTime `gorm:"index"`
}
func MyQuery(db *gorm.DB) {
if c, ok := db.Get(FindCondition); ok {
switch c {
case fc_null:
db = db.Statement.Where("archived_at is NULL")
case fc_notnull:
db = db.Statement.Where("archived_at is not NULL")
}
}
}
func main() {
db := config.CreateMysql()
db.Callback().Query().Before("gorm:query").Register("my_plugin:after_query", MyQuery)
db.AutoMigrate(User{})
us := []User{}
db.Debug().Set(FindCondition, fc_notnull).Find(&us)
//SELECT * FROM `users` WHERE `users`.`archived_at` IS NOT NULL
fmt.Printf("%v\n", us)
us2 := []User{}
db.Debug().Set(FindCondition, fc_null).Find(&us2)
//SELECT * FROM `users` WHERE `users`.`archived_at` IS NULL
fmt.Printf("%v\n", us)
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论