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 ของ graphQL5 directories, 14 files
ก่อนอื่นเราจะสร้าง mkdir db ไว้ก่อนเพื่อทำการ map volumes ให้กับ docker ของเรา
version: "3.8"services:db:image: postgres:14.1-alpinerestart: alwaysenvironment:- POSTGRES_USER=postgres- POSTGRES_PASSWORD=postgresports:- 5432:5432volumes:- db:/var/lib/postgresql/datavolumes:db:driver: local
docker จะสร้าง postgres มาให้เราที่ port 5432
จากนั้นเราจะมาออกแบบ data customer ของเราก่อน เพื่อให้ postgres นำไป automigrate สร้าง table ให้เรา หลังจาก run main.go โดยเราจะสร้างที่ file repository/customer.go และผมจะใช้ gorm เป็น orm ในการจัดการ database นะครับ
package repositoryimport ("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 {BarrothModelCustomerID 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 ของเรา โดย
ต่อไปเราจะทำการสร้่าง adapter เพื่อนำ database connection เข้ามาใช้งานใน repository นี้ repository/customer_db.go
package repositoryimport ("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 []Customererr := c.db.Find(&customers).Errorif err != nil {return nil, err}return customers, nil}func (c customerRepository) GetByID(id int) (*Customer, error) {var customer Customererr := c.db.Where("id = ?", id).First(&customer).Errorif err != nil {return nil, err}return &customer, nil}func (c customerRepository) CreateCustomer(customer *Customer) (Customer, error) {err := c.db.Create(customer).Errorif 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 = locnewLogger := 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.go2022/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 servicetype (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 serviceimport "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 []CustomerResfor _, 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 handlerimport ("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 อะไรบ้าง
type Customer {CustomerID Int!Name String!DateOfBirth String!City String!ZipCode String!Status Int!}type Query {GetCustomer(id: Int): CustomerGetCustomers(): [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 intok 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 intcusName stringdateOfBirth stringcity stringzipCode stringstatus intok 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
]
จากนั้นเราจะลอง เพิ่ม customer ใหม่เข้าไปด้วย
mutation {CreateCustomer(CustomerID: 3, Name: "Kano", DateOfBirth: "1981-02-14", City: "Bangkok", ZipCode: "10250", Status: 1)}
ต่อมาเราจะลอง query กัน
{GetCustomer(id: 2) {citycustomer_iddate_of_birthnamestatuszip_code}}
{Friends: GetCustomers {date_of_birthname},me: GetCustomer(id: 2){name}}
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
Quick Links
Legal Stuff