Golang GORM 实现类似于软删除的 archived_at 功能。

huangapple go评论91阅读模式
英文:

Golang GORM Implement archived_at similar to soft delete

问题

我正在使用 GORM V1 进行工作。我们有一个需求,希望添加一个类似于 GORM 的 deleted_atarchived_at 列。实体可以被归档和取消归档,默认情况下,我们希望查询 archived_at 为 NULL 的记录。

我的当前想法是利用 GORM 回调,在注册回调之前:

  1. 注册一个在 gorm:query 回调之前的回调函数。
  2. 检查模式/模型是否具有所需的 archived_at 字段。
  3. 如果有该字段,则添加必要的条件。

到这里听起来不错,但如何高效地复制 archived 的 Unscoped() 等效功能呢?

  • 获取归档记录的惯用方式是什么?如何指示在必要的回调函数中是否应添加 archived_at 列?
  • 我还将遇到这样一种情况,我只想获取已归档的记录(archived_at 不为 NULL 的记录)。

编辑 - 我的用例是同时使用 deleted_atarchived_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

  1. To register a callback before gorm:query callback.
  2. Check if the schema/model has required archived_at field.
  3. 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_atdeleted_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)
}

huangapple
  • 本文由 发表于 2023年2月21日 11:54:55
  • 转载请务必保留本文链接:https://go.coder-hub.com/75516157.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定