HomeArtTechHackBlockchain
ONLINE ·
Index
/
Technology
/
Article

บันทึก การทำ Hexagonal - gofiber - graphQL - Postgres จ่ะ

Operator
Khomkrid Lerdprasert
Filed
March 17, 2022
Channel
Technology
Read
~1 min
บันทึก การทำ Hexagonal - gofiber - graphQL - Postgres จ่ะ

บันทึก การทำ Hexagonal - gofiber - graphQL - Postgres จ่ะ

Project Layout ของเรา

.
├── docker-compose.yml // ใช้ run Postgres Database ทดสอบ
├── go.mod
├── go.sum
├── handler // ส่วนจัดการ view ไปยังผู้ใช้งาน
│   ├── customer.go
│   └── graphql_handler.go
├── main.go // file main
├── readme.md
├── repository // ส่วนล่างสุดในการติดต่อ Database
│   ├── customer.go
│   └── customer_db.go
├── resolver // ส่วนจัดการ view ของ graphQL
│   └── resolver.go
├── schema // โครงสร้าง ของ graphQL ว่าเรามี object model แบบไหนยังไง
│   └── schema.go
├── service // ส่วนของ business logic
│   ├── customer.go
│   └── customer_service.go
└── type_schema.graphql // ตัว schema ของ graphQL
5 directories, 14 files

ก่อนอื่นเราจะสร้าง mkdir db ไว้ก่อนเพื่อทำการ map volumes ให้กับ docker ของเรา

version: "3.8"
services:
db:
image: postgres:14.1-alpine
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
ports:
- 5432:5432
volumes:
- db:/var/lib/postgresql/data
volumes:
db:
driver: local

และสั่ง docker-compose up -d

docker จะสร้าง postgres มาให้เราที่ port 5432

จากนั้นเราจะมาออกแบบ data customer ของเราก่อน เพื่อให้ postgres นำไป automigrate สร้าง table ให้เรา หลังจาก run main.go โดยเราจะสร้างที่ file repository/customer.go และผมจะใช้ gorm เป็น orm ในการจัดการ database นะครับ

package repository
import (
"time"
"gorm.io/gorm"
)
type (
BarrothModel struct {
ID uint `gorm:"primaryKey" json:"id"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at" swaggerignore:"true"`
}
Customer struct {
BarrothModel
CustomerID int `gorm:"type:INT4" json:"customer_id"`
Name string `gorm:"type:VARCHAR(100)" json:"name"`
DateOfBirth string `gorm:"type:DATE" json:"date_of_birth"`
City string `gorm:"type:VARCHAR(100)" json:"city"`
ZipCode string `gorm:"type:VARCHAR(10)" json:"zip_code"`
Status int `gorm:"type:INT2" json:"status"`
}
CustomerRepository interface {
GetAll() ([]Customer, error)
GetByID(id int) (*Customer, error)
CreateCustomer(customer *Customer) (Customer, error)
}
)

จะเห็นว่า เราจะมีการสร้าง interface ขึ้นมา 3 function ที่ใช้ในการทำงานกับ database ของเรา โดย

  1. GetAll() ([]Customer, error) เราจะใช้ในการดึงข้อมูล customer ทั้งหมดของเราออกมา
  2. GetByID(id int) (*Customer, error) เราจะใช้ดึงข้อมูล customer by id
  3. CreateCustomer(customer *Customer) (Customer, error) ใช้สร้าง customer ใหม่

ต่อไปเราจะทำการสร้่าง adapter เพื่อนำ database connection เข้ามาใช้งานใน repository นี้ repository/customer_db.go

package repository
import (
"gorm.io/gorm"
)
type (
customerRepository struct {
db *gorm.DB
}
)
func NewCustomerRepository(db *gorm.DB) CustomerRepository {
return customerRepository{db: db}
}

และทำการ implement 3 func ของ interface ของเราขึ้นมาใหม่ ภายใต้ struct ของ customerRepository repository/customer_db.go

func (c customerRepository) GetAll() ([]Customer, error) {
var customers []Customer
err := c.db.Find(&customers).Error
if err != nil {
return nil, err
}
return customers, nil
}
func (c customerRepository) GetByID(id int) (*Customer, error) {
var customer Customer
err := c.db.Where("id = ?", id).First(&customer).Error
if err != nil {
return nil, err
}
return &customer, nil
}
func (c customerRepository) CreateCustomer(customer *Customer) (Customer, error) {
err := c.db.Create(customer).Error
if err != nil {
return Customer{}, err
}
return *customer, nil
}

