HomeArtTechHackBlockchain

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

By Khomkrid Lerdprasert
Published in Technology
March 17, 2022
1 min read
บันทึก การทำ 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

Share

Previous Article
Docker compose ทำ mongodb มาเชื่อมต่อ กับ golang
Khomkrid Lerdprasert

Khomkrid Lerdprasert

Full Stack Life

Related Posts

สร้าง Key pair เพื่อทำการ signing document signature ด้วย Go lang
March 13, 2024
1 min
© 2024, All Rights Reserved.
Powered By

Quick Links

Author

Social Media