【问题标题】:How to mimic a union type如何模仿联合类型
【发布时间】:2022-11-25 06:54:03
【问题描述】:

我知道使用自定义类型是一个常见问题,但请耐心等待...

我想定义一个自定义类型“ConnectionInfo”(见下文):

type DataSource struct {
    gorm.Model

    Name           string
    Type           DataSourceType `sql:"type:ENUM('POSTGRES')" gorm:"column:data_source_type"`
    ConnectionInfo ConnectionInfo `gorm:"embedded"`
}

我想将 ConnectionInfo 限制为有限数量的类型之一,即:

type ConnectionInfo interface {
    PostgresConnectionInfo | MySQLConnectionInfo
}

我怎样才能做到这一点?

到目前为止我的进展:

我定义了一个 ConnectionInfo 接口(我现在知道这在 GORM 中是无效的,但我该如何绕过它?)

type ConnectionInfo interface {
    IsConnectionInfoType() bool
}

然后我用两种类型实现了这个接口(并实现了扫描器和估价器接口),如下所示:

type PostgresConnectionInfo struct {
    Host     string
    Port     int
    Username string
    Password string
    DBName   string
}

func (PostgresConnectionInfo) IsConnectionInfoType() bool {
    return true
}

func (p *PostgresConnectionInfo) Scan(value interface{}) error {
    bytes, ok := value.([]byte)
    if !ok {
        return fmt.Errorf("failed to unmarshal the following to a PostgresConnectionInfo value: %v", value)
    }

    result := PostgresConnectionInfo{}
    if err := json.Unmarshal(bytes, &result); err != nil {
        return err
    }
    *p = result

    return nil
}

func (p PostgresConnectionInfo) Value() (driver.Value, error) {
    return json.Marshal(p)
}

但是我当然会收到以下错误:

unsupported data type: <myproject>/models.ConnectionInfo

工作答案

感谢 Shahriar Ahmed,我有一个惯用的解决方案,我只是想添加一些额外的信息来说明我是如何(尝试)在处理模型包之外的 ConnectionInfo 类型时确保一些类型安全的。

我的 ConnectionInfo 字段现在看起来像这样:

type DataSource struct {
    gorm.Model

    ConnectionInfo connectionInfo `gorm:"type:jsonb;not null"`
}

它的类型是 Shahriar 的建议,包括每个也实现 Scanner 和 Valuer 接口的子类型/变体:

type connectionInfo struct {
    Postgres *PostgresConnectionInfo `gorm:"-" json:"postgres,omitempty"`
    MySQL    *MySQLConnectionInfo    `gorm:"-" json:"mysql,omitempty"`
}

func (c *connectionInfo) Scan(src any) error {
    switch src := src.(type) {
    case nil:
        return nil
    case []byte:
        var res connectionInfo
        err := json.Unmarshal(src, &res)
        *c = res
        return err
    default:
        return fmt.Errorf("unable to scan type %T into connectionInfo", src)
    }
}

func (c connectionInfo) Value() (driver.Value, error) {
    return json.Marshal(c)
}

但是,我没有导出 'connectionInfo' 类型(通过使用小写的 'c'),并且我创建了一个导出的 'ConnectionInfo' 接口,'PostgresConnectionInfo' 和 'MySQLConnectionInfo' 类型实现了该接口:

type ConnectionInfo interface {
    IsConnectionInfoType() bool
}

type PostgresConnectionInfo struct {
    Host     string `json:"host" binding:"required"`
    Port     int    `json:"port" binding:"required"`
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
    DBName   string `json:"dbName" binding:"required"`
}

func (PostgresConnectionInfo) IsConnectionInfoType() bool {
    return true
}

当想要引用抽象类型时,我将使用 ConnectionInfo,然后将其传递给我的模型包,该包将使用下面的方法获取具体类型并实例化“connectionInfo”类型:

func getConcreteConnectionInfo(connInfo ConnectionInfo) connectionInfo {
    switch v := connInfo.(type) {
    case *PostgresConnectionInfo:
        return connectionInfo{Postgres: v}
    case *MySQLConnectionInfo:
        return connectionInfo{MySQL: v}
    default:
        panic(fmt.Sprintf("Unknown connection info type: %T", connInfo))
    }
}

