Gorm 自定義數據類型
在處理數據庫交互時,我們經常會遇到將數據在 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 db
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 中的自定義數據類型,並在實際項目中靈活運用它們。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/rXv53o50OM-ua43BUAms9Q