GORM には embedded というタグが用意されており、値オブジェクト(Value Object)チックな構造体を他の構造体に簡単に埋め込むことができそうなので、メモ。
値オブジェクト
値オブジェクトとは、住所情報のように、不変かつ交換可能なオブジェクトです。 "港区 六本木 1-1-x" という同じ値オブジェクトが 2 つ存在する場合、その 2 つは同等である必要があります。
今回は住所情報を持つ値オブジェクトチックな構造体を表現する Address Struct を用意し、他の構造体に埋め込む実装を行います。
"チック" と表現しているのは、今回の実装では値の再代入を禁止する方法がないため、完全な値オブジェクトと言えないと判断したためです。
Address Struct
住所情報を持つ Address Struct を用意する。Address Struct は Street (住所の通り情報) と City (住所の都市情報) を内部で持ち、それらを組み合せて使うメソッドが用意しています。
package valueobject type Address struct { Street string City string } // IsClose は引数で受ける住所情報と自身の住所情報を比較し、住所が近いかを真偽値で返す. func (a *Address) IsClose(address Address) bool { return a.City == address.City } // ToString は住所情報を結合した文字列を返す. func (a *Address) ToString() string { return a.Street + " " + a.City }
Customer Struct
実際に値オブジェクトを埋め込ませる構造体として Customer Struct を用意します。Customer Struct は Address (住所情報) を持つ構造体です。
ここで GORM の embedded タグを使って Address Struct を埋め込みます。
embeddedPrefix はカラム名のプレフィックスを省略するのに使います。
package entity import ( vo "github.com/tabakazu/value_object_sample/valueobject" ) type Customer struct { ID uint `gorm:"primaryKey"` Name string `gorm:"column:name"` Address vo.Address `gorm:"embedded;embeddedPrefix:address_"` } // これは下記と同等になる // type Customer struct { // ID uint `gorm:"primaryKey"` // Name string `gorm:"column:name"` // AddressStreet string // AddressCity string // }
DB と連携
実際に実装した Struct と DB のデータをマッピングしてみます。
事前に下記のテーブル、データを用意します。
CREATE TABLE `customers` ( `id` bigint unsigned NOT NULL AUTO_INCREMENT, `name` varchar(255) COMMENT '名前', `address_street` varchar(255) COMMENT '住所 通り', `address_city` varchar(255) COMMENT '住所 都市', `created_at` datetime(6) NOT NULL, `updated_at` datetime(6) NOT NULL, `deleted_at` datetime(6) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; INSERT INTO `customers` (name,address_street,address_city,created_at,updated_at)) VALUES ("John Snow", "Roppongi 1-1-x", "Minato-ku", NOW(), NOW());
実際に DB からデータを取得します。埋め込んだ値オブジェクトも正しくマッピングされており、定義した各メソッドを呼び出せます。
// DB からデータ取得 var cus entity.Customer db, _ := gorm.Open(mysql.Open(os.Getenv("MYSQL_URL")), &gorm.Config{}) db.First(&cus, "name = ?", "John Snow") // Address で定義された City を取得 cus.Address.City //=> "Minato-ku" // Address で実装した IsClose メソッド otherAddress := valueobject.Address{City: "Minato-ku"} cus.Address.IsClose(otherAddress) // => true otherAddress := valueobject.Address{City: "Shibuya-ku"} cus.Address.IsClose(otherAddress) // => false // Address で実装した ToString メソッド cus.Address.ToString() // Roppongi 1-1-1 Minato-ku // Address の比較 otherAddress := vo.Address{City: "Minato-ku", Street: "Roppongi 1-1-x"} cus.Address == otherAddress // true otherAddress := vo.Address{City: "Minato-ku", Street: "Roppongi 1-1-y"} cus.Address == otherAddress // false
データ挿入時も問題なく動作します。
cus := entity.Customer{ Username: "Tyrion Lannister", Address: vo.Address{ City: "Minato-ku", Street: "Roppongi 1-1-x", }, } db, _ := gorm.Open(mysql.Open(os.Getenv("MYSQL_URL")), &gorm.Config{}) if err := db.Create(&cus).Error; err != nil { fmt.Println(err) } else { fmt.Println("Success") } // Success
良き値オブジェクトを発見した時は是非使いたい機能でした。
以上です。