たばりばりスタイル

たばりばりスタイル

バリバリバリ⚡︎

GORMで値オブジェクトチックな構造体を埋め込む

GORM には embedded というタグが用意されており、値オブジェクト(Value Object)チックな構造体を他の構造体に簡単に埋め込むことができそうなので、メモ。

gorm.io

値オブジェクト

値オブジェクトとは、住所情報のように、不変かつ交換可能なオブジェクトです。 "港区 六本木 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

良き値オブジェクトを発見した時は是非使いたい機能でした。

以上です。