이번 포스트에서는 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"
)
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가 자동으로 저장된 것을 알 수 있습니다.



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(®ion)
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 관계의 모델링 방법과 쿼리 방법에 대해서도 알아보도록 하겠습니다. 해피코딩~! 😎