Skip to content

Commit

Permalink
add JSONSlice generic for slice data (#199)
Browse files Browse the repository at this point in the history
* add JSONSlice generic for slice data

remove unused method

* try rebuild to fix pg error

try fix pg error

* add docs
  • Loading branch information
alingse authored Mar 23, 2023
1 parent b78869d commit 202f3ee
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 1 deletion.
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ type UserWithJSON struct {
}

var user = UserWithJSON{
Name: "hello"
Name: "hello",
Attributes: datatypes.JSONType[Attribute]{
Data: Attribute{
Age: 18,
Expand Down Expand Up @@ -214,6 +214,48 @@ DB.Model(&user).Updates(jsonMap)

NOTE: it's not support json query

## JSONSlice[T]

sqlite, mysql, postgres supported

```go
import "gorm.io/datatypes"

type Tag struct {
Name string
Score float64
}

type UserWithJSON struct {
gorm.Model
Name string
Tags datatypes.JSONSlice[Tag]
}

var tags = []Tag{{Name: "tag1", Score: 0.1}, {Name: "tag2", Score: 0.2}}
var user = UserWithJSON{
Name: "hello",
Tags: datatypes.NewJSONSlice(tags),
}

// Create
DB.Create(&user)

// First
var result UserWithJSON
DB.First(&result, user.ID)

// Update
var tags2 = []Tag{{Name: "tag3", Score: 10.1}, {Name: "tag4", Score: 10.2}}
jsonMap = UserWithJSON{
Tags: datatypes.NewJSONSlice(tags2),
}

DB.Model(&user).Updates(jsonMap)
```

NOTE: it's not support json query

## JSONArray

mysql supported
Expand Down
63 changes: 63 additions & 0 deletions json_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ type JSONType[T any] struct {
Data T
}

func NewJSONType[T any](data T) JSONType[T] {
return JSONType[T]{
Data: data,
}
}

// Value return json value, implement driver.Valuer interface
func (j JSONType[T]) Value() (driver.Value, error) {
return json.Marshal(j.Data)
Expand Down Expand Up @@ -78,3 +84,60 @@ func (js JSONType[T]) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {

return gorm.Expr("?", string(data))
}

// JSONSlice give a generic data type for json encoded slice data.
type JSONSlice[T any] []T

func NewJSONSlice[T any](s []T) JSONSlice[T] {
return JSONSlice[T](s)
}

// Value return json value, implement driver.Valuer interface
func (j JSONSlice[T]) Value() (driver.Value, error) {
return json.Marshal(j)
}

// Scan scan value into JSONType[T], implements sql.Scanner interface
func (j *JSONSlice[T]) Scan(value interface{}) error {
var bytes []byte
switch v := value.(type) {
case []byte:
bytes = v
case string:
bytes = []byte(v)
default:
return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
}
return json.Unmarshal(bytes, &j)
}

// GormDataType gorm common data type
func (JSONSlice[T]) GormDataType() string {
return "json"
}

// GormDBDataType gorm db data type
func (JSONSlice[T]) GormDBDataType(db *gorm.DB, field *schema.Field) string {
switch db.Dialector.Name() {
case "sqlite":
return "JSON"
case "mysql":
return "JSON"
case "postgres":
return "JSONB"
}
return ""
}

func (j JSONSlice[T]) GormValue(ctx context.Context, db *gorm.DB) clause.Expr {
data, _ := json.Marshal(j)

switch db.Dialector.Name() {
case "mysql":
if v, ok := db.Dialector.(*mysql.Dialector); ok && !strings.Contains(v.ServerVersion, "MariaDB") {
return gorm.Expr("CAST(? AS JSON)", string(data))
}
}

return gorm.Expr("?", string(data))
}
73 changes: 73 additions & 0 deletions json_type_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ func TestJSONType(t *testing.T) {
}, {
Name: "json-3",
Attributes: newJSONType[Attribute]([]byte(`{"tags": ["tag1","tag2","tag3"]`)),
}, {
Name: "json-4",
Attributes: datatypes.NewJSONType(Attribute{Tags: []string{"tag1", "tag2", "tag3"}}),
}, {
Name: "json-5",
Attributes: datatypes.JSONType[Attribute]{Data: Attribute{Tags: []string{"tag1", "tag2", "tag3"}}},
}}

if err := DB.Create(&users).Error; err != nil {
Expand Down Expand Up @@ -92,3 +98,70 @@ func TestJSONType(t *testing.T) {
}
}
}

func TestJSONSlice(t *testing.T) {
if SupportedDriver("sqlite", "mysql", "postgres") {
type Tag struct {
Name string
Score float64
}
type UserWithJSON2 struct {
gorm.Model
Name string
Tags datatypes.JSONSlice[Tag]
}
type UserWithJSON = UserWithJSON2

DB.Migrator().DropTable(&UserWithJSON{})
if err := DB.Migrator().AutoMigrate(&UserWithJSON{}); err != nil {
t.Errorf("failed to migrate, got error: %v", err)
}

// Go's json marshaler removes whitespace & orders keys alphabetically
// use to compare against marshaled []byte of datatypes.JSON
var tags = []Tag{{Name: "tag1", Score: 0.1}, {Name: "tag2", Score: 0.2}}

users := []UserWithJSON{{
Name: "json-1",
Tags: datatypes.JSONSlice[Tag]{{Name: "tag1", Score: 1.1}, {Name: "tag2", Score: 1.2}},
}, {
Name: "json-2",
Tags: datatypes.NewJSONSlice([]Tag{{Name: "tag3", Score: 0.3}, {Name: "tag4", Score: 0.4}}),
}, {
Name: "json-3",
Tags: datatypes.JSONSlice[Tag](tags),
}, {
Name: "json-4",
Tags: datatypes.NewJSONSlice(tags),
}}

if err := DB.Create(&users).Error; err != nil {
t.Errorf("Failed to create users %v", err)
}

var result UserWithJSON
if err := DB.First(&result, users[0].ID).Error; err != nil {
t.Fatalf("failed to find user with json key, got error %v", err)
}
AssertEqual(t, result.Name, users[0].Name)
AssertEqual(t, result.Tags[0], users[0].Tags[0])

// FirstOrCreate
jsonMap := UserWithJSON{
Tags: datatypes.NewJSONSlice(tags),
}
if err := DB.Where(&UserWithJSON{Name: "json-1"}).Assign(jsonMap).FirstOrCreate(&UserWithJSON{}).Error; err != nil {
t.Errorf("failed to run FirstOrCreate")
}

// Update
jsonMap = UserWithJSON{
Tags: datatypes.NewJSONSlice(tags),
}
var result3 UserWithJSON
result3.ID = 1
if err := DB.Model(&result3).Updates(jsonMap).Error; err != nil {
t.Errorf("failed to run Updates")
}
}
}

0 comments on commit 202f3ee

Please sign in to comment.