GO: Database Restful API (GIN+GORM)
Published: 2022-09-04 11:20:07

## สร้าง Project

เริ่มต้นสร้าง Go project ชื่อ tripbooking (project ตัวอย่าง) กันก่อนเลย โดยสร้าง folder ชื่อ tripbooking  แล้วเข้าไป folder รันคำสั่งสร้าง project และเปิด vscode ที่ folder นั้น

```
mkdir tripbooking 
cd tripbooking 
go mod init tripbooking 
code .
```

---

สร้าง folder ที่จะเอาไว้เก็บไฟล์ต่างๆให้เป็นระเบียบ โดยในที่นี่ขึ้นอยู่กับความเหมาะสม สำหรับที่จะแนะนำให้สร้าง folder ทั้งหมด 4 folder คือ 

- controller = เอาไว้เก็บ restful api controller (path) ต่างๆ
- service = ส่วนประมวลผลต่างๆของ Api
- model = struct ของ database model 
- repository = ส่วนที่เชื่อมต่อไปยัง database

และสร้างไฟล์อีก 3 ไฟล์คือ
- config.yaml = ไฟล์การตั้งค่าต่างๆของ project
- server.go = ไฟล์เริ่มต้นการทำงานของ Server โดย GIN
- main.go = ไฟล์เริ่มต้นการทำงานของ project

โดยใน main.go ก็จะมี code ของ func main() เริ่มต้น ตามรูป


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9rkzwqqqrljp78s3ptcn.png)


---

เพิ่ม libraries ที่เกี่ยวข้องกับ project 

```
go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/mysql
go get github.com/spf13/viper
go install github.com/cosmtrek/air@latest
```

---

## สร้าง Server ด้วย GIN

ที่ไฟล์ server.go สร้าง server ของ restful api ด้วย GIN

```go
package main

import (
	"net/http"
	"os"
	"github.com/gin-gonic/gin"
)

func StartServer() {
	router := gin.Default()
	router.SetTrustedProxies(nil)
	router.GET("/", hello)
	port := os.Getenv("PORT")
	if port == "" {
		port = "8888"
	}
	router.Run(":" + port)
}

func hello(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"message": "Hello World!!!",
	})
}

```

ใน main.go สั่งให้เรียก func StartServer()

```go
package main

func main() {
	StartServer()
}

```

รันโปรแกรมด้วย plugin air เพื่อให้ auto reload เมื่อเราแก้ไข code โดยต้องกำหนดค่าเริ่มต้นให้ก่อน

```
air init
air
```
ทดสอบเรียก Api อาจใช้ Thunder Client, Talend หรือ Postman ก็ได้


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w10mwk579c9t6pe396r1.png)

---

## ติดต่อ Database ด้วย GORM
ตั้งค่า dsn ที่จะเชื่อมไปยังฐานข้อมูล MySQL ในไฟล์ config.yaml (อย่าลืมเปิด Server MySQL ตัวอย่างล่ะ หรือ ถ้าเชื่อมไปยัง Server ที่ Online อยู่ก็ได้เลย) 

```yaml
mysql:
  dsn: "username:password@tcp(localhost:3306)/tripbooking?collation=utf8mb4_unicode_ci&parseTime=true"
```

จากนั้น สร้างไฟล์ database.go ใน folder repository แล้วเขียนคำสั่งใช้การติดต่อไปยังฐานข้อมูล

```go
package repository


import (
	"github.com/spf13/viper"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

var db *gorm.DB

func NewDatabaseConnection() (*gorm.DB, error) {
	if db == nil {
		viper.SetConfigName("config")
		viper.AddConfigPath(".")
		err := viper.ReadInConfig()
		if err != nil {
			panic(err)
		}
		dsn := viper.GetString("mysql.dsn")
		dialector := mysql.Open(dsn)
		db, err = gorm.Open(dialector)
		if err != nil {
			return nil, err
		}
	}
	return db, nil
}
```

ซึ่งได้จะ func NewDatabaseConnection() ไว้ใช้ในการติดต่อไปยังฐานข้อมูลที่เราได้ตั้งค่าไว้

---
ติดต่อไปดึงข้อมูลจากฐานข้อมูล (Table) ต้องมี Model (Entity) ที่เป็นตัวแทนของ Table นั้นๆ ตัวอย่างเช่น จะดึงข้อมูลจาก Table destination ก็ต้องสร้าง Model Destination ขี้นมาก่อน

สร้างไฟล์ destinationModel.go ใน folder model และเขียน model ตามโครงสร้างของ table destination

```go
package model

type Destination struct {
	ID   uint   `gorm:"column:idx;primaryKey"`
	Zone string `gorm:"column:zone;size:255"`
}

func (Destination) TableName() string {
	return "destination"
}
```

ในกรณีที่ไม่อยากเขียน struct ของ model เอง ให้ใช้วิธีนี้คือ

- เข้า phpmyadmin แล้วรันคำสั่ง (destination คือชื่อ table ที่ต้องการ) โดยเลือกการแสดงผลตรง option ให้เป็น แบบ Full Text

```
SHOW CREATE TABLE destination
```


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4cd1kfch1d8detju66yb.png)


- จะได้คำสั่งสร้าง Table ออกมา ให้ copy คำสั่ง Create นั้นไว้ แล้วเข้าที่เวบ https://sql2gorm.mccode.info
- วางคำสั่ง sql ลงไป เวบจะสร้าง struct ของ model ให้ตามรูป (เลือก json tag เป็น include ด้วยนะ ถ้าจะใช้เป็น DTO รับข้อมูลรูปแบบ json)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l0gdev0hqgzpg4xme4el.png)



---

สร้าง Repositoy ที่เป็นตัวเชื่อมไปยังฐานข้อมูล โดยในการเขียนนี้ เราจะสร้างให้อยู่ในรูปแบบ Interface (ถ้ายังไม่รู้จักจะงงๆหน่อยนะ) เพื่อให้สะดวกต่อการใช้งาน อาจดูยุ่งยาก แต่มันดีในอนาคตเวลาโปรแกรมมันขยายและซับซ้อนขึ้น

สร้างไฟล์ destinationRepository.go ไว้ใน folder repository และเขียนให้ไปดึงข้อมูลของ table destination มา (FindAll) โดยมี pattern ของการเขียนดังตัวอย่าง

```go
package repository

import (
	"tripbooking/model"

	"gorm.io/gorm"
)

// repository struct (<name>DB)
type destinationRepo struct {
	db *gorm.DB
}

// repository contructor (New<name>Repository)
func NewDestinationRepository() DestinationRepository {
	db, err := NewDatabaseConnection()
	if err != nil {
		return nil
	}
	return destinationRepo{db: db}
}

// repository interface (<name>Repository)
type DestinationRepository interface {
	FindAll() (*[]model.Destination, error)
}

// implement functions conformed interface
func (d destinationRepo) FindAll() (*[]model.Destination, error) {
	destinations := []model.Destination{}
	result := d.db.Find(&destinations)
	if result.Error != nil {
		return nil, result.Error
	}
	return &destinations, nil
}
```

---

เมื่อสร้าง Repository เสร็จแล้ว ขี้นต่อไปคือการสร้าง Service เพื่อใช้ประมวลผลต่างๆ 

**เราจะไม่ประมวลใน repository และ controller ถ้าไม่จำเป็น**

สร้างไฟล์ destinationService.go ใน folder service เพื่อใช้เขียนการประมวลผลในรูปแบบของ service ซึ่งจะมี pattern การเขียนแบบ interface  เหมือน repository

```go
package service

import (
	"tripbooking/model"
	"tripbooking/repository"
)

// service struct (<name>Serv)
type destinationServ struct{}

// service contructor (New<name>Service)
func NewDestinationService() DestinationService {
	return destinationServ{}
}

// create repositories
var destinationRepo = repository.NewDestinationRepository()

// service interface (<name>Service)
type DestinationService interface {
	GetAllDestinations() (*[]model.Destination, error)
}

func (d destinationServ) GetAllDestinations() (*[]model.Destination, error) {

	destinations, err := destinationRepo.FindAll()
	if err != nil {
		return nil, err
	}
	return destinations, nil
}
```

---

เมื่อได้ส่วนประมวลผลแล้ว ต่อไปจะทำการสร้าง controller เพื่อให้ user สามารถเรียกใช้งาน api เราได้ โดยสร้างไฟล์ destinationController.go ใน folder controller แล้วเขียนเรียกใช้งาน service ดังตัวอย่าง

```go
package controller

import (
	"net/http"
	"tripbooking/service"

	"github.com/gin-gonic/gin"
)

// create services
var destinationServ = service.NewDestinationService()

// Initiate Controller
func NewDestinationController(router *gin.Engine) {
	r := router.Group("/destination")
	{
		r.GET("", findAll)
	}
}

// Implement Controller
func findAll(c *gin.Context) {
	destinations, err := destinationServ.GetAllDestinations()
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": err,
		})
	}
	c.JSON(http.StatusOK, destinations)
}
```

---

เมื่อได้ controller แล้ว ต่อไป จะเป็นการผูก Controller เข้าไปยัง Server ที่เราสร้าง 

มาที่ไฟล์ server.go และเพิ่ม (Register) Controller ของเราเข้าไป ตาม code ตัวอย่าง

```go
func StartServer() {
	router := gin.Default()
	router.SetTrustedProxies(nil)
	router.GET("/", hello)
	port := os.Getenv("PORT")
	if port == "" {
		port = "8888"
	}
	
	// **** Register controllers
	controller.NewDestinationController(router)

	router.Run(":" + port)
}
```
จากนั้นทดลองเรียก api โดยใช้ /destination จะได้ผลลัพธ์ตามตัวอย่าง

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50fmcl4i151rk9tpmiai.png)

---
เสร็จสิ้นการเริ่มต้น GO Project ในการสร้าง Restful api เชื่อมต่อไปยัง MySQL ด้วย GIN และ GORM