欢迎回来,伙计们?!今天,我们讨论在数据库之间来回移动数据时可能遇到的一个特定用例。首先,让我为今天的挑战设定界限。为了坚持现实生活中的例子,让我们借用美国陆军的一些概念?我们的任务是编写一个小软件来保存和读取军官在职业生涯中取得的成绩。
我们的软件需要处理军官及其各自的等级。乍一看,这似乎很简单,而且我们可能不需要任何自定义数据类型。但是,为了展示此功能,让我们使用非常规方式来表示数据。因此,我们被要求定义 Go 结构和数据库关系之间的自定义映射。此外,我们必须定义特定的逻辑来解析数据。让我们通过查看该计划的目标来扩展这一点?.
为了方便起见,我们用一张图来描述代码和 SQL 对象之间的关系:
让我们逐一关注每个容器。
在这里,我们定义了两个结构体。 Grade 结构体包含军事等级的非详尽列表?️。该结构不会是数据库中的表。相反,Officer 结构体包含 ID、姓名和指向 Grade 结构体的指针,指示该军官到目前为止已取得的成绩。
每当我们向数据库写入官员时,grades_achieved 列必须包含一个字符串数组,其中填充了所达到的成绩(Grade 结构中具有 true 的成绩)。
关于 SQL 对象,我们只有军官表。 id 和 name 列是不言自明的。然后,我们有 Grades_achieved 列,将官员的成绩保存在字符串集合中。
每当我们从数据库中解码官员时,我们都会解析 Grades_achieved 列并创建 Grade 结构的匹配“实例”。
您可能已经注意到这种行为不是标准行为。我们必须做出一些安排,以期望的方式实现它。
这里,模型的布局故意过于复杂。请尽可能坚持使用更简单的解决方案。
Gorm 为我们提供了自定义数据类型。它们为我们定义数据库的检索和保存提供了极大的灵活性。我们必须实现两个接口:Scanner 和 Valuer?。前者指定从数据库获取数据时要应用的自定义行为。后者指示如何将值写入数据库。两者都帮助我们实现我们需要的非常规映射逻辑。
我们必须实现的函数签名是 Scan(value interface{}) error 和 Value() (driver.Value, error)。现在,让我们看一下代码。
此示例的代码位于两个文件中:domain/models.go 和 main.go。让我们从第一个开始,处理模型(在 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 { // we should have utilized the "comma, ok" idiom 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。现在,让我们看一下 main.go 文件。
在这里,我们准备数据库连接,将对象迁移到关系(ORM代表Object Relation Mapping),然后插入和获取记录下来测试逻辑。代码如下:
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函数来在数据库中添加虚拟数据。数据位于 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}');
main() 函数首先设置数据库连接。在本演示中,我们使用了 PostgreSQL。然后,我们确保军官表存在于数据库中,并且与最新版本的 models.Officer 结构保持同步。由于该程序是一个示例,因此我们做了两件事:
最后,为了确保一切按预期进行,我们做了几件事:
这个文件就是这样。现在,让我们测试一下我们的工作?.
在运行代码之前,请确保您的计算机上正在运行 PostgreSQL 实例。使用 Docker ?,你可以运行这个命令:
docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres
现在,我们可以通过发出命令来安全地运行我们的应用程序: go run 。 ?
输出为:
[ { "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 和 自定义数据类型 的博文。我始终建议您坚持使用最直接的方法。仅当您最终需要时才选择此选项。这种方法增加了灵活性,但代价是使代码变得更复杂和更不健壮(结构定义的微小变化可能会导致错误和需要额外的工作)。
记住这一点。如果您坚持约定,则整个代码库中的内容就会变得不那么冗长。
这是结束这篇博文的一个很好的引用。
如果您意识到需要自定义数据类型,这篇博文应该是一个很好的起点,可以为您提供一个可行的解决方案。
请告诉我您的感受和想法。任何反馈总是值得赞赏!如果您对某个特定主题感兴趣,请联系我,我会将其列入候选名单。下次见,注意安全,再见!
免責聲明: 提供的所有資源部分來自互聯網,如果有侵犯您的版權或其他權益,請說明詳細緣由並提供版權或權益證明然後發到郵箱:[email protected] 我們會在第一時間內為您處理。
Copyright© 2022 湘ICP备2022001581号-3