GORM ではモデル間の関連を実装する際に、あるモデル構造体のメンバーに別のモデル構造体のメンバーを定義し Preload を行うことで関連の参照を引っ張ることができる。
その関連の型は HasOne である場合はモデル構造体、またはモデル構造体のポインタ。 HasMany である場合はモデル構造体の配列となる。その際実装で初期状態と実際にPreloadしたがデータが存在しなかった場合どうなるかあいまいであったのでまとめることとした。
検証
例えば Human というモデルがあったとして Father/Mother は必ず存在するし、それに紐づく PersonalInfo は必ず存在する。動物も飼うかもしれないから Dogs という任意の数の犬をペットとして保持できるようなモデルを考えよう。それを golang / gorm で実装すると下記のようになる。
type Human struct {
gorm.Model
FatherId *uint64
MotherId *uint64
PersonalInfoId uint64
Name string
Father *Human
Mother *Human
PersonalInfo PersonalInfo
Dogs []Dog
}
type PersonalInfo struct {
gorm.Model
Tel string
Address string
}
type Dog struct {
gorm.Model
ManId uint64
Name string
}
ちなみに Father / Mother については必ず存在するという過程だが、 golang / gorm を用いた実装ではポインタを用いる必要があった。ポインタにしないと自己参照となってしまい永遠にその構造を定義できないからだ。
検証用データ
これらを検証するために今回は下記のようなデータを用意した。適当に goose を用いている。
-- +goose Up
-- SQL in section 'Up' is executed when this migration is applied
create table human (
id bigint unsigned primary key,
father_id bigint unsigned,
mother_id bigint unsigned,
personal_info_id bigint unsigned,
name varchar(64) not null,
created_at datetime,
updated_at datetime,
deleted_at datetime
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4;
create table personal_info (
id bigint unsigned primary key,
tel varchar(64),
address varchar(64),
created_at datetime,
updated_at datetime,
deleted_at datetime
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4;
create table dog (
id bigint unsigned primary key,
name varchar(64),
human_id bigint unsigned,
created_at datetime,
updated_at datetime,
deleted_at datetime
) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4;
insert into personal_info(id, tel, address) values(1, '090-1', 'japan');
insert into human(id, father_id, mother_id, personal_info_id, name) values(1, null, null, 1, 'root man');
insert into personal_info(id, tel, address) values(2, '080-1', 'japan');
insert into human(id, father_id, mother_id, personal_info_id, name) values(2, null, null, 2, 'root woman');
insert into personal_info(id, tel, address) values(3, '090-2', 'japan');
insert into human(id, father_id, mother_id, personal_info_id, name) values(3, 1, 2, 3, 'taro');
insert into personal_info(id, tel, address) values(4, '090-3', 'japan');
insert into human(id, father_id, mother_id, personal_info_id, name) values(4, 1, 2, 4, 'jiro');
insert into dog(id, name, human_id) values(1, 'pochi', 4);
insert into dog(id, name, human_id) values(2, 'kuro', 4);
-- +goose Down
-- SQL section 'Down' is executed when this migration is rolled back
drop table human;
drop table personal_info;
drop table dog;
検証コード
簡単に下記のようなコードを用意して検証。
package main
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"sandbox/main/model"
)
func main() {
var rootMan model.Human
var rootWoman model.Human
var taro model.Human
var jiro model.Human
var err error
var db *gorm.DB
if db, err = gorm.Open("mysql", "root:@tcp(127.0.0.1:3305)/go_sandbox?charset=utf8mb4&parseTime=True&loc=Local"); err != nil {
fmt.Printf("unable to open database %+v\n", err)
return
}
db.SingularTable(true)
db.Preload("Father").Preload("Mother").Preload("Dogs").Preload("PersonalInfo").Where("id = 1").First(&rootMan)
db.Preload("Father").Preload("Mother").Preload("Dogs").Preload("PersonalInfo").Where("id = 2").First(&rootWoman)
db.Preload("Father").Preload("Mother").Preload("Dogs").Preload("PersonalInfo").Where("id = 3").First(&taro)
db.Preload("Father").Preload("Mother").Preload("Dogs").Preload("PersonalInfo").Where("id = 4").First(&jiro)
}
結果
モデル | モデルのポインタ | モデルの配列 | |
初期状態 | ゼロ初期化されたモデルインスタンス | nil | nil |
Preloadで参照が存在しない | – | nil | 要素0の配列 |
Preloadで参照が存在する | データを持ったモデルインスタンス | データを持ったモデルインスタンスのポインタ | 要素1以上の配列 |
考察
これで何が言えるかというと関連を使ったドメインロジックを行う際などに、本当にデータがないのか、それともPreloadが行われていないのかという切り分けができる。
例えばサービスロジックがモデルの関連が Preload されている前提でロジックが実装されているとすると、あるコンテキスト上では Preload されているが、あるコンテキスト上では Preload されていない場合、片方では正しい結果が得られないというバグが発生しうる。その際に Preload されていなければそのサービスの方で Preload を担保してあげるという実装ができる。
そもそもそのロジックやめたほうがいいという主張があることは百も承知であるが、GORMの機能として実装されている以上こういう実装がまかり通っている実装に直面する場合もある。やむを得ず最適化せざる得ない場合などには使えるのではないかと思う。
個人的には GORM を使うにしても Preload などせずに Repository などを設計するのが良さそう。でも ORM って多機能性をアピールするためにこういうの設計しがちだよね。
コメントを残す