【问题讨论】:

  • 是的,我知道这一点 - 我该如何解决这个问题?
  • 恐怕没有像其他支持多态 ORM 的语言那样奇特的功能。在这里,我将实现 2 个字段(一次只填充一个)并使用 DataSource.Type 的值来区分要查看的字段。或者我会使用额外的单个字符串字段,在其中序列化/反序列化连接信息到/从中,但我需要使用在 DataSource 上定义的 AfterFind 钩子,它会查看 Type 字段并根据其value 它会将 json 字符串反序列化为 PostgresConnectionInfoMySQLConnectionInfo。类似于通过`BeforeSave 进行序列化。
  • *忘了说字符串字段将包含 json。并且 ConnectionInfo 字段需要被 gorm 使用 gorm:"-" 忽略。相当 hacky 的解决方案:/
  • 我注意到 GORM 文档确实提到了对多态性的支持,但它没有提供太多关于如何使用它的信息gorm.io/docs/has_one.html#Polymorphism-Association
  • 如果我的 ConnectionInfo 类型的结构不同,多态性是否适用? IE。连接到 postgres 和 influxdb 所需的详细信息将有所不同。

标签: go go-gorm


【解决方案1】:

除了使用这个 union,您还可以使用这种方式 GITHUB LINK。您可以克隆这些存储库并运行代码。这是工作。

package storage

import (
    "database/sql/driver"
    "encoding/json"
    "fmt"
    "log"

    "gorm.io/driver/sqlite"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

type DataSourceType string

const (
    POSTGRES DataSourceType = "POSTGRES"
    MYSQL    DataSourceType = "MYSQL"
)

type PostgresConnectionInfo struct {
    Host     string
    Port     int
    Username string
    Password string
    DBName   string
}

type MySQLConnectionInfo struct {
    Host     string
    Port     int
    Username string
    Password string
    DBName   string
}

type ConnectionInfo struct {
    Postgres *PostgresConnectionInfo `gorm:"-" json:"postgres,omitempty"`
    Mysql    *MySQLConnectionInfo    `gorm:"-" json:"mysql,omitempty"`
}
type DataSource struct {
    gorm.Model
    Name           string
    Type           DataSourceType `sql:"type:ENUM('POSTGRES')" gorm:"column:data_source_type"`
    ConnectionInfo ConnectionInfo `gorm:"type:json" `
}

func (a *ConnectionInfo) Scan(src any) error {
    switch src := src.(type) {
    case nil:
        return nil
    case []byte:
        var res ConnectionInfo
        err := json.Unmarshal(src, &res)
        *a = res
        return err

    default:
        return fmt.Errorf("scan: unable to scan type %T into struct", src)
    }

}

func (a ConnectionInfo) Value() (driver.Value, error) {
    ba, err := json.Marshal(a)
    return ba, err
}

func GormTest2() {
    db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    if err != nil {
        log.Fatal("could not open database")
    }
    err = db.AutoMigrate(&DataSource{})
    if err != nil {
        log.Fatal("could not migrate database")
    }
    createTestData1(db)
    fetchData1(db)
}

func createTestData1(db *gorm.DB) {
    ds := []DataSource{
        {
            Name: "Postgres",
            Type: POSTGRES,
            ConnectionInfo: ConnectionInfo{
                Postgres: &PostgresConnectionInfo{
                    Host:     "localhost",
                    Port:     333,
                    Username: "sdlfj",
                    Password: "sdfs",
                    DBName:   "sdfsd",
                },
            },
        },
        {
            Name: "Mysql",
            Type: MYSQL,
            ConnectionInfo: ConnectionInfo{
                Mysql: &MySQLConnectionInfo{
                    Host:     "localhost",
                    Port:     333,
                    Username: "sdlfj",
                    Password: "sdfs",
                    DBName:   "sdfsd",
                },
            },
        },
    }
    err := db.Create(&ds).Error
    if err != nil {
        log.Println("failed to create data")
    }
}

func fetchData1(db *gorm.DB) {
    var dsList []DataSource
    if err := db.Find(&dsList).Error; err != nil {
        log.Println("failed to load data")
    }
    log.Println(dsList)
}

【讨论】:

  • 哇,非常感谢您的回复,我非常感谢您为此付出的努力。我将在明天实现它,并考虑在创建 ConnectionInfo 类型时如何确保类型安全。我正在考虑使用抽象工厂模式 (refactoring.guru/design-patterns/abstract-factory/go/example) 来创建 ConnectionInfo - 你怎么看?
  • 我已经更新了原始帖子以扩展您向我展示的内容 - 再次感谢您。
猜你喜欢
  • 2019-04-21
  • 1970-01-01
  • 2019-04-25
  • 1970-01-01
  • 1970-01-01
  • 2022-10-02
  • 1970-01-01
  • 2011-03-31
相关资源
最近更新 更多