Repository: go-gorm/gorm
Pull request #5108 (2022-02-23 12:25:33)

feat: support gob serialize
<!--
Make sure these boxes checked before submitting your pull request.

For significant changes, please open an issue to make an agreement on an implementation design/plan first before starting it.
-->

- [x] Do only one thing
- [x] Non breaking API changes
- [x] Tested

### What did this pull request do?

<!--
provide a general description of the code changes in your pull request
-->

Implement a GobSerializer in schema/serializer to support Gob encoding in the latest released serializer tag feature. 

### User Case Description

<!-- Your use case -->

In the past, if we want to set the value of a struct into a certain field, we would marshal the struct to a string/bytes(normally with json/msgpack/gob). With the support for serializer, it becomes a relatively easy job. 

But now it only supports json and unixtime. Golang provide native support for gob, which has a better performance over json. Hopefully, this will help developers reduce the trouble on writing gob encoding and decoding.



Patch:
diff --git a/schema/naming.go b/schema/naming.go
index 125094b..47a2b36 100644
--- a/schema/naming.go
+++ b/schema/naming.go
@@ -3,7 +3,6 @@ package schema
 import (
 	"crypto/sha1"
 	"encoding/hex"
-	"fmt"
 	"regexp"
 	"strings"
 	"unicode/utf8"
@@ -95,7 +94,7 @@ func (ns NamingStrategy) formatName(prefix, table, name string) string {
 		h.Write([]byte(formattedName))
 		bs := h.Sum(nil)
 
-		formattedName = fmt.Sprintf("%v%v%v", prefix, table, name)[0:56] + hex.EncodeToString(bs)[:8]
+		formattedName = formattedName[0:56] + hex.EncodeToString(bs)[:8]
 	}
 	return formattedName
 }
diff --git a/schema/serializer.go b/schema/serializer.go
index 6859753..09da6d9 100644
--- a/schema/serializer.go
+++ b/schema/serializer.go
@@ -1,11 +1,12 @@
 package schema
 
 import (
+	"bytes"
 	"context"
 	"database/sql"
 	"database/sql/driver"
+	"encoding/gob"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"reflect"
 	"strings"
@@ -32,6 +33,7 @@ func GetSerializer(name string) (serializer SerializerInterface, ok bool) {
 func init() {
 	RegisterSerializer("json", JSONSerializer{})
 	RegisterSerializer("unixtime", UnixSecondSerializer{})
+	RegisterSerializer("gob", GobSerializer{})
 }
 
 // Serializer field value serializer
@@ -83,7 +85,7 @@ func (JSONSerializer) Scan(ctx context.Context, field *Field, dst reflect.Value,
 		case string:
 			bytes = []byte(v)
 		default:
-			return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", dbValue))
+			return fmt.Errorf("failed to unmarshal JSONB value: %#v", dbValue)
 		}
 
 		err = json.Unmarshal(bytes, fieldValue.Interface())
@@ -123,3 +125,33 @@ func (UnixSecondSerializer) Value(ctx context.Context, field *Field, dst reflect
 	}
 	return
 }
+
+// GobSerializer gob serializer
+type GobSerializer struct {
+}
+
+// Scan implements serializer interface
+func (GobSerializer) Scan(ctx context.Context, field *Field, dst reflect.Value, dbValue interface{}) (err error) {
+	fieldValue := reflect.New(field.FieldType)
+
+	if dbValue != nil {
+		var bytesValue []byte
+		switch v := dbValue.(type) {
+		case []byte:
+			bytesValue = v
+		default:
+			return fmt.Errorf("failed to unmarshal gob value: %#v", dbValue)
+		}
+		decoder := gob.NewDecoder(bytes.NewBuffer(bytesValue))
+		err = decoder.Decode(fieldValue.Interface())
+	}
+	field.ReflectValueOf(ctx, dst).Set(fieldValue.Elem())
+	return
+}
+
+// Value implements serializer interface
+func (GobSerializer) Value(ctx context.Context, field *Field, dst reflect.Value, fieldValue interface{}) (interface{}, error) {
+	buf := new(bytes.Buffer)
+	err := gob.NewEncoder(buf).Encode(fieldValue)
+	return buf.Bytes(), err
+}


Test patch:
diff --git a/schema/naming_test.go b/schema/naming_test.go
index 1fdab9a..3f598c3 100644
--- a/schema/naming_test.go
+++ b/schema/naming_test.go
@@ -193,7 +193,7 @@ func TestFormatNameWithStringLongerThan64Characters(t *testing.T) {
 	ns := NamingStrategy{}
 
 	formattedName := ns.formatName("prefix", "table", "thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVeryLongString")
-	if formattedName != "prefixtablethisIsAVeryVeryVeryVeryVeryVeryVeryVeryVeryLo180f2c67" {
+	if formattedName != "prefix_table_thisIsAVeryVeryVeryVeryVeryVeryVeryVeryVery180f2c67" {
 		t.Errorf("invalid formatted name generated, got %v", formattedName)
 	}
 }
diff --git a/schema/relationship_test.go b/schema/relationship_test.go
index e2cf11a..40ffc32 100644
--- a/schema/relationship_test.go
+++ b/schema/relationship_test.go
@@ -576,3 +576,39 @@ func TestHasManySameForeignKey(t *testing.T) {
 		References: []Reference{{"ID", "User", "UserRefer", "Profile", "", true}},
 	})
 }
