皆さん、おかえりなさい?!今日は、データベースとの間でデータをやり取りするときに直面する可能性のある具体的なユースケースについて説明します。まず、今日の課題の境界線を設定しましょう。現実の例に固執するために、アメリカ陸軍からいくつかの概念を借用しましょう。私たちの契約は、警察官のキャリアで達成した成績を保存して読み取るための小さなソフトウェアを作成することです。
私たちのソフトウェアは、それぞれの等級の陸軍将校を処理する必要があります。一見すると簡単そうに見えますが、おそらくここではカスタム データ タイプは必要ありません。ただし、この機能を示すために、従来とは異なる方法でデータを表現してみましょう。このおかげで、Go 構造体と DB リレーション間のカスタム マッピングを定義するように求められます。さらに、データを解析するための特定のロジックを定義する必要があります。プログラムのターゲットを見て、これをさらに詳しく見てみましょう?
作業を簡単にするために、図を使用してコードと SQL オブジェクトの関係を示してみましょう。
各コンテナに 1 つずつ注目してみましょう。
ここでは 2 つの構造体を定義しました。 Grade 構造体は、軍事等級の非網羅的なリストを保持します ?️。この構造体はデータベース内のテーブルにはなりません。逆に、Officer 構造体には ID、名前、Grade 構造体へのポインタが含まれており、これまでにその役員が達成したグレードを示します。
DB に役員を書き込むときは常に、列 Grade_achieved には、達成した成績 (Grade 構造体で true になっているもの) が入力された文字列の配列が含まれている必要があります。
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 では構造体として変換される) を扱うことから始めましょう。
まず、このファイルのコードを紹介します。
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 }
それでは、関連する部分を強調表示してみましょう?:
これら 2 つのメソッドのおかげで、DB インタラクション中に Grade 型を送信および取得する方法を制御できます。次に、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:
Lastly, to ensure that everything works as expected, we do a couple of things:
That's it for this file. Now, let's test our work ?.
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.
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 サイトの他の関連記事を参照してください。