The customized data type has to implement the Scanner and interfaces, so GORM knowns to how to receive/save it into the database
For example:
There are many third party packages implement the /Valuer
interface, which can be used with GORM together, for example:
import (
"github.com/google/uuid"
"github.com/lib/pq"
)
type Post struct {
ID uuid.UUID `gorm:"type:uuid;default:uuid_generate_v4()"`
Title string
Tags pq.StringArray `gorm:"type:text[]"`
}
type GormDataTypeInterface interface {
GormDataType() string
}
type GormDBDataTypeInterface interface {
GormDBDataType(*gorm.DB, *schema.Field) string
}
The result of GormDataType
will be used as the general data type and can be obtained from schema.Field
‘s field DataType
, which might be helpful when writing plugins or for example:
GormDBDataType
usually returns the right data type for current driver when migrating, for example:
func (JSON) GormDBDataType(db *gorm.DB, field *schema.Field) string {
// use field.Tag, field.TagSettings gets field's tags
// checkout https://github.com/go-gorm/gorm/blob/master/schema/field.go for all options
// returns different database type based on driver name
switch db.Dialector.Name() {
case "mysql", "sqlite":
return "JSON"
case "postgres":
return "JSONB"
}
return ""
}
If the struct hasn’t implemented the GormDBDataTypeInterface
or interface, GORM will guess its data type from the struct’s first field, for example, will use string
for NullString
String string // use the first field's data type
Valid bool
}
type User struct {
Name NullString // data type will be string
}
Create/Update from SQL Expr
type Location struct {
X, Y int
}
func (loc Location) GormDataType() string {
return "geometry"
}
func (loc Location) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
return clause.Expr{
SQL: "ST_PointFromText(?)",
Vars: []interface{}{fmt.Sprintf("POINT(%d %d)", loc.X, loc.Y)},
}
}
// Scan implements the sql.Scanner interface
func (loc *Location) Scan(v interface{}) error {
// Scan a value into struct from database driver
}
type User struct {
ID int
Name string
Location Location
}
db.Create(&User{
Name: "jinzhu",
})
db.Model(&User{ID: 1}).Updates(User{
Name: "jinzhu",
Location: Location{X: 100, Y: 100},
})
// UPDATE `user_with_points` SET `name`="jinzhu",`location`=ST_PointFromText("POINT(100 100)") WHERE `id` = 1
You can also create/update with SQL Expr from map, checkout and Update with SQL Expression for details
Value based on Context
If you want to create or update a value depends on current context, you can also implements the GormValuerInterface
interface, for example:
type EncryptedString struct {
Value string
}
func (es EncryptedString) GormValue(ctx context.Context, db *gorm.DB) (expr clause.Expr) {
if encryptionKey, ok := ctx.Value("TenantEncryptionKey").(string); ok {
return clause.Expr{SQL: "?", Vars: []interface{}{Encrypt(es.Value, encryptionKey)}}
} else {
db.AddError(errors.New("invalid encryption key"))
}
return
}
If you want to build some query helpers, you can make a struct that implements the clause.Expression
interface:
// Generates SQL with clause Expression
db.Find(&user, datatypes.JSONQuery("attributes").HasKey("role"))
db.Find(&user, datatypes.JSONQuery("attributes").HasKey("orgs", "orga"))
// MySQL
// SELECT * FROM `users` WHERE JSON_EXTRACT(`attributes`, '$.role') IS NOT NULL
// SELECT * FROM `users` WHERE JSON_EXTRACT(`attributes`, '$.orgs.orga') IS NOT NULL
// PostgreSQL
// SELECT * FROM "user" WHERE "attributes"::jsonb ? 'role'
// SELECT * FROM "user" WHERE "attributes"::jsonb -> 'orgs' ? 'orga'
db.Find(&user, datatypes.JSONQuery("attributes").Equals("jinzhu", "name"))
// MySQL
// SELECT * FROM `user` WHERE JSON_EXTRACT(`attributes`, '$.name') = "jinzhu"
// SELECT * FROM "user" WHERE json_extract_path_text("attributes"::json,'name') = 'jinzhu'
Customized Data Types Collections
We created a Github repo for customized data types collections , pull request welcome ;)