在处理数据库交互时,我们经常会遇到将数据在 Go 结构体和数据库关系之间来回转换的需求。今天,我们将深入探讨 Gorm 中的自定义数据类型,这是一种强大的工具,可以帮助我们实现灵活的数据映射和自定义逻辑。
使用场景:处理军官等级
为了更好地理解自定义数据类型的用途,我们以一个现实的例子来说明:构建一个软件来存储和读取美军军官的等级信息。
这个例子中,我们定义了两个结构体:
Go 结构体
Grade: 包含一个军官等级列表,例如:少尉、上尉、上校、将军。 Grade
结构体本身不会映射到数据库中的表。Officer: 包含军官的 ID、姓名以及指向 Grade
结构体的指针,表示该军官已达到的等级。
当我们将军官信息写入数据库时,grades_achieved
列将包含一个字符串数组,其中包含军官已达到的等级(Grade
结构体中值为 true
的等级)。
数据库关系
officers 表: 包含 id
、name
和grades_achieved
列。grades_achieved
列存储一个字符串集合,表示军官的等级。
从数据库中读取军官信息时,我们将解析 grades_achieved
列并创建一个与之匹配的 Grade
结构体实例。
这种数据映射方式并非标准做法,我们需要使用自定义数据类型来实现这种非标准的映射逻辑。
自定义数据类型
Gorm 提供了 自定义数据类型 功能,允许我们自定义数据在数据库和 Go 结构体之间转换的行为。我们需要实现两个接口:Scanner
和 Valuer
。
Scanner: 定义从数据库中读取数据时的自定义行为。 Valuer: 定义将数据写入数据库时的自定义行为。
这两个接口分别需要实现 Scan(value interface{}) error
和 Value() (driver.Value, error)
方法。
代码示例
以下代码展示了如何使用自定义数据类型来实现军官等级信息存储和读取:
domain/models.go
文件
package models
import (
"database/sql/driver"
"slices"
"strings"
)
type Grade struct {
Lieutenant bool
Captain bool
Colonel bool
General bool
}
type Officer struct {
ID uint64 `gorm:"primaryKey"`
Name string
GradesAchieved *Grade `gorm:"type:varchar[]"`
}
func (g *Grade) Scan(value interface{}) error {
valueRaw := value.(string)
valueRaw = strings.Replace(strings.Replace(valueRaw, "{", "", -1), "}", "", -1)
grades := strings.Split(valueRaw, ",")
if slices.Contains(grades, "lieutenant") {
g.Lieutenant = true
}
if slices.Contains(grades, "captain") {
g.Captain = true
}
if slices.Contains(grades, "colonel") {
g.Colonel = true
}
if slices.Contains(grades, "general") {
g.General = true
}
return nil
}
func (g Grade) Value() (driver.Value, error) {
grades := make([]string, 0, 4)
if g.Lieutenant {
grades = append(grades, "lieutenant")
}
if g.Captain {
grades = append(grades, "captain")
}
if g.Colonel {
grades = append(grades, "colonel")
}
if g.General {
grades = append(grades, "general")
}
return grades, nil
}
代码要点:
Grade
结构体: 定义了军官等级列表。Officer
结构体: 定义了军官的属性,并使用gorm:"type:varchar[]"
注解将GradesAchieved
字段映射为数据库中的一个字符串数组。Grade
结构体的Scan
方法: 从数据库中读取数据时,解析grades_achieved
列并设置Grade
结构体相应的属性。Grade
结构体的Value
方法: 将Grade
结构体转换为一个字符串数组,以便写入数据库中的grades_achieved
列。
main.go
文件
package main
import (
"encoding/json"
"fmt"
"os"
"gormcustomdatatype/models"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func seedDB(db *gorm.DB, file string) error {
data, err := os.ReadFile(file)
if err != nil {
return err
}
if err := db.Exec(string(data)).Error; err != nil {
return err
}
return nil
}
// docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres
func main() {
dsn := "host=localhost port=54322 user=postgres password=postgres dbname=postgres sslmode=disable"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Fprintf(os.Stderr, "could not connect to DB: %v", err)
return
}
db.AutoMigrate(&models.Officer{})
defer func() {
db.Migrator().DropTable(&models.Officer{})
}()
if err := seedDB(db, "data.sql"); err != nil {
fmt.Fprintf(os.Stderr, "failed to seed DB: %v", err)
return
}
// print all the officers
var officers []models.Officer
if err := db.Find(&officers).Error; err != nil {
fmt.Fprintf(os.Stderr, "could not get the officers from the DB: %v", err)
return
}
data, _ := json.MarshalIndent(officers, "", "\t")
fmt.Fprintln(os.Stdout, string(data))
// add a new officer
db.Create(&models.Officer{
Name: "Monkey D. Garp",
GradesAchieved: &models.Grade{
Lieutenant: true,
Captain: true,
Colonel: true,
General: true,
},
})
var garpTheHero models.Officer
if err := db.First(&garpTheHero, 4).Error; err != nil {
fmt.Fprintf(os.Stderr, "failed to get officer from the DB: %v", err)
return
}
data, _ = json.MarshalIndent(&garpTheHero, "", "\t")
fmt.Fprintln(os.Stdout, string(data))
}
代码要点:
seedDB
函数: 用于向数据库中添加测试数据。main
函数: 建立数据库连接,自动迁移模型到数据库,添加测试数据,读取所有军官信息,并添加一个新的军官。
data.sql
文件
INSERT INTO public.officers
(id, "name", grades_achieved)
VALUES(nextval('officers_id_seq'::regclass), 'john doe', '{captain,lieutenant}'),
(nextval('officers_id_seq'::regclass), 'gerard butler', '{general}'),
(nextval('officers_id_seq'::regclass), 'chuck norris', '{lieutenant,captain,colonel}');
这个文件包含了用于测试的军官信息。
测试结果
运行代码后,我们将得到以下输出:
[
{
"ID": 1,
"Name": "john doe",
"GradesAchieved": {
"Lieutenant": true,
"Captain": true,
"Colonel": false,
"General": false
}
},
{
"ID": 2,
"Name": "gerard butler",
"GradesAchieved": {
"Lieutenant": false,
"Captain": false,
"Colonel": false,
"General": true
}
},
{
"ID": 3,
"Name": "chuck norris",
"GradesAchieved": {
"Lieutenant": true,
"Captain": true,
"Colonel": true,
"General": false
}
}
]
{
"ID": 4,
"Name": "Monkey D. Garp",
"GradesAchieved": {
"Lieutenant": true,
"Captain": true,
"Colonel": true,
"General": true
}
}
结果显示,程序成功读取了数据库中的军官信息,并成功添加了一个新的军官。
总结
自定义数据类型是 Gorm 中一个强大的功能,它允许我们实现灵活的数据映射和自定义逻辑。虽然它可以为我们提供更大的灵活性,但也增加了代码的复杂度和维护难度。在实际开发中,我们应该尽可能遵循标准做法,只有在必要时才使用自定义数据类型。
希望本文能帮助你更好地理解 Gorm 中的自定义数据类型,并在实际项目中灵活运用它们。
还没有评论,来说两句吧...