2

I know that using custom types is a common question, but bear with me...

I would like to define a custom type 'ConnectionInfo' (see below):

type DataSource struct {
    gorm.Model

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

I would like to restrict ConnectionInfo to be one of a limited number of types, i.e.:

type ConnectionInfo interface {
    PostgresConnectionInfo | MySQLConnectionInfo
}

How can I do this?

My progress thus far:

I defined a ConnectionInfo interface (I now know this is invalid in GORM, but how do I get around it?)

type ConnectionInfo interface {
    IsConnectionInfoType() bool
}

I've then implemented this interface with two types (and implemented the scanner and valuer interfaces) like so:

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

But of course I get the following error:

unsupported data type: /models.ConnectionInfo

blackgreen
  • 34,072
  • 23
  • 111
  • 129
Charlie Clarke
  • 177
  • 1
  • 9
  • Yes I'm aware of that - how can I get around this? – Charlie Clarke Nov 23 '22 at 15:34
  • 2
    I'm afraid there's no such a fancy feature like in other languages that supports polymorfic ORMs. Here I'd implement either 2 fields (only one populated at a time) and using value of `DataSource.Type` to distinguish which field to look into. Or I'd use additional single string field where I'd serialize/deserializethe connection info to/from, but I'd need to use `AfterFind` hook defined on `DataSource` that would look into `Type` field and according to its value it would deserialize the json string into `PostgresConnectionInfo` or `MySQLConnectionInfo`. Similar for serialization via`BeforeSave. – bambula Nov 23 '22 at 16:03
  • *forgot to say that the string field would contain json. And that the `ConnectionInfo` field would need to be ignored by gorm using `gorm:"-"`.Quite hacky solution :/ – bambula Nov 23 '22 at 16:04
  • I notice that the GORM docs do mention support for polymorphism, but it doesn't provide much information on how to use it https://gorm.io/docs/has_one.html#Polymorphism-Association – Charlie Clarke Nov 23 '22 at 16:14
  • Does polymorphism apply if the structure of my ConnectionInfo types varies? I.e. the details needed to connect to a postgres and an influxdb will be different. – Charlie Clarke Nov 23 '22 at 16:30
  • Does this answer your question? [Best practice for unions in Go](https://stackoverflow.com/questions/21553398/best-practice-for-unions-in-go) – Shahriar Ahmed Nov 24 '22 at 08:09
  • @ShahriarAhmed this does mimic a union types behaviour, but the problem with that is an interface is not a valid type to be passed to GORM as it doesn't know what table(s) and fields to create from that. – Charlie Clarke Nov 24 '22 at 09:37

1 Answers1

2

Instead of using this union, you can approach this way GITHUB LINK. You can clone these repo and run the code. This is working.

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

Shahriar Ahmed
  • 502
  • 4
  • 11
  • Wow, thank you so much for your reply, I really appreciate the effort you put into this. I shall implement this tomorrow and have a think about how I can ensure type safety when creating a ConnectionInfo type. I'm thinking of perhaps using an abstract factory pattern (https://refactoring.guru/design-patterns/abstract-factory/go/example) to create ConnectionInfo - what do you think? – Charlie Clarke Nov 24 '22 at 20:36
  • I've updated the original post to extend on what you've shown me - thank you again. – Charlie Clarke Nov 24 '22 at 22:35