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: <myproject>/models.ConnectionInfo
WORKING ANSWER
Thanks to Shahriar Ahmed, I have an idiomatic solution, I just wanted to add a little extra on how I am (trying) to ensure a bit of type safety when handling the ConnectionInfo type outside of the models package.
My ConnectionInfo field now looks like so:
type DataSource struct {
gorm.Model
ConnectionInfo connectionInfo `gorm:"type:jsonb;not null"`
}
Its type is how Shahriar advised and includes each of the subtypes/variants that also implement the Scanner and Valuer interfaces:
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)
}
However, I have not exported the 'connectionInfo' type (by using a lowercase 'c'), and I've created an exported 'ConnectionInfo' interface which the 'PostgresConnectionInfo' and 'MySQLConnectionInfo' types implement:
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
}
When wanting to reference the abstract type, I will use ConnectionInfo and then pass this to my models package which will use the below get the concrete type and instantiate a 'connectionInfo' type:
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))
}
}
CodePudding user response:
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)
}