จากนั้นเราจะไปที่ file main.go เพื่อ สร้าง database connection

func main() {
dns := "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Bangkok"
dial := postgres.Open(dns)
db, err := createDatabaseConnection(dial, dns)
if err != nil {
log.Println(err)
}
customerRepo := repository.NewCustomerRepository(db)
cus, err := customerRepo.GetByID(1)
if err != nil {
log.Println(err)
}
log.Println(cus)
}
func createDatabaseConnection(dial gorm.Dialector, dns string) (db *gorm.DB, err error) {
loc, err := time.LoadLocation("Asia/Bangkok")
if err != nil {
return nil, err
}
time.Local = loc
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags),
logger.Config{
SlowThreshold: time.Second,
LogLevel: logger.Error,
Colorful: true,
},
)
mydb, err := gorm.Open(dial, &gorm.Config{
Logger: newLogger,
})
if err != nil {
return nil, err
}
sqlDB, err := mydb.DB()
if err != nil {
return nil, err
}
sqlDB.SetMaxIdleConns(50)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetMaxOpenConns(100)
mydb.AutoMigrate(&repository.Customer{})
return mydb, nil
}

เมื่อเราสั่ง go run main.go

$ go run main.go
2022/03/17 13:53:16 &{{1 2022-03-16 21:33:27.400137 +0700 +07 2022-03-16 21:33:27.400137 +0700 +07 {0001-01-01 00:00:00 +0000 UTC false}} 2 Arnon 2022-03-16T00:00:00Z bangkok 10250 1}

เราจะได้ data ของ customer ที่ id = 1 แสดงออกมาจาก repository

ต่อมาผมจะไปสร้าง service เพื่อทำ business logic และจะให้ service เป็นคนเรียกใช้ repository แทน

service/customer.go

package service
type (
CustomerRes struct {
CustomerID int `json:"customer_id"`
Name string `json:"name"`
DateOfBirth string `json:"date_of_birth"`
City string `json:"city"`
ZipCode string `json:"zip_code"`
Status int `json:"status"`
}
CustomerService interface {
GetCustomer(id int) (CustomerRes, error)
GetCustomers() ([]CustomerRes, error)
CreateCustomer(customer *CustomerRes) (CustomerRes, error)
}
)

ซึ่งส่วนใหญ่ผมจะตั้งชื่อ interface ใน service ที่จะเรียกใช้ repository ให้เหมือนกันกับ interface ใน repository จะได้ไม่ งง ว่าอะไรไปเรียกอะไร

ต่อไป เราจะไป implement service/customer_service.go ให้ customerService รับ repository มา แล้ว return service ออกมา

package service
import "hexagonal/architecture/repository"
type (
customerService struct {
customerRepository repository.CustomerRepository
}
)
func NewCustomerService(cus repository.CustomerRepository) CustomerService {
return customerService{customerRepository: cus}
}
func (c customerService) GetCustomer(id int) (CustomerRes, error) {
customer, err := c.customerRepository.GetByID(id)
if err != nil {
return CustomerRes{}, err
}
return CustomerRes{
CustomerID: customer.CustomerID,
Name: customer.Name,
DateOfBirth: customer.DateOfBirth,
City: customer.City,
ZipCode: customer.ZipCode,
Status: customer.Status,
}, nil
}
func (c customerService) GetCustomers() ([]CustomerRes, error) {
customers, err := c.customerRepository.GetAll()
if err != nil {
return nil, err
}
var customerRes []CustomerRes
for _, v := range customers {
customerRes = append(customerRes, CustomerRes{
CustomerID: v.CustomerID,
Name: v.Name,
DateOfBirth: v.DateOfBirth,
City: v.City,
ZipCode: v.ZipCode,
Status: v.Status,
})
}
return customerRes, nil
}
func (c customerService) CreateCustomer(customer *CustomerRes) (CustomerRes, error) {
newCus := repository.Customer{
CustomerID: customer.CustomerID,
Name: customer.Name,
DateOfBirth: customer.DateOfBirth,
City: customer.City,
ZipCode: customer.ZipCode,
Status: customer.Status,
}
res, err := c.customerRepository.CreateCustomer(&newCus)
if err != nil {
return CustomerRes{}, err
}
return CustomerRes{
CustomerID: res.CustomerID,
Name: res.Name,
DateOfBirth: res.DateOfBirth,
City: res.City,
ZipCode: res.ZipCode,
Status: res.Status,
}, nil
}

และไปแก้ไข main.go กัน

customerRepo := repository.NewCustomerRepository(db)
customerService := service.NewCustomerService(customerRepo)
cus, err := customerService.GetCustomer(1)
if err != nil {
log.Println(err)
}
log.Println(cus)

ต่อมาเราจะไปทำ handler/customer.go เพื่อเตรียมจัดการ presentation view layer ของเรา ด้วย gofiber อีกที

package handler
import (
"hexagonal/architecture/service"
"github.com/gofiber/fiber/v2"
)
type (
customerHandler struct {
customerService service.CustomerService
}
)
func NewCustomerHandler(cus service.CustomerService) customerHandler {
return customerHandler{customerService: cus}
}
func (c customerHandler) GetCustomer(f *fiber.Ctx) error {
id, err := f.ParamsInt("id", 1)
if err != nil {
return err
}
customer, err := c.customerService.GetCustomer(id)
if err != nil {
return err
}
return f.Status(fiber.StatusOK).JSON(fiber.Map{
"data": customer,
"error": nil,
})
}
func (c customerHandler) GetCustomers(f *fiber.Ctx) error {
customers, err := c.customerService.GetCustomers()
if err != nil {
return err
}
return f.Status(fiber.StatusOK).JSON(fiber.Map{
"data": customers,
"error": nil,
})
}

มาถึงขั้นตอนนี้ main.go ของเรา จะมีหน้าตาแบบนี้

func main() {
dns := "host=localhost user=postgres password=postgres dbname=postgres port=5432 sslmode=disable TimeZone=Asia/Bangkok"
dial := postgres.Open(dns)
db, err := createDatabaseConnection(dial, dns)
if err != nil {
log.Println(err)
}
customerRepo := repository.NewCustomerRepository(db)
customerService := service.NewCustomerService(customerRepo)
customerHandler := handler.NewCustomerHandler(customerService)
app := fiber.New(fiber.Config{
BodyLimit: 100 * 1024 * 1024,
})
app.Use(requestid.New())
app.Use(fiberLogger.New(fiberLogger.Config{
Format: "[${time}] ${method} ${path}",
TimeFormat: "02-Jan-2006",
TimeZone: "Asia/Bangkok",
}))
app.Use(cors.New(cors.Config{
AllowOrigins: "*",
AllowHeaders: "Origin, Content-Type, Accept,Authorization",
}))
app.Get("/customer/:id", customerHandler.GetCustomer)
app.Get("/customers", customerHandler.GetCustomers)
app.Get("/graph", func(c *fiber.Ctx) error {
fasthttpadaptor.NewFastHTTPHandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
gh.ServeHTTP(writer, request)
})(c.Context())
return nil
})
app.Post("/graph", func(c *fiber.Ctx) error {
fasthttpadaptor.NewFastHTTPHandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
gh.ServeHTTP(writer, request)
})(c.Context())
return nil
})
err = app.Listen(":3000")
if err != nil {
panic(err)
}
}

ต่อมาเราจะมาเล่น graphQL กัน ทำยังไงให้มันอยู่ในรูปแบบ hexagonal ซึ่งผมก็จะมั่วๆไปตามความเข้าใจส่วนตัวของผมต่อ

เริ่มจากเราจะมาสร้าง type_schema.graphql กัน เพื่อบอกว่า graphql ของเราจะมี interface อะไรบ้าง และมี struct model อะไรบ้าง

อ่าน graphql schema ได้ที่นี่

type Customer {
CustomerID Int!
Name String!
DateOfBirth String!
City String!
ZipCode String!
Status Int!
}
type Query {
GetCustomer(id: Int): Customer
GetCustomers(): [Customer]
}
type Mutation {
CreateCustomer(CustomerID: Int!,Name: String!,DateOfBirth: String!,City: String!,ZipCode: String!,Status: Int!): Customer!
}

โดยจะแบ่งเป็น query ในการดึงข้อมูล และ Mutation ในการ จัดการข้อมูล

จากนั้นจะมาทำการ define schema กันที่ schema/schema.go

โดยเราจะบอกว่า Customer ของเราที่ define ใน type_schema.graphql นั้น มี type อย่างไรใน Golang

var (
Customer = graphql.NewObject(
graphql.ObjectConfig{
Name: "Customer",
Fields: graphql.Fields{
"customer_id": &graphql.Field{
Type: graphql.Int,
},
"name": &graphql.Field{
Type: graphql.String,
},
"date_of_birth": &graphql.Field{
Type: graphql.String,
},
"city": &graphql.Field{
Type: graphql.String,
},
"zip_code": &graphql.Field{
Type: graphql.String,
},
"status": &graphql.Field{
Type: graphql.Int,
},
},
},
)
)

