고 언어 GORM 1:N (One To Many) 관계 모델링과 쿼리

5 분 소요

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

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

아주 간단한 예로 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": "파이썬 강좌 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 명령어를 사용해보겠습니다.

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": "파이썬 강좌 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 관계의 모델링 방법과 쿼리 방법에 대해서도 알아보도록 하겠습니다. 해피코딩~! 😎