go

在 gorm 中使用自定义类型

Posted by Zeusro on December 25, 2024
👈🏻 Select language

对于简单类型,gorm 实现了从数据库类型到程序语言类型的转换,而对于自定义类型,需要实现序列化和反序列化方法。

序列化方法指的是从go的代码类型到数据库的类型的转换,对应 Value 方法; 反序列化指的是从数据库的类型到go的代码类型的转换,对应 Scan 方法。

自有类型

自有类型的话,实现2个方法即可。麻烦的在于第三方类型。

第三方类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type FatPod struct {
AnnotationAffinity       PodAffinityTerms             `json:"annotation_affinity" gorm:"type:PodAffinityTerms;column:annotation_affinity" comment:"pod反亲和性注解"`
ResourceLimit            *corev1.ResourceRequirements `json:"-" gorm:"-" comment:"资源限制"`
ResourceLimitP           ResourceRequirementsP        `json:"resource_limit" gorm:"type:ResourceRequirementsP;column:resource_limit" comment:"资源限制"`
}

type PodAffinityTerms []PodAffinityTerm
type ResourceRequirementsP corev1.ResourceRequirements

// Scan 实现 sql.Scanner 接口,Scan 将 value 扫描至 Jsonb,反序列化操作
func (obj *ResourceRequirementsP) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
	}
	result := ResourceRequirementsP{}
	err := json.Unmarshal(bytes, &result)
	*obj = result
	return err
}

//Value  实现 driver.Valuer 接口,Value 返回 json value,序列化操作
func (obj ResourceRequirementsP) Value() (driver.Value, error) {
	if &obj == nil {
		return []byte("{}"), nil
	}
	return json.Marshal(obj)
}

如果结构体使用了第三方包,我们不方便直接修改这个类型。如果原本的类型是对象数组,那么把原本的[]OBJECT,调成 NEWTYPE 即可,在NEWTYPE中实现2个方法。

type NEWTYPE []OBJECT

如果原本的类型是struct,或者*struct,我建议是在结构体中新增一个类型别名,对这个类型实现序列化和反序列方法。然后在标签那里做一点手脚。如上面代码所示: 原本的类型ResourceLimit不变,用一个ResourceLimitP作为实际落库和显示的属性。

这样程序中原本涉及旧字段的地方都不需要改。只需要在存数据库的地方,对新加的属性赋值即可。

其他注意事项

在数据表增加列后,我给 annotation_affinity 用的默认值是’{}’,但是这字段实际是数组,这导致反序列化时失败了: sql: Scan error on column index 62, name “annotation_affinity”: json: cannot unmarshal object into Go value of type dao.PodAffinityTerms;

所以如果数据库中存在不符合规范的值,是否要让 Scan 方法返回错误,这是一个需要考虑的点。

除此以外,我发现 Value 方法,可以在程序层面,给这个字段赋予默认值。具体用法就是当判断对象指针为空时,返回 []byte(“{}”), nil

For simple types, gorm implements conversion from database types to programming language types. For custom types, serialization and deserialization methods need to be implemented.

The serialization method refers to the conversion from Go code types to database types, corresponding to the Value method; Deserialization refers to the conversion from database types to Go code types, corresponding to the Scan method.

Custom Types

For custom types, implementing 2 methods is sufficient. The trouble lies with third-party types.

Third-Party Types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type FatPod struct {
AnnotationAffinity       PodAffinityTerms             `json:"annotation_affinity" gorm:"type:PodAffinityTerms;column:annotation_affinity" comment:"pod反亲和性注解"`
ResourceLimit            *corev1.ResourceRequirements `json:"-" gorm:"-" comment:"资源限制"`
ResourceLimitP           ResourceRequirementsP        `json:"resource_limit" gorm:"type:ResourceRequirementsP;column:resource_limit" comment:"资源限制"`
}

type PodAffinityTerms []PodAffinityTerm
type ResourceRequirementsP corev1.ResourceRequirements

// Scan implements the sql.Scanner interface, Scan scans value into Jsonb, deserialization operation
func (obj *ResourceRequirementsP) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
	}
	result := ResourceRequirementsP{}
	err := json.Unmarshal(bytes, &result)
	*obj = result
	return err
}

//Value implements the driver.Valuer interface, Value returns json value, serialization operation
func (obj ResourceRequirementsP) Value() (driver.Value, error) {
	if &obj == nil {
		return []byte("{}"), nil
	}
	return json.Marshal(obj)
}