ต่อมา ผมจะ ทำการสร้าง struct customerSchema เพื่อ return resolver ออกมา ซึ่งเราจะไปเขียน business logic ใน resolver นี่อีกที ผมมองว่า ใน resolver นี้จะทำงานเหมือนกับ service ส่วน schema ผมจะทำงานเหมือน handler ของ rest ที่เราทำไปเมื่อกี้

type (
customerSchema struct {
customerResolver resolver.CustomerResolver
}
)
func NewCustomerSchema(customerResolver resolver.CustomerResolver) customerSchema {
return customerSchema{
customerResolver: customerResolver,
}
}

ต่อมาเราจะไปทำ resolver/resolver.go เพื่อจัดการ ส่วนของ business logic และส่งต่อไปให้กับ repository ที่ติดต่อกับ database ด้วยการสร้าง interface ขึ้นมา 3 ตัว ที่จะนำไปใช้กับ Query และ Mutation ของ graphql

type (
customerResolver struct {
customerService service.CustomerService
}
CustomerResolver interface {
GetCustomer(params graphql.ResolveParams) (interface{}, error)
GetCustomers(params graphql.ResolveParams) (interface{}, error)
CreateCustomer(params graphql.ResolveParams) (interface{}, error)
}
)
func NewCustomerResolver(cus service.CustomerService) CustomerResolver {
return customerResolver{
customerService: cus,
}
}

ต่อมาเราจะกลับมาที่ schema/schema.go เพื่อ implement Query และ Mutation ให้ pass params ไปให้ resolver ของเรา

func (c customerSchema) Query() *graphql.Object {
objectConfig := graphql.ObjectConfig{
Name: "Query",
Fields: graphql.Fields{
"GetCustomers": &graphql.Field{
Type: graphql.NewList(Customer),
Description: "Get all Customer",
Resolve: c.customerResolver.GetCustomers,
},
"GetCustomer": &graphql.Field{
Type: Customer,
Description: "Get Customer By ID",
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.Int,
},
},
Resolve: c.customerResolver.GetCustomer,
},
},
}
return graphql.NewObject(objectConfig)
}
func (c customerSchema) Mutation() *graphql.Object {
objectConfig := graphql.ObjectConfig{
Name: "Mutation",
Fields: graphql.Fields{
"CreateCustomer": &graphql.Field{
Type: graphql.String,
Description: "Store a new customer",
Args: graphql.FieldConfigArgument{
"CustomerID": &graphql.ArgumentConfig{
Type: graphql.Int,
},
"Name": &graphql.ArgumentConfig{
Type: graphql.String,
},
"DateOfBirth": &graphql.ArgumentConfig{
Type: graphql.String,
},
"City": &graphql.ArgumentConfig{
Type: graphql.String,
},
"ZipCode": &graphql.ArgumentConfig{
Type: graphql.String,
},
"Status": &graphql.ArgumentConfig{
Type: graphql.Int,
},
},
Resolve: c.customerResolver.CreateCustomer,
},
},
}
return graphql.NewObject(objectConfig)
}

กลับมาที่ resolver/resolver.go เราจะมาเขียนข้างใน func interface กัน ให้ไปเรียกข้อมูลมาจาก repository ของเราก่อนหน้านี้

func (c customerResolver) GetCustomer(params graphql.ResolveParams) (interface{}, error) {
var (
id int
ok bool
)
if id, ok = params.Args["id"].(int); !ok || id == 0 {
return nil, fmt.Errorf("id is not integer or zero")
}
customer, err := c.customerService.GetCustomer(id)
if err != nil {
return nil, err
}
return customer, nil
}
func (c customerResolver) GetCustomers(params graphql.ResolveParams) (interface{}, error) {
customers, err := c.customerService.GetCustomers()
if err != nil {
return nil, err
}
return customers, nil
}
func (c customerResolver) CreateCustomer(params graphql.ResolveParams) (interface{}, error) {
var (
cusID int
cusName string
dateOfBirth string
city string
zipCode string
status int
ok bool
)
if cusID, ok = params.Args["CustomerID"].(int); !ok || cusID == 0 {
return nil, fmt.Errorf("id is not int or 0")
}
if cusName, ok = params.Args["Name"].(string); !ok || cusName == "" {
return nil, fmt.Errorf("id is not string or nil")
}
if dateOfBirth, ok = params.Args["DateOfBirth"].(string); !ok || dateOfBirth == "" {
return nil, fmt.Errorf("id is not string or nil")
}
if city, ok = params.Args["City"].(string); !ok || city == "" {
return nil, fmt.Errorf("id is not string or nil")
}
if zipCode, ok = params.Args["ZipCode"].(string); !ok || zipCode == "" {
return nil, fmt.Errorf("id is not string or nil")
}
if status, ok = params.Args["Status"].(int); !ok || status == 0 {
return nil, fmt.Errorf("id is not int or 0")
}
log.Println(`input`, cusID, cusName, dateOfBirth, city, zipCode, status)
newCus := service.CustomerRes{
CustomerID: cusID,
Name: cusName,
DateOfBirth: dateOfBirth,
City: city,
ZipCode: zipCode,
Status: status,
}
res, err := c.customerService.CreateCustomer(&newCus)
if err != nil {
return nil, err
}
return res, nil
}

