GORMにおける関連参照の初期状態とPreload時の状態について調べた

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

結果

モデルモデルのポインタモデルの配列
初期状態ゼロ初期化されたモデルインスタンスnilnil
Preloadで参照が存在しないnil要素0の配列
Preloadで参照が存在するデータを持ったモデルインスタンスデータを持ったモデルインスタンスのポインタ要素1以上の配列

考察

これで何が言えるかというと関連を使ったドメインロジックを行う際などに、本当にデータがないのか、それともPreloadが行われていないのかという切り分けができる。

例えばサービスロジックがモデルの関連が Preload されている前提でロジックが実装されているとすると、あるコンテキスト上では Preload されているが、あるコンテキスト上では Preload されていない場合、片方では正しい結果が得られないというバグが発生しうる。その際に Preload されていなければそのサービスの方で Preload を担保してあげるという実装ができる。

そもそもそのロジックやめたほうがいいという主張があることは百も承知であるが、GORMの機能として実装されている以上こういう実装がまかり通っている実装に直面する場合もある。やむを得ず最適化せざる得ない場合などには使えるのではないかと思う。

個人的には GORM を使うにしても Preload などせずに Repository などを設計するのが良さそう。でも ORM って多機能性をアピールするためにこういうの設計しがちだよね。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください