ตัวอย่างการสร้าง Many to Many Association ผ่าน GORM
Tags: go, gorm
Published: 2020-06-07 12:35:24

GORM เป็น Go ORM ช่วยให้เรา Map ระหว่างโครงสร้างของ Column ใน Table ของ Database กับ Field ของ Struct ใน Go ได้

ตัวอย่างโพสต์นี้เราจะให้เห็นการ Map ของโครงสร้างที่มีความสัมพันธ์กันแบบ Many to Many เช่นเรามี table `actors` และมี table `films` ซึ่งแต่ละ record ของ `actors` จะโยงกับ `films` ได้หลายเรื่อง และ `films` เองก็โยงกับ `actors` ได้หลาย `actors` ซึ่งการเก็บข้อมูลสำหรับความสัมพันธ์แบบนี้ เราจะสร้างอีก table เพื่อโยง primary key ของ `actors` กับ primary key ของ `films` เข้าด้วยกัน ดังนี้

```sql
CREATE TABLE IF NOT EXISTS actor_films(
    actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE,
    film_id INTEGER NOT NULL REFERENCES films(id) ON DELETE CASCADE,
    PRIMARY KEY (actor_id, film_id)
);
```

ในมุมของ struct ใน Go เราจะสร้าง struct `Actor` และมี field `Films` เพื่อเก็บ list ของ `Film` ที่โยงกับ `Actor` แล้วเราจะใช้ `gorm:"many2many:actor_films;"` struct tag เพื่อบอกว่า Fields นี้จะถูกโหลดข้อมูลของ Film โดยอาศัยความสัมพันธ์ผ่านตาราง `actor_films` นั่นเอง

```go
type Actor struct {
	ID        uint      `json:"id"`
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
	Films     []Film    `json:"films" gorm:"many2many:actor_films;"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}
```

ในตัวอย่างนี้เราก็มีอีกคู่ที่เป็น Many to Many นั่นคือ `films` กับ `categories` เราก็อาศัย table เพื่อโยงความสัมพันธ์เช่นกันชื่อ `film_catagories`

```sql
CREATE TABLE IF NOT EXISTS film_categories(
    film_id INTEGER NOT NULL REFERENCES films(id) ON DELETE CASCADE,
    category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
    PRIMARY KEY (film_id, category_id)
);
```

แล้วใน `Film` struct เราก็กำหนด gorm struct tag แบบนี้

```go
type Film struct {
	ID         uint       `json:"id"`
	Title      string     `json:"title"`
	LanguageID uint       `json:"-"`
	Language   Language   `json:"language"`
	Categories []Category `json:"categories" gorm:"many2many:film_categories;"`
	CreatedAt  time.Time  `json:"created_at"`
	UpdatedAt  time.Time  `json:"updated_at"`
}
```

สุดท้ายเมื่อเรา map field โดยอาศัย gorm struct tag แล้วเราสามารถใช้ method `Preload` เพื่อสั่งใช้ query association field อย่าง `Films` และ `Categories` หรือ `Language` ของ `Film` ก็ได้ เช่น

```go
var dest []Actor
	db.Preload("Films.Language").Preload("Films.Categories").Find(&dest)
	json.NewEncoder(os.Stdout).Encode(&dest)
```

### โค้ด table ทั้งหมดในตัวอย่าง
```sql
BEGIN;

CREATE TABLE IF NOT EXISTS actors(
    id SERIAL NOT NULL PRIMARY KEY,
    created_at TIMESTAMPTZ NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL,
    first_name CHARACTER VARYING,
    last_name CHARACTER VARYING
);

CREATE TABLE IF NOT EXISTS languages(
    id SERIAL NOT NULL PRIMARY KEY,
    created_at TIMESTAMPTZ NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL,
    name CHARACTER VARYING
);

CREATE TABLE IF NOT EXISTS films(
    id SERIAL NOT NULL PRIMARY KEY,
    created_at TIMESTAMPTZ NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL,
    title CHARACTER VARYING,
    language_id INTEGER NOT NULL REFERENCES languages(id)
);

CREATE TABLE IF NOT EXISTS categories(
    id SERIAL NOT NULL PRIMARY KEY,
    created_at TIMESTAMPTZ NOT NULL,
    updated_at TIMESTAMPTZ NOT NULL,
    name CHARACTER VARYING
);

CREATE TABLE IF NOT EXISTS film_categories(
    film_id INTEGER NOT NULL REFERENCES films(id) ON DELETE CASCADE,
    category_id INTEGER NOT NULL REFERENCES categories(id) ON DELETE CASCADE,
    PRIMARY KEY (film_id, category_id)
);

CREATE TABLE IF NOT EXISTS actor_films(
    actor_id INTEGER NOT NULL REFERENCES actors(id) ON DELETE CASCADE,
    film_id INTEGER NOT NULL REFERENCES films(id) ON DELETE CASCADE,
    PRIMARY KEY (actor_id, film_id)
);

COMMIT;
```

### โค้ด Go ที่ใช้ GORM ช่วย map field
```go
package main

import (
	"encoding/json"
	"log"
	"os"
	"time"

	"github.com/jinzhu/gorm"
	_ "github.com/jinzhu/gorm/dialects/postgres"
)

type Actor struct {
	ID        uint      `json:"id"`
	FirstName string    `json:"first_name"`
	LastName  string    `json:"last_name"`
	Films     []Film    `json:"films" gorm:"many2many:actor_films;"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

type Film struct {
	ID         uint       `json:"id"`
	Title      string     `json:"title"`
	LanguageID uint       `json:"-"`
	Language   Language   `json:"language"`
	Categories []Category `json:"categories" gorm:"many2many:film_categories;"`
	CreatedAt  time.Time  `json:"created_at"`
	UpdatedAt  time.Time  `json:"updated_at"`
}

type Language struct {
	ID        uint      `json:"id"`
	Name      string    `json:"name"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

type Category struct {
	ID        uint      `json:"id"`
	Name      string    `json:"name"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

func initialData(db *gorm.DB) {
	tx := db.Begin()
	defer tx.Commit()

	lang := Language{
		Name: "English",
	}
	tx.Create(&lang)
	cate := Category{
		Name: "Documentary",
	}
	tx.Create(&cate)
	film := Film{
		Title:      "Young Language",
		Language:   lang,
		Categories: []Category{cate},
	}
	tx.Create(&film)
	actor := Actor{
		FirstName: "Ed",
		LastName:  "Chase",
		Films:     []Film{film},
	}
	tx.Create(&actor)
}

func main() {
	connURL := os.Getenv("POSTGRESQL_URL")
	log.Println(connURL)
	db, err := gorm.Open("postgres", connURL)
	if err != nil {
		panic("failed to connect database")
	}
	defer db.Close()

	initialData(db)

	var dest []Actor
	db.Preload("Films.Language").Preload("Films.Categories").Find(&dest)
	json.NewEncoder(os.Stdout).Encode(&dest)
}
```

หรือจะ clone ไปลองเล่นได้ทีนี่ https://github.com/iporsut/example-gorm-many-to-many