「労働者が自分の仕事をうまくやりたいなら、まず自分の道具を研ぎ澄まさなければなりません。」 - 孔子、「論語。陸霊公」
表紙 > プログラミング > Gorm: カスタム データ型の概要

Gorm: カスタム データ型の概要

2024 年 11 月 8 日に公開
ブラウズ:650

おかえりなさい、皆さん?!今日は、データベースとの間でデータをやり取りするときに直面する可能性のある具体的なユースケースについて説明します。まず、今日の課題の境界線を設定しましょう。現実の例に固執するために、アメリカ陸軍からいくつかの概念を借用しましょう。私たちの契約は、役員のキャリアで達成した成績を保存して読み取るための小さなソフトウェアを作成することです。

Gorm のカスタム データ型

私たちのソフトウェアは、それぞれの等級の陸軍将校を処理する必要があります。一見すると簡単そうに見えますが、おそらくここではカスタム データ タイプは必要ありません。ただし、この機能を示すために、従来とは異なる方法でデータを表現してみましょう。このおかげで、Go 構造体と DB リレーション間のカスタム マッピングを定義するように求められます。さらに、データを解析するための特定のロジックを定義する必要があります。プログラムのターゲットを見て、これをさらに詳しく見てみましょう ?.

処理するユースケース

作業を簡単にするために、図を使用してコードと SQL オブジェクトの関係を示しましょう:

Gorm: Sneak Peek of Custom Data Types

各コンテナに 1 つずつ注目してみましょう。

Go 構造体 ?

ここでは 2 つの構造体を定義しました。 Grade 構造体は、軍事等級の非網羅的なリストを保持します ?️。この構造体はデータベース内のテーブルにはなりません。逆に、Officer 構造体には ID、名前、Grade 構造体へのポインタが含まれており、これまでにその役員が達成したグレードを示します。

DB に役員を書き込むときは常に、列 Grade_achieved には、達成された成績 (Grade 構造体で true を持つもの) が入力された文字列の配列が含まれている必要があります。

DB関係?

SQL オブジェクトに関しては、office テーブルのみがあります。 id 列と name 列は一目瞭然です。次に、役員の成績を文字列のコレクションに保持する Grade_achieved 列があります。


データベースから役員をデコードするたびに、grades_achieved 列を解析し、Grade 構造体の一致する「インスタンス」を作成します。

この動作が標準的な動作ではないことに気づいたかもしれません。望ましい方法でそれを実現するには、何らかの手配をしなければなりません。

ここでは、モデルのレイアウトを意図的に複雑にしすぎています。可能な限り、より単純な解決策を使用してください。

カスタムデータ型

Gorm はカスタム データ型を提供します。これらにより、データベースへの取得とデータベースからの保存を定義する際に、非常に柔軟な対応が可能になります。 Scanner と Valuer という 2 つのインターフェイスを実装する必要があります。前者は、DB からデータをフェッチするときに適用するカスタム動作を指定します。後者は、データベースに値を書き込む方法を示します。どちらも、必要な非従来型のマッピング ロジックを実現するのに役立ちます。


実装する必要がある関数のシグネチャは、Scan(valueinterface{}) error と Value() (driver.Value, error) です。それでは、コードを見てみましょう。

コード

この例のコードは、domain/models.go と main.go の 2 つのファイルに存在します。最初のモデル (Go では構造体として変換される) を扱うことから始めましょう。

ドメイン/models.go ファイル

まず、このファイルのコードを示します:


