ホームページ >バックエンド開発 >Golang >Gorm: カスタム データ型の概要

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

DDD
DDDオリジナル
2024-09-13 20:15:36857ブラウズ

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

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 ファイル

まず、このファイルのコードを紹介します。

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
}

それでは、関連する部分を強調表示してみましょう?:

  1. Grade 構造体には、ソフトウェアで予測された評点のみがリストされます
  2. Officer 構造体はエンティティの特性を定義します。このエンティティは DB 内のリレーションです。 2 つの Gorm 記法を適用しました。
    1. ID フィールドに gorm:"primaryKey" を追加して、リレーションの主キーとして定義します
    2. gorm:"type:varchar[]" を使用して、フィールド GradesAchieved を DB 内の varchar の配列としてマップします。それ以外の場合は、役員テーブルの別の DB テーブルまたは追加の列として変換されます
  3. Grade 構造体は Scan 関数を実装します。ここでは、生の値を取得して調整し、g 変数にいくつかのフィールドを設定して、
  4. を返します。
  5. Grade 構造体は、値レシーバー型として Value 関数も実装します (今回はレシーバーを変更する必要はありません。* 参照は使用しません)。役員テーブル
  6. の列grade_achievedに書き込む値を返します。

これら 2 つのメソッドのおかげで、DB インタラクション中に Grade 型を送信および取得する方法を制御できます。次に、main.go ファイルを見てみましょう。

main.go ファイル?

ここでは、DB 接続を準備し、オブジェクトをリレーション (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))
}

Now, let's see the relevant sections of this file. First, we define the seedDB function to add dummy data in the DB. The data lives in the data.sql file with the following content:

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}');

The main() function starts by setting up a DB connection. For this demo, we used PostgreSQL. Then, we ensure the officers table exists in the database and is up-to-date with the newest version of the models.Officer struct. Since this program is a sample, we did two additional things:

  • Removal of the table at the end of the main() function (when the program terminates, we would like to remove the table as well)
  • Seeding of some dummy data

Lastly, to ensure that everything works as expected, we do a couple of things:

  1. Fetching all the records in the DB
  2. Adding (and fetching back) a new officer

That's it for this file. Now, let's test our work ?.

The Truth Moment

Before running the code, please ensure that a PostgreSQL instance is running on your machine. With Docker ?, you can run this command:

docker run -d -p 54322:5432 -e POSTGRES_PASSWORD=postgres postgres

Now, we can safely run our application by issuing the command: go run . ?

The output is:

[
        {
                "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
        }
}

Voilà! Everything works as expected. We can re-run the code several times and always have the same output.

That's a Wrap

I hope you enjoyed this blog post regarding Gorm and the Custom Data Types. I always recommend you stick to the most straightforward approach. Opt for this only if you eventually need it. This approach adds flexibility in exchange for making the code more complex and less robust (a tiny change in the structs' definitions might lead to errors and extra work needed).

Keep this in mind. If you stick to conventions, you can be less verbose throughout your codebase.

That's a great quote to end this blog post.
If you realize that Custom Data Types are needed, this blog post should be a good starting point to present you with a working solution.

Please let me know your feelings and thoughts. Any feedback is always appreciated! If you're interested in a specific topic, reach out, and I'll shortlist it. Until next time, stay safe, and see you soon!

以上がGorm: カスタム データ型の概要の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。