สุดท้ายเราจะกลับมาที่ main.go และทำการเพิ่ม การเรียกใช้งาน graphql ด้วยการรับ service เข้ามา

cResolver := resolver.NewCustomerResolver(customerService)
cSchema := schema.NewCustomerSchema(cResolver)
graphqlSchema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: cSchema.Query(),
Mutation: cSchema.Mutation(),
})
if err != nil {
log.Println(err)
}
gh := gqlHandler.New(&gqlHandler.Config{
Schema: &graphqlSchema,
GraphiQL: true,
Pretty: true,
})
.
.
.
.
.
.
app.Get("/graph", func(c *fiber.Ctx) error {
fasthttpadaptor.NewFastHTTPHandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
gh.ServeHTTP(writer, request)
})(c.Context())
return nil
})
app.Post("/graph", func(c *fiber.Ctx) error {
fasthttpadaptor.NewFastHTTPHandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
gh.ServeHTTP(writer, request)
})(c.Context())
return nil
})

เมื่อเราสั่ง run go run main.go และเข้าไปที่ http://localhost:3000/graph จะพบกับ graphql playground

graphql playground
graphql playground
]

จากนั้นเราจะลอง เพิ่ม customer ใหม่เข้าไปด้วย

mutation {
CreateCustomer(CustomerID: 3, Name: "Kano", DateOfBirth: "1981-02-14", City: "Bangkok", ZipCode: "10250", Status: 1)
}

mutation
mutation

ต่อมาเราจะลอง query กัน

{
GetCustomer(id: 2) {
city
customer_id
date_of_birth
name
status
zip_code
}
}

query 1
query 1

{
Friends: GetCustomers {
date_of_birth
name
},
me: GetCustomer(id: 2){
name
}
}

query 2
query 2

result

{
"data": {
"Friends": [
{
"date_of_birth": "2022-03-16T00:00:00Z",
"name": "Arnon"
},
{
"date_of_birth": "2022-03-16T00:00:00Z",
"name": "Khomkrid L"
},
{
"date_of_birth": "1981-02-14T00:00:00Z",
"name": "Kano"
},
{
"date_of_birth": "1981-02-14T00:00:00Z",
"name": "Kano"
},
{
"date_of_birth": "1981-02-14T00:00:00Z",
"name": "Kano"
}
],
"me": {
"name": "Khomkrid L"
}
}
}

โดยตัวอย่าง code ทั้งหมดจะอยู่ที่ https://github.com/aofiee/hexagonal-fiber-postgres

◎ Tags

##Golang##gofiber##Hexagonal##Postgres
Khomkrid Lerdprasert
Operator

Khomkrid Lerdprasert

Technical Lead — building AI-powered platforms, omni-channel chat systems, and telemedicine solutions with Go, Next.js & clean architecture. 20+ years shipping software from crypto wallets to e-learning systems. Bangkok-based. Writes code late at night, brews beer on weekends.

GithubInstagram
Previous · transmission
Docker compose ทำ mongodb มาเชื่อมต่อ กับ golang
Next · transmission
Set up iPad Pro M1 for Web Development
Metadata
Channel
Technology
Filed
March 17, 2022
Read
~1 min
Language
TH / EN
Transmit

Related

Saxking Track — แอป iOS สำหรับฝึกซ้อมแซกโซโฟนกับ Backing Track
Khomkrid Lerdprasert
April 23, 2026
1 min
aofiee.dev
signal / noise / code · craft
© 2019 – 2026, Khomkrid Lerdprasert.
All transmissions logged.
No newsletter. No profiling. Cookies require consent.
PGP · 7F3D 2024 A21E B584 · 0x7F3D
Channels
  • Art & Culture
  • Technology
  • Hack 101
  • Blockchain 101
  • Archive / All posts
— END OF TRANSMISSION —
// powered by curiosity, coffee, & wuxia
BKK · 13°45′N · 100°30′E