Gorm 自定義數據類型

在處理數據庫交互時,我們經常會遇到將數據在 Go 結構體和數據庫關係之間來回轉換的需求。今天,我們將深入探討 Gorm 中的自定義數據類型,這是一種強大的工具,可以幫助我們實現靈活的數據映射和自定義邏輯。

使用場景:處理軍官等級

爲了更好地理解自定義數據類型的用途,我們以一個現實的例子來說明:構建一個軟件來存儲和讀取美軍軍官的等級信息。

這個例子中,我們定義了兩個結構體:

Go 結構體

當我們將軍官信息寫入數據庫時,grades_achieved 列將包含一個字符串數組,其中包含軍官已達到的等級(Grade 結構體中值爲 true 的等級)。

數據庫關係

從數據庫中讀取軍官信息時,我們將解析 grades_achieved 列並創建一個與之匹配的 Grade 結構體實例。

這種數據映射方式並非標準做法,我們需要使用自定義數據類型來實現這種非標準的映射邏輯。

自定義數據類型

Gorm 提供了 自定義數據類型 功能,允許我們自定義數據在數據庫和 Go 結構體之間轉換的行爲。我們需要實現兩個接口:ScannerValuer

這兩個接口分別需要實現 Scan(value interface{}) errorValue() (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
}

代碼要點:

  1. Grade 結構體: 定義了軍官等級列表。

  2. Officer 結構體: 定義了軍官的屬性,並使用 gorm:"type:varchar[]" 註解將 GradesAchieved 字段映射爲數據庫中的一個字符串數組。

  3. Grade 結構體的 Scan 方法: 從數據庫中讀取數據時,解析 grades_achieved 列並設置 Grade 結構體相應的屬性。

  4. 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))
}

代碼要點:

  1. seedDB 函數: 用於向數據庫中添加測試數據。

  2. 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