+
+type Author struct {
+	gorm.Model
+}
+
+type Book struct {
+	gorm.Model
+	Author   Author
+	AuthorID uint
+}
+
+func (Book) TableName() string {
+	return "my_schema.a_very_very_very_very_very_very_very_very_long_table_name"
+}
+
+func TestParseConstraintNameWithSchemaQualifiedLongTableName(t *testing.T) {
+	s, err := schema.Parse(
+		&Book{},
+		&sync.Map{},
+		schema.NamingStrategy{},
+	)
+	if err != nil {
+		t.Fatalf("Failed to parse schema")
+	}
+
+	expectedConstraintName := "fk_my_schema_a_very_very_very_very_very_very_very_very_l4db13eec"
+	constraint := s.Relationships.Relations["Author"].ParseConstraint()
+
+	if constraint.Name != expectedConstraintName {
+		t.Fatalf(
+			"expected constraint name %s, got %s",
+			expectedConstraintName,
+			constraint.Name,
+		)
+	}
+}
diff --git a/tests/serializer_test.go b/tests/serializer_test.go
index 3ed733d..a8a4e28 100644
--- a/tests/serializer_test.go
+++ b/tests/serializer_test.go
@@ -19,11 +19,20 @@ type SerializerStruct struct {
 	Name            []byte                 `gorm:"json"`
 	Roles           Roles                  `gorm:"serializer:json"`
 	Contracts       map[string]interface{} `gorm:"serializer:json"`
+	JobInfo         Job                    `gorm:"type:bytes;serializer:gob"`
 	CreatedTime     int64                  `gorm:"serializer:unixtime;type:time"` // store time in db, use int as field type
 	EncryptedString EncryptedString
 }
 
 type Roles []string
+
+type Job struct {
+	Title    string
+	Number   int
+	Location string
+	IsIntern bool
+}
+
 type EncryptedString string
 
 func (es *EncryptedString) Scan(ctx context.Context, field *schema.Field, dst reflect.Value, dbValue interface{}) (err error) {
@@ -56,6 +65,12 @@ func TestSerializer(t *testing.T) {
 		Contracts:       map[string]interface{}{"name": "jinzhu", "age": 10},
 		EncryptedString: EncryptedString("pass"),
 		CreatedTime:     createdAt.Unix(),
+		JobInfo: Job{
+			Title:    "programmer",
+			Number:   9920,
+			Location: "Kenmawr",
+			IsIntern: false,
+		},
 	}
 
 	if err := DB.Create(&data).Error; err != nil {