If the struct uses a third-party package, we cannot easily modify this type directly. If the original type is an object array, then change the original []OBJECT to NEWTYPE, and implement 2 methods in NEWTYPE.

type NEWTYPE []OBJECT

If the original type is a struct, or *struct, I suggest adding a type alias in the struct, implement serialization and deserialization methods for this type. Then do a little manipulation in the tags. As shown in the code above: The original type ResourceLimit remains unchanged, use ResourceLimitP as the actual stored and displayed attribute.

This way, places in the program that originally involved the old field don’t need to be changed. You only need to assign values to the newly added attribute where data is stored in the database.

Other Notes

After adding a column to the data table, I used the default value '{}' for annotation_affinity, but this field is actually an array, which caused deserialization to fail: sql: Scan error on column index 62, name “annotation_affinity”: json: cannot unmarshal object into Go value of type dao.PodAffinityTerms;

So if there are non-standard values in the database, whether to make the Scan method return an error is a point to consider.

In addition, I found that the Value method can assign default values to this field at the program level. The specific usage is to return []byte("{}"), nil when the object pointer is determined to be empty.

Для простых типов gorm реализует преобразование из типов базы данных в типы языка программирования. Для пользовательских типов необходимо реализовать методы сериализации и десериализации.

Метод сериализации относится к преобразованию из типов кода Go в типы базы данных, соответствует методу Value; Десериализация относится к преобразованию из типов базы данных в типы кода Go, соответствует методу Scan.

Пользовательские типы

Для пользовательских типов достаточно реализовать 2 метода. Проблема заключается в типах сторонних разработчиков.

Типы сторонних разработчиков

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type FatPod struct {
AnnotationAffinity       PodAffinityTerms             `json:"annotation_affinity" gorm:"type:PodAffinityTerms;column:annotation_affinity" comment:"pod反亲和性注解"`
ResourceLimit            *corev1.ResourceRequirements `json:"-" gorm:"-" comment:"资源限制"`
ResourceLimitP           ResourceRequirementsP        `json:"resource_limit" gorm:"type:ResourceRequirementsP;column:resource_limit" comment:"资源限制"`
}

type PodAffinityTerms []PodAffinityTerm
type ResourceRequirementsP corev1.ResourceRequirements

// Scan реализует интерфейс sql.Scanner, Scan сканирует value в Jsonb, операция десериализации
func (obj *ResourceRequirementsP) Scan(value interface{}) error {
	bytes, ok := value.([]byte)
	if !ok {
		return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value))
	}
	result := ResourceRequirementsP{}
	err := json.Unmarshal(bytes, &result)
	*obj = result
	return err
}

//Value реализует интерфейс driver.Valuer, Value возвращает json value, операция сериализации
func (obj ResourceRequirementsP) Value() (driver.Value, error) {
	if &obj == nil {
		return []byte("{}"), nil
	}
	return json.Marshal(obj)
}

Если структура использует пакет стороннего разработчика, мы не можем легко изменить этот тип напрямую. Если исходный тип — это массив объектов, измените исходный []OBJECT на NEWTYPE и реализуйте 2 метода в NEWTYPE.

type NEWTYPE []OBJECT

Если исходный тип — это struct или *struct, я предлагаю добавить псевдоним типа в структуре, реализовать методы сериализации и десериализации для этого типа. Затем немного манипулировать тегами. Как показано в коде выше: Исходный тип ResourceLimit остаётся без изменений, используйте ResourceLimitP как фактически сохраняемый и отображаемый атрибут.

Таким образом, места в программе, которые изначально затрагивали старое поле, не нужно изменять. Вам нужно только присвоить значения новому добавленному атрибуту там, где данные сохраняются в базе данных.

Другие замечания

После добавления столбца в таблицу данных я использовал значение по умолчанию '{}' для annotation_affinity, но это поле на самом деле является массивом, что привело к сбою десериализации: sql: Scan error on column index 62, name “annotation_affinity”: json: cannot unmarshal object into Go value of type dao.PodAffinityTerms;

Поэтому, если в базе данных есть нестандартные значения, следует ли заставить метод Scan возвращать ошибку — это момент, который нужно рассмотреть.

Кроме того, я обнаружил, что метод Value может назначать значения по умолчанию для этого поля на уровне программы. Конкретное использование — возвращать []byte("{}"), nil, когда указатель объекта определяется как пустой.