이번 포스트에서는 GORM을 사용하여 일 대 다수 (one to many) 관계 모델링을 하는 방법과 쿼리를 하는 방법에 대해서 알아보도록 하겠습니다.

일 대 다수의 관계는 부모와 자식, 또는 회사와 직원, 학교와 학생 등의 관계를 말할 수 있는데요, 학교는 다수의 학생을 가질 수 있지만, 학생은 단 하나의 학교에 속하는 경우를 생각하시면 됩니다.

아주 간단한 예로 Book (책) -> Shop (가게) -> Region (지역) 의 관계를 가진 테이블을 만들어 보겠습니다.

먼저 아주 간단하게 모델링을 해보도록 하겠습니다.

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라는 데이터베이스를 생성하시고 아래의 코드를 실행해서 테이블을 생성하도록 하겠습니다.

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개의 테이블이 어떤 관계를 갖고 있는지 알 수 있습니다.

이번에는 데이터를 저장해보도록 하겠습니다.

package main

import (
	"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)
	}

	region := models.Region{
		Name: "서울",
		Shops: []models.Shop{
			{
				Name: "서울 1호점",
				Books: []models.Book{
					{
						Name: "파이썬 강좌 1편",
						Price: 20000,
					},
					{
						Name: "파이썬 강좌 2편",
						Price: 20000,
					},
					{
						Name: "파이썬 강좌 3편",
						Price: 20000,
					},
				},
			},
			{
				Name: "서울 2호점",
				Books: []models.Book{
					{
						Name: "고 강좌 1편",
						Price: 20000,
					},
					{
						Name: "고 강좌 2편",
						Price: 20000,
					},
					{
						Name: "고 강좌 3편",
						Price: 20000,
					},
				},
			},
		},
	}

	db.Create(&region)
}

데이터는 각각의 테이블로 저장이 되고 외래키 (foreign key) 로 사용되고 있는 region_id와 shop_id가 자동으로 저장된 것을 알 수 있습니다.

regions
shops
books

SQL 명령어로 각각의 테이블을 연결해서 데이터를 출력하려면 JOIN 명령어를 사용하면 됩니다.

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 메소드를 사용해서 테이블을 조인해서 출력해보도록 하겠습니다.

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": "파이썬 강좌 1편",
					"Price": 20000,
					"ShopID": 1
				},
				{
					"ID": 2,
					"Name": "파이썬 강좌 2편",
					"Price": 20000,
					"ShopID": 1
				},
				{
					"ID": 3,
					"Name": "파이썬 강좌 3편",
					"Price": 20000,
					"ShopID": 1
				}
			]
		},
		{
			"ID": 2,
			"Name": "서울 2호점",
			"RegionID": 1,
			"Books": [
				{
					"ID": 4,
					"Name": "고 강좌 1편",
					"Price": 20000,
					"ShopID": 2
				},
				{
					"ID": 5,
					"Name": "고 강좌 2편",
					"Price": 20000,
					"ShopID": 2
				},
				{
					"ID": 6,
					"Name": "고 강좌 3편",
					"Price": 20000,
					"ShopID": 2
				}
			]
		}
	]
}

테이블의 JOIN은 잘 됐지만 위에서 SQL로 실행한 것과 같은 형식의 데이터가 출력되지는 않았습니다. SQL 명령어를 실행한 것과 같이 SELECT 명령어를 사용해보겠습니다.

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": "파이썬 강좌 1편"
	},
	{
		"RegionName": "서울",
		"ShopName": "서울 1호점",
		"BookName": "파이썬 강좌 2편"
	},
	{
		"RegionName": "서울",
		"ShopName": "서울 1호점",
		"BookName": "파이썬 강좌 3편"
	},
	{
		"RegionName": "서울",
		"ShopName": "서울 2호점",
		"BookName": "고 강좌 1편"
	},
	{
		"RegionName": "서울",
		"ShopName": "서울 2호점",
		"BookName": "고 강좌 2편"
	},
	{
		"RegionName": "서울",
		"ShopName": "서울 2호점",
		"BookName": "고 강좌 3편"
	}
]

다음에 기회가 되면 1:1 관계와 N:N 관계의 모델링 방법과 쿼리 방법에 대해서도 알아보도록 하겠습니다. 해피코딩~! 😎