Many-to-many関係を使う

今回は GORM で Many-to-many 関係を扱う。

複数のユーザと複数の楽曲が用意されていて、各ユーザのお気に入りの楽曲を格納できるようにする。E-R図にするとこんな感じ。

f:id:taegawa:20170114140153p:plain

とりあえずコードはこんな感じ。

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
	FavMusics []Music `gorm:"many2many:user_fav_musics;"`
}

type Music struct {
	gorm.Model
	Title string
}

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

	for _, id := range []uint{1, 2, 3} {
		var user User
		db.First(&user, id).Related(&user.FavMusics, "FavMusics")
		fmt.Println("---------------------------")
		fmt.Printf("%s さんのお気に入り曲\n", user.Name)
		for _, music := range user.FavMusics {
			fmt.Printf("曲名: %s\n", music.Title)
		}
	}
	fmt.Println("---------------------------")
}

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

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

	prepareUsers(db)
	prepareMusics(db)
	prepareFavMusics(db)

	return db
}

func prepareUsers(db *gorm.DB) {
	var usernames = []string{"Mario", "Luigi", "Peach"}

	for _, username := range usernames {
		user := User{
			Name: username,
		}
		db.Create(&user)
	}
}

func prepareMusics(db *gorm.DB) {
	var titles = []string{
		"放課後ロマンス",
		"私Loveなオトメ",
		"ニーハイエゴイスト",
		"禁断無敵のダーリン",
		"S・M・L",
	}

	for _, title := range titles {
		music := Music{
			Title: title,
		}
		db.Create(&music)
	}
}

func prepareFavMusics(db *gorm.DB) {
	//  お気に入り楽曲
	//  {user.ID, music.ID} の組
	var favorites = [][]uint{
		{1, 1}, {1, 2}, {1, 4},
		{2, 2}, {2, 3}, {2, 5},
		{3, 2}, {3, 5},
	}
	for _, favorite := range favorites {
		var user User
		user.ID = favorite[0]

		var music Music
		music.ID = favorite[1]

		db.Model(&user).Association("FavMusics").Append(&music)
	}
}

(実行結果)

---------------------------
Mario さんのお気に入り曲
曲名: 放課後ロマンス
曲名: 私Loveなオトメ
曲名: 禁断無敵のダーリン
---------------------------
Luigi さんのお気に入り曲
曲名: 私Loveなオトメ
曲名: ニーハイエゴイスト
曲名: S・M・L
---------------------------
Peach さんのお気に入り曲
曲名: 私Loveなオトメ
曲名: S・M・L
---------------------------

Model の定義

type User struct {
	gorm.Model
	Name      string
	FavMusics []Music `gorm:"many2many:user_fav_musics;"`
}

type Music struct {
	gorm.Model
	Title string
}

モデルはユーザ(User)と楽曲(Music)のみ定義する。User 側に FavMusics というフィールドを追加し、ここにお気に入りの楽曲が格納されるようにする。また FavMusics フィールドのタグで、many-to-many 関係を使用すること、および関係テーブルの名称を上記のように宣言する。AutoMigration 使用時は GORM が関係テーブル user_fav_musics を自動的に作成してくれる。

検索

		db.First(&user, id).Related(&user.FavMusics, "FavMusics")

特定ユーザに紐づくお気に入り楽曲を取得する場合は、Related()の第2引数でフィールド名を指定する。LogMode を true にすれば、上記を実行したとき以下のようなSQL文が発行されているのが分かる。

User 検索

SELECT * 
  FROM `users`  
 WHERE `users`.deleted_at IS NULL 
    AND ((`users`.`id` = '3')) 
 ORDER BY `users`.`id` ASC LIMIT 1

User のお気に入り楽曲を検索

SELECT `musics`.* 
  FROM `musics` 
    INNER JOIN `user_fav_musics` ON `user_fav_musics`.`music_id` = `musics`.`id` 
 WHERE `musics`.deleted_at IS NULL 
    AND ((`user_fav_musics`.`user_id` IN ('3'))) 
 ORDER BY `musics`.`id` ASC

関係の追加

		var user User
		user.ID = favorite[0]

		var music Music
		music.ID = favorite[1]

		db.Model(&user).Association("FavMusics").Append(&music)

多分 Association Mode を使う方法が一番素直なのではないかと。Association() の引数で関係を定義したフィールド名を指定し、Append()で追加する。ちなみに user, music は、両者の関連を特定するために必要なフィールド(ここではID)だけ定義されていれば十分。あらかじめすべてのフィールドをDBから問い合わせておく必要はない。

INSERT INTO `user_fav_musics` (`music_id`,`user_id`) 
SELECT '1','1' 
  FROM DUAL 
 WHERE NOT EXISTS 
    (SELECT * FROM `user_fav_musics` WHERE `music_id` = '1' AND `user_id` = '1')

発行されるSQL文は上記の通り。同じ組み合わせがすでに存在する場合、二重に登録されることはないようだ。