Association で任意の外部キー名を使う

初詣行ってきました

大吉引いて幸先良いです。
あと、巫女さんが可愛かったです。

本題

GORM の Association 関連で、任意の外部キー名を使用しようとしてハマったのでメモ。

標準の外部キー名を使う方法

その前に、まずは Convention に従った外部キー名を使う方法。
これはGORMのマニュアルにも書かれている通り。今回は Belongs-to の関連で試してみる。

f:id:taegawa:20170108063437p:plain

今回は上記の通り、ユーザ(users)と、それに紐づくアイコンイメージ(images)をデータとして保持できるようにする。アイコンイメージには任意のイメージ名と、そのURLを持っている。

※以下の例では association という database を使用する。もともと同名の database があって、かつ users, images というテーブルがあると一旦削除されてしまうので注意。

package main

import (
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jinzhu/gorm"
)

const (
	dsn = "root@/association?parseTime=true&loc=Asia%2fTokyo"
)

type User struct {
	gorm.Model
	Name    string
	Image   Image
	ImageID uint
}

type Image struct {
	gorm.Model
	Name string
	Url  string
}

func main() {
	db := prepare()
	defer db.Close()

	for _, id := range []uint{1, 2, 3} {
		var user User
		fmt.Println("-------------------------------")
		db.First(&user, id).Related(&user.Image)
		fmt.Printf("名前    : %s\n", user.Name)
		fmt.Printf("アイコン:\n")
		fmt.Printf("  名称: %s\n", user.Image.Name)
		fmt.Printf("  URL : %s\n", user.Image.Url)
	}
	fmt.Println("-------------------------------")
}

func prepare() *gorm.DB {
	db, err := gorm.Open("mysql", dsn)
	if err != nil {
		panic("Failed to connect database")
	}
	db.DropTableIfExists(&User{}, &Image{})

	db.AutoMigrate(&Image{})
	db.AutoMigrate(&User{})

	var members = []map[string]string{
		{"Name": "ミク", "ImageFile": "miku.jpg"},
		{"Name": "マホ", "ImageFile": "maho.jpg"},
		{"Name": "コヒメ", "ImageFile": "kohime.jpg"},
	}

	for _, member := range members {
		image := Image{
			Name: member["Name"] + "アイコン",
			Url:  "https://image.example.com/" + member["ImageFile"],
		}
		db.Create(&image)

		user := User{
			Name:    member["Name"],
			ImageID: image.ID,
		}
		db.Create(&user)
	}

	return db
}

実行結果

-------------------------------
名前    : ミク
アイコン:
  名称: ミクアイコン
  URL : https://image.example.com/miku.jpg
-------------------------------
名前    : マホ
アイコン:
  名称: マホアイコン
  URL : https://image.example.com/maho.jpg
-------------------------------
名前    : コヒメ
アイコン:
  名称: コヒメアイコン
  URL : https://image.example.com/kohime.jpg
-------------------------------

Userのモデルは以下のように定義されている。

type User struct {
	gorm.Model
	Name    string
	Image   Image
	ImageID uint
}

ここでは、フィールド Image が Image型であることが宣言されているが、その取得元レコードへの外部キーが ImageID であることは特に明示していない。また、問い合わせで

		db.First(&user, id).Related(&user.Image)

としているが、Related()で関連レコードを取得する際にも、外部キーが ImageID であることを明示していない。

それでも Related() が問題なく外部キーを特定し関連レコードを取得できるのは、特に指示のない限り、

  1. 関連モデルの名称 + 'Id'
  2. 自身のモデルの名称 + 'Id'

を外部キーとみなすようになっているから。
ここでは Image が Image型であるため、

  1. ImageID
  2. UserID

の順に外部キーを探していき、同名のキーが見つかった時点でそれを使用する。

任意の外部キーを使う方法

ところが、外部キーの名称を (モデル名) + 'Id' 「以外」にしたいケースというのも多々ある。例えば今回はアイコンイメージのIDを ImageID というキー名で参照しているが、これはあまり良い名称とは言えない。アイコンイメージであるなら分かりやすく IconImageID としたほうがいいし、また背景画像など、同じ Image モデルを参照する別のフィールドが追加されたときは、少なくともどちらかは ImageID 以外の外部キー名を使わなければならない。

そこで User モデルの ImageID を IconImageID に変更してみる。

type User struct {
	gorm.Model
	Name        string
	IconImage   Image
	IconImageID uint
}

名称変更自体はこれで良いのだが、IconImageID というキー名はこのままでは外部キーとして認識されない。これを解決する方法はいくつかあるが、GORM ドキュメントの Association の項で触れられている範囲内では2つのやり方がある。

Related() で明示的に外部キーを指定する方法

Related() の第2引数で外部キー名を明示的に指定することができる。

        db.First(&user, id).Related(&user.IconImage, "IconImageID")

お手軽。

Association Mode を使う

モデルを宣言する際、モデル間の関連についてタグで指定することができる。

type User struct {
	gorm.Model
	Name        string
	IconImage   Image `gorm:"ForeignKey:IconImageID"`
	IconImageID uint
}

上記のようにすると、「IconImage でレコードを取得する際は、外部キーとして IconImageID を使用する」と明示的に指示できる。
そして関連レコードを取得する際は、

        db.First(&user, id).Association("IconImage").Find(&user.IconImage)

のように、Association で取得対象のフィールド名を指定すれば、タグで指定したものが外部キーとして使用される。

※修正済みのソースはこちら