Search code examples
goenumsgo-gorm

how to define custom sql type enum field


I want to define custom sql type enum like this:

package db

import (
    "database/sql/driver"
    "github.com/spf13/cast"
)

type Enum int64

func (e Enum) Value() (driver.Value, error) {
    return int64(e), nil
}

func (e *Enum) Scan(v any) error {
    *e = Enum(cast.ToInt64(v))
    return nil
}

And I want use the following:

package db_test

import (
    "db"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "testing"
)

type StatusEnum = db.Enum

const (
    StatusEnumEnable StatusEnum = iota
    StatusEnumDisable
)

// this is error because this type alias can not define new method
//func (e *StatusEnum) String() string{
//  if v,ok := StatusEnumMap[*e];ok{
//      return v
//  }
//  return "unknown"
//}

var StatusEnumMap = map[StatusEnum]string{
    StatusEnumEnable:  "Enable",
    StatusEnumDisable: "Disable",
}

type module struct {
    Status StatusEnum
}

func TestEnum(t *testing.T) {
    db, _ := gorm.Open(sqlite.Open(":memory:"))

    db.AutoMigrate(&module{})

    db.Model(&module{}).Create(&module{Status: StatusEnumDisable})

    var out *module
    // SELECT * FROM `modules` WHERE status=1 ORDER BY `modules`.`status` LIMIT 1
    db.Debug().Model(&module{}).Where("status=?", StatusEnumDisable).First(&out)

    t.Log(out)
    t.Log(StatusEnumMap[out.Status])
    //t.Log(out.Status.String())
}

Now I'm using it like this StatusEnumMap[EnumValue] when I want the string format. I think value.String() is more elegant and simple. fmt formatting can also print strings instead of numbers.

How can I better implement this feature? Thank you for your suggestions.

The above question has been answered by @Zeke Lu.

Another little problem, If I did not used Valuer and delete the String method, the sql condition status=1 will be status="1", why? Whether the query performance of sql will be affected if the type inconsistency in mysql.

The second problem:just is gorm debug info. Actual execute sql is status=1. See gorm-issue


Solution

  • It seems that you don't need to implement the Valuer and Scanner interfaces if the underlying type is int64. If that's true, the type Enum is not needed. The following example works without Enum:

    Note: I modified your demo slightly to implement the Stringer interface on type StatusEnum instead of *StatusEnum.

    package db_test
    
    import (
        "testing"
    
        "gorm.io/driver/sqlite"
        "gorm.io/gorm"
    )
    
    type StatusEnum int64
    
    const (
        StatusEnumEnable StatusEnum = iota
        StatusEnumDisable
    )
    
    var StatusEnumMap = map[StatusEnum]string{
        StatusEnumEnable:  "Enable",
        StatusEnumDisable: "Disable",
    }
    
    func (e StatusEnum) String() string {
        if v, ok := StatusEnumMap[e]; ok {
            return v
        }
        return "unknown"
    }
    
    type module struct {
        Status StatusEnum
    }
    
    func TestEnum(t *testing.T) {
        db, _ := gorm.Open(sqlite.Open(":memory:"))
    
        db.AutoMigrate(&module{})
    
        db.Model(&module{}).Create(&module{Status: StatusEnumDisable})
    
        var out *module
        // SELECT * FROM `modules` WHERE status=1 ORDER BY `modules`.`status` LIMIT 1
        db.Debug().Model(&module{}).Where("status=?", StatusEnumDisable).First(&out)
    
        t.Log(out)
        t.Logf("status value: %d, status string: %s", out.Status, out.Status)
    }
    

    The output is:

    &{Disable}
    status value: 1, status string: Disable
    

    Please note that the first one is &{Disable} now. It's &{1} before.