Search code examples
gogo-gorm

Golang GORM Implement archived_at similar to soft delete


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).


Solution

  • 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;