Go言語 GORM 1:N (One To Many) 関係モデリングとクエリ

読了 4分

今回の記事ではGORMを使って一対多(one to many)の関係モデリングをする方法とクエリをする方法について見ていきましょう。

一対多の関係は、親と子、あるいは会社と社員、学校と生徒などの関係を指しますが、学校は多数の生徒を持つことができますが、生徒はただ1つの学校に属するという場合を考えていただければよいです。

ごく簡単な例として、Book(本)👉Shop(店)👉Region(地域)の関係を持ったテーブルを作っていきます。

まず、ごく簡単にモデリングをしていきましょう。

models.go
package models

type Region struct {
    ID int
    Name string
    Shops []Shop
}

type Shop struct {
    ID int
    Name string
    RegionID int
    Books []Book
}

type Book struct {
    ID int
    Name string
    Price float64
    ShopID int
}

私はMySQLを使います。 データベースにbook_storeというデータベースを作成し、次のコードを実行してテーブルを作成していきます。

main.go
package main

import (
	"github.com/sanghee911/bookstore/backend/src/models"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func Connect() {
	var err error
	DB, err = gorm.Open(mysql.New(mysql.Config{
		DSN:                       "root:root123@tcp(dev-server:3306)/book_store?charset=utf8mb4&parseTime=True&loc=Local",
		DefaultStringSize:         256,
		DisableDatetimePrecision:  true,
	}), &gorm.Config{})

	if err != nil {
		panic("Could not connect to the database!")
	}
}

func AutoMigrate() {
	err := DB.AutoMigrate(
		&models.Region{}, &models.Shop{}, &models.Book{},
	)

	if err != nil {
		panic(err)
	}
}

func main() {
	Connect()
	AutoMigrate()
}

データベースに次のようなテーブルが作成されました。一目で3つのテーブルがどのような関係を持っているかわかります。

models
models

次はデータを保存していきましょう。

main.go
package main

import (
	"github.com/sanghee911/bookstore/backend/src/models"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var DB *gorm.DB

func Connect() {
	var err error
	DB, err = gorm.Open(mysql.New(mysql.Config{
		DSN:                       "root:root123@tcp(dev-server:3306)/book_store?charset=utf8mb4&parseTime=True&loc=Local",
		DefaultStringSize:         256,
		DisableDatetimePrecision:  true,
	}), &gorm.Config{})

	if err != nil {
		panic("Could not connect to the database!")
	}
}

func AutoMigrate() {
	err := DB.AutoMigrate(
		&models.Region{}, &models.Shop{}, &models.Book{},
	)

	if err != nil {
		panic(err)
	}
}

func main() {
	Connect()
	AutoMigrate()
}

データはそれぞれのテーブルに保存され、外部キー(foreign key)として使われているregion_idとshop_idが自動で保存されたことがわかります。

regions table
regions table
shops table
shops table
books table
books table

SQLコマンドでそれぞれのテーブルを連結してデータを出力するには、JOINコマンドを使えばよいです。

SQL
select r.name as regionName, s.name as shopName, b.name as bookName from regions r join shops s on r.id = s.region_id join books b on s.id = b.shop_id;

すると、次のようなテーブルが出力されます。

次は同じテーブルをGORMが提供するPreloadメソッドを使って、テーブルをジョインして出力していきましょう。

main.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/sanghee911/bookstore/backend/src/models"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"log"
)

func main() {
	dsn := "root:root123@tcp(dev-server:3306)/book_store?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN: dsn,
	}), &gorm.Config{})

	if err != nil {
		log.Fatal(err)
	}

	var region models.Region
	db.Preload("Shops.Books").First(&region)
	data, _ := json.Marshal(region)
	var prettyJSON bytes.Buffer
	_ = json.Indent(&prettyJSON, data, "", "\t")
	fmt.Println(prettyJSON.String())
}

結果値はJSON形式で次のように出力されました。

{
	"ID": 1,
	"Name": "東京",
	"Shops": [
		{
			"ID": 1,
			"Name": "東京1号店",
			"RegionID": 1,
			"Books": [
				{
					"ID": 1,
					"Name": "Python講座 第1巻",
					"Price": 20000,
					"ShopID": 1
				},
				{
					"ID": 2,
					"Name": "Python講座 第2巻",
					"Price": 20000,
					"ShopID": 1
				},
				{
					"ID": 3,
					"Name": "Python講座 第3巻",
					"Price": 20000,
					"ShopID": 1
				}
			]
		},
		{
			"ID": 2,
			"Name": "東京2号店",
			"RegionID": 1,
			"Books": [
				{
					"ID": 4,
					"Name": "Go講座 第1巻",
					"Price": 20000,
					"ShopID": 2
				},
				{
					"ID": 5,
					"Name": "Go講座 第2巻",
					"Price": 20000,
					"ShopID": 2
				},
				{
					"ID": 6,
					"Name": "Go講座 第3巻",
					"Price": 20000,
					"ShopID": 2
				}
			]
		}
	]
}

テーブルのJOINはうまくいきましたが、上でSQLで実行したのと同じ形式のデータが出力されたわけではありません。SQLコマンドを実行したのと同じように、SELECTコマンドを使ってみましょう。

main.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"log"
)

type Result struct {
	RegionName string
	ShopName string
	BookName string
}

func main() {
	dsn := "root:root123@tcp(dev-server:3306)/book_store?charset=utf8mb4&parseTime=True&loc=Local"
	db, err := gorm.Open(mysql.New(mysql.Config{
		DSN: dsn,
	}), &gorm.Config{})

	if err != nil {
		log.Fatal(err)
	}

	var results []Result

	db.Table("regions").
		Select("regions.name as RegionName, shops.name as ShopName, books.name as BookName").
		Joins("join shops on shops.region_id = regions.id join books on books.shop_id = shops.id").
		Scan(&results)
	data, _ := json.Marshal(results)
	var prettyJSON bytes.Buffer
	_ = json.Indent(&prettyJSON, data, "", "\t")
	fmt.Println(prettyJSON.String())
}

SQLで実行したデータと同じデータが出力されました。

[
	{
		"RegionName": "東京",
		"ShopName": "東京1号店",
		"BookName": "Python講座 第1巻"
	},
	{
		"RegionName": "東京",
		"ShopName": "東京1号店",
		"BookName": "Python講座 第2巻"
	},
	{
		"RegionName": "東京",
		"ShopName": "東京1号店",
		"BookName": "Python講座 第3巻"
	},
	{
		"RegionName": "東京",
		"ShopName": "東京2号店",
		"BookName": "Go講座 第1巻"
	},
	{
		"RegionName": "東京",
		"ShopName": "東京2号店",
		"BookName": "Go講座 第2巻"
	},
	{
		"RegionName": "東京",
		"ShopName": "東京2号店",
		"BookName": "Go講座 第3巻"
	}
]

今後の記事で、1:1関係とN:N関係のモデリングとクエリ方法も整理します。

X