パッケージモデル 輸入 ( 「データベース/SQL/ドライバー」 「スライス」 「文字列」 ) タイプ グレード構造体 { ブール中尉 キャプテンブール ブール大佐 一般的なブール値 } タイプオフィサー構造体{ ID uint64 `gorm:"primaryKey"` 名前文字列 達成した成績 *成績 `gorm:"type:varchar[]"` } func (g *Grade) Scan(値インターフェイス{}) エラー { // 「カンマ、OK」というイディオムを使用するべきでした valueRaw := 値.(文字列) valueRaw = strings.Replace(strings.Replace(valueRaw, "{", "", -1), "}", "", -1) 成績 := strings.Split(valueRaw, ",") if スライス.Contains(成績, "中尉") { g.中尉 = true } if スライス.Contains(grades, "キャプテン") { g.キャプテン = true } if スライス.Contains(成績, "大佐") { g.大佐 = true } if スライス.Contains(grades, "general") { g.一般 = true } nilを返す } func (g Grade) Value() (driver.Value, error) { 成績 := make([]string, 0, 4) if g.中尉 { 等級 = append(等級, "中尉") } if g.キャプテン { 成績 = append(成績, "キャプテン") } if g.大佐 { 等級 = append(等級, "大佐") } if g.General { 成績 = append(成績, "一般") } 成績を返す、nil }
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 構造体には、ソフトウェアで予測された成績のみがリストされます
  1. Officer 構造体はエンティティの特性を定義します。このエンティティは DB 内のリレーションです。 2 つの Gorm 記法を適用しました。
    1. ID フィールドの gorm:"primaryKey" をリレーションの主キーとして定義します
    2. gorm:"type:varchar[]" を使用して、フィールド GradesAchieved を DB 内の varchar の配列としてマップします。それ以外の場合は、独立した DB テーブルまたは役員テーブルの追加の列として変換されます
  2. Grade 構造体は Scan 関数を実装します。ここでは、生の値を取得し、それを調整し、 g 変数にいくつかのフィールドを設定して、
  3. を返します。
  4. Grade 構造体は、値レシーバー型として Value 関数も実装します (今回はレシーバーを変更する必要はありません。* 参照は使用しません)。役員テーブル
  5. の列grade_achievedに書き込む値を返します。
これら 2 つのメソッドのおかげで、DB インタラクション中に Grade タイプを送信および取得する方法を制御できます。次に、main.go ファイルを見てみましょう。

main.go ファイル?

ここでは、DB 接続を準備し、オブジェクトをリレーション (ORM は

Object Relation Mapping の略) に移行し、挿入およびフェッチします。ロジックをテストするためのレコード。以下はコードです:

パッケージメイン 輸入 ( 「エンコーディング/json」 「fmt」 「オス」 「gormcustomdatatype/models」 「gorm.io/ドライバー/postgres」 「ゴーム.io/ゴーム」 ) func seedDB(db *gorm.DB, ファイル文字列) エラー { データ、エラー:= os.ReadFile(ファイル) エラーの場合 != nil { エラーを返す } if err := db.Exec(string(data)).Error;エラー != nil { エラーを返す } nilを返す } // docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres 関数 main() { dsn := "ホスト = ローカルホスト ポート = 54322 ユーザー = postgres パスワード = postgres dbname = postgres sslmode = 無効" db、エラー := gorm.Open(postgres.Open(dsn), &gorm.Config{}) エラーの場合 != nil { fmt.Fprintf(os.Stderr, "DB に接続できませんでした: %v", err) 戻る } db.AutoMigrate(&models.Officer{}) 遅延関数() { db.Migrator().DropTable(&models.Officer{}) }() エラーの場合:= seedDB(db, "data.sql");エラー != nil { fmt.Fprintf(os.Stderr, "DB のシードに失敗しました: %v", err) 戻る } // すべての役員を出力します var 役員 []models.Officer エラーの場合:= db.Find(&officers).Error;エラー != nil { fmt.Fprintf(os.Stderr, "DB から役員を取得できませんでした: %v", err) 戻る } データ、_ := json.MarshalIndent(役員, "", "\t") fmt.Fprintln(os.Stdout, 文字列(データ)) // 新しい役員を追加します db.Create(&models.Officer{ 名前:「モンキー・D・ガープ」、 達成した成績: &models.Grade{ 中尉: 本当です、 船長:本当だよ 大佐: 本当です、 一般: 本当、 }、 }) var garpTheHero モデル.Officer if err := db.First(&garpTheHero, 4).Error;エラー != nil { fmt.Fprintf(os.Stderr, "DB から役員を取得できませんでした: %v", err) 戻る } データ、_ = json.MarshalIndent(&garpTheHero, "", "\t") fmt.Fprintln(os.Stdout, 文字列(データ)) }
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
}
次に、このファイルの関連セクションを見てみましょう。まず、DBにダミーデータを追加するseedDB関数を定義します。データは、次の内容の data.sql ファイル内に存在します:


INSERT INTO public.officers (id, "名前", 成績_達成) VALUES(nextval('officers_id_seq'::regclass), 'ジョン・ドウ', '{大尉,中尉}'), (nextval('officers_id_seq'::regclass), 'ジェラルド・バトラー', '{general}'), (nextval('officers_id_seq'::regclass), 'チャック・ノリス', '{中尉,大尉,大佐}');
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
}
main() 関数は、DB 接続を設定することから始まります。このデモでは、PostgreSQL を使用しました。次に、officer テーブルがデータベースに存在し、models.Officer 構造体の最新バージョンで最新であることを確認します。このプログラムはサンプルなので、さらに 2 つのことを行いました:

    main() 関数の最後でのテーブルの削除 (プログラム終了時にテーブルも削除したいと考えています)
  • ダミーデータのシード
最後に、すべてが期待どおりに動作することを確認するために、いくつかのことを行います:

    DB内のすべてのレコードを取得します
  1. 新しい役員を追加 (およびフェッチバック)
このファイルは以上です。さて、作業をテストしましょう ?.

真実の瞬間

コードを実行する前に、PostgreSQL インスタンスがマシン上で実行されていることを確認してください。 Docker ? を使用すると、次のコマンドを実行できます:


docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres
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
}
これで、 go run コマンドを発行することでアプリケーションを安全に実行できるようになりました。 ?

出力は次のとおりです:


[ { 「ID」:1、 "名前": "ジョン・ドゥ", "達成した成績": { 「中尉」:本当です、 「キャプテン」:本当です、 「大佐」: 偽、 「一般」: false } }、 { 「ID」:2、 "名前": "ジェラルド・バトラー", "達成した成績": { 「中尉」: 偽、 「キャプテン」: false、 「大佐」: 偽、 「一般」: true } }、 { 「ID」:3、 "名前": "チャック ノリス", "達成した成績": { 「中尉」:本当です、 「キャプテン」:本当です、 「大佐」:本当です、 「一般」: false } } ] { 「ID」:4、 "名前": "モンキー・D・ガープ", "達成した成績": { 「中尉」:本当です、 「キャプテン」:本当です、 「大佐」:本当です、 「一般」: true } }
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
}
ほら!すべてが期待どおりに機能します。コードを数回再実行すると、常に同じ出力が得られます。

それはラップです

Gormカスタム データ タイプ に関するこのブログ投稿を楽しんでいただけたでしょうか。私は常に、最も単純なアプローチに固執することをお勧めします。最終的に必要になった場合にのみ、これを選択してください。このアプローチでは、コードがより複雑になり堅牢性が低下する代わりに柔軟性が追加されます (構造体の定義のわずかな変更によりエラーが発生し、追加の作業が必要になる可能性があります)。

これを覚えておいてください。規則に従えば、コードベース全体の冗長性を減らすことができます。

これはこのブログ投稿を締めくくるのに最適な引用です。

カスタム データ型が必要であることがわかった場合は、このブログ投稿が有効なソリューションを示す良い出発点となるはずです。

あなたの気持ちや考えを教えてください。フィードバックはいつでも大歓迎です!特定のトピックにご興味がございましたら、ご連絡ください。候補者リストに掲載させていただきます。次回まで、安全に過ごしてください。またお会いしましょう!

リリースステートメント この記事は次の場所に転載されています: https://dev.to/ossan/gorm-sneak-peek-of-custom-data-types-97n?1 侵害がある場合は、[email protected] に連絡して削除してください。
最新のチュートリアル もっと>

免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。

Copyright© 2022 湘ICP备2022001581号-3