Golang HTTP Server 3 - Khởi tạo database và thêm layer model, services, controller
Trong bài trước, chúng ta đã khởi tạo Postgre database và kết nối tới server, trong bài này chúng ta sẽ tạo một table, khởi tạo seed data, thêm vài layers mới cho dự án.
Source code cho toàn bộ series: Github
Thiết kế model và tạo table
Tạo file model/todo.go
và follow code:
package models
import "time"
type Todo struct {
ID int64 `gorm:"primary_key;auto_increment" json:"id"`
Title string `gorm:"size:200" json:"title"`
Description string `gorm:"size:3000" json:"description" `
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
// TableName method sets table name for TODO model
func (todo *Todo) TableName() string {
return "todo"
}
// ResponseMap -> response map method of TODO
func (todo *Todo) ResponseMap() map[string]interface{} {
resp := make(map[string]interface{})
resp["id"] = todo.ID
resp["title"] = todo.Title
resp["body"] = todo.Description
resp["created_at"] = todo.CreatedAt
resp["updated_at"] = todo.UpdatedAt
return resp
}
Trong file này, chúng ta tạo một model, sau đó GORM sẽ giúp chúng ta tạo table trong postgres database.
Repository layer
Tạo file api/repository/todo.go
với nôi dung như sau
package repository
import (
"todolist/infrastructure"
"todolist/models"
)
// TodoRepository -> TodoRepository
type TodoRepository struct {
db infrastructure.Database
}
// NewTodoRepository : fetching database
func NewTodoRepository(db infrastructure.Database) TodoRepository {
return TodoRepository{
db: db,
}
}
// Save -> Method for saving todo to database
func (p TodoRepository) Save(todo models.Todo) error {
return p.db.DB.Create(&todo).Error
}
// FindAll -> Method for fetching all todos from database
func (p TodoRepository) FindAll(todo models.Todo, keyword string) (*[]models.Todo, int64, error) {
var todos []models.Todo
var totalRows int64 = 0
queryBuider := p.db.DB.Order("created_at desc").Model(&models.Todo{})
// Search parameter
if keyword != "" {
queryKeyword := "%" + keyword + "%"
queryBuider = queryBuider.Where(
p.db.DB.Where("todo.title LIKE ? ", queryKeyword))
}
err := queryBuider.
Where(todo).
Find(&todos).
Count(&totalRows).Error
return &todos, totalRows, err
}
// Update -> Method for updating Post
func (p TodoRepository) Update(todo models.Todo) error {
return p.db.DB.Save(&todo).Error
}
// Find -> Method for fetching post by id
func (p TodoRepository) Find(todo models.Todo) (models.Todo, error) {
var todos models.Todo
err := p.db.DB.
Debug().
Model(&models.Todo{}).
Where(&todo).
Take(&todos).Error
return todos, err
}
// Delete Deletes Post
func (p TodoRepository) Delete(todo models.Todo) error {
return p.db.DB.Delete(&todo).Error
}
Layer này dùng để connect tới DB, và thực hiện một số hàm CURD cơ bản bằng cách sử dụng thư viện GORM.
Services layer
Tạo file api/service/todo.go
với nội dung:
package service
import (
"todolist/api/repository"
"todolist/models"
)
// TodoService TodoService struct
type TodoService struct {
repository repository.TodoRepository
}
// NewTodoService : returns the TodoService struct instance
func NewTodoService(r repository.TodoRepository) TodoService {
return TodoService{
repository: r,
}
}
// Save -> calls todo repository save method
func (p TodoService) Save(todo models.Todo) error {
return p.repository.Save(todo)
}
// FindAll -> calls todo repo find all method
func (p TodoService) FindAll(todo models.Todo, keyword string) (*[]models.Todo, int64, error) {
return p.repository.FindAll(todo, keyword)
}
// Update -> calls todorepo update method
func (p TodoService) Update(todo models.Todo) error {
return p.repository.Update(todo)
}
// Delete -> calls todo repo delete method
func (p TodoService) Delete(id int64) error {
var todo models.Todo
todo.ID = id
return p.repository.Delete(todo)
}
// Find -> calls todo repo find method
func (p TodoService) Find(todo models.Todo) (models.Todo, error) {
return p.repository.Find(todo)
}
Layer dùng dể giao tiếp giữa controler và repository layer, thực hiện những business rule. Trong project này, business đơn giản chỉ là call repository function tương ứng.
Controller layer
File: api/controller/todo.go
package controller
import (
"net/http"
"strconv"
"todolist/api/service"
"todolist/models"
"todolist/util"
"github.com/gin-gonic/gin"
)
// TodoController -> TodoController
type TodoController struct {
service service.TodoService
}
// NewTodoController : NewTodoController
func NewTodoController(s service.TodoService) TodoController {
return TodoController{
service: s,
}
}
// GetTodos : GetTodos controller
func (p TodoController) GetTodos(ctx *gin.Context) {
var todos models.Todo
keyword := ctx.Query("keyword")
data, total, err := p.service.FindAll(todos, keyword)
if err != nil {
util.ErrorJSON(ctx, http.StatusBadRequest, "Failed to find questions")
return
}
respArr := make([]map[string]interface{}, 0, 0)
for _, n := range *data {
resp := n.ResponseMap()
respArr = append(respArr, resp)
}
ctx.JSON(http.StatusOK, &util.Response{
Success: true,
Message: "Todo result set",
Data: map[string]interface{}{
"rows": respArr,
"total_rows": total,
}})
}
// AddTodo : AddTodo controller
func (p *TodoController) AddTodo(ctx *gin.Context) {
var todo models.Todo
ctx.ShouldBindJSON(&todo)
if todo.Title == "" {
util.ErrorJSON(ctx, http.StatusBadRequest, "Title is required")
return
}
if todo.Description == "" {
util.ErrorJSON(ctx, http.StatusBadRequest, "Body is required")
return
}
err := p.service.Save(todo)
if err != nil {
util.ErrorJSON(ctx, http.StatusBadRequest, "Failed to create post")
return
}
util.SuccessJSON(ctx, http.StatusCreated, "Successfully Created Post")
}
// GetTodo : get Todo by id
func (p *TodoController) GetTodo(c *gin.Context) {
idParam := c.Param("id")
id, err := strconv.ParseInt(idParam, 10, 64) //type conversion string to int64
if err != nil {
util.ErrorJSON(c, http.StatusBadRequest, "id invalid")
return
}
var todo models.Todo
todo.ID = id
foundTodo, err := p.service.Find(todo)
if err != nil {
util.ErrorJSON(c, http.StatusBadRequest, "Error Finding Post")
return
}
response := foundTodo.ResponseMap()
c.JSON(http.StatusOK, &util.Response{
Success: true,
Message: "Result set of Post",
Data: &response})
}
func (p *TodoController) DeleteTodo(c *gin.Context) {
idParam := c.Param("id")
id, err := strconv.ParseInt(idParam, 10, 64) //type conversion string to uint64
if err != nil {
util.ErrorJSON(c, http.StatusBadRequest, "id invalid")
return
}
err = p.service.Delete(id)
if err != nil {
util.ErrorJSON(c, http.StatusBadRequest, "Failed to delete Post")
return
}
response := &util.Response{
Success: true,
Message: "Deleted Sucessfully"}
c.JSON(http.StatusOK, response)
}
// UpdateTodo : get update by id
func (p TodoController) UpdateTodo(ctx *gin.Context) {
idParam := ctx.Param("id")
id, err := strconv.ParseInt(idParam, 10, 64)
if err != nil {
util.ErrorJSON(ctx, http.StatusBadRequest, "id invalid")
return
}
var todo models.Todo
todo.ID = id
todoRecord, err := p.service.Find(todo)
if err != nil {
util.ErrorJSON(ctx, http.StatusBadRequest, "todo with given id not found")
return
}
ctx.ShouldBindJSON(&todoRecord)
if todoRecord.Title == "" {
util.ErrorJSON(ctx, http.StatusBadRequest, "Title is required")
return
}
if todoRecord.Description == "" {
util.ErrorJSON(ctx, http.StatusBadRequest, "Body is required")
return
}
if err := p.service.Update(todoRecord); err != nil {
util.ErrorJSON(ctx, http.StatusBadRequest, "Failed to store todo")
return
}
response := todoRecord.ResponseMap()
ctx.JSON(http.StatusOK, &util.Response{
Success: true,
Message: "Successfully Updated todo",
Data: response,
})
}
Layer này dùng đẻ lấy thông tin người dùng, thực hiện một số validation đơn giản, ngoài ra còn call tới 1 utils function để thông nhất response cho chương trình.
Utils function
File: util/response.go
package util
import "github.com/gin-gonic/gin"
// Response struct
type Response struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
// ErrorJSON : json error response function
func ErrorJSON(c *gin.Context, statusCode int, data interface{}) {
c.JSON(statusCode, gin.H{"error": data})
}
// SuccessJSON : json error response function
func SuccessJSON(c *gin.Context, statusCode int, data interface{}) {
c.JSON(statusCode, gin.H{"msg": data})
}
Add endpoint
File api/routes/todo.go
package routes
import (
"todolist/api/controller"
"todolist/infrastructure"
)
// PostRoute -> Route for question module
type TodoRoute struct {
Controller controller.TodoController
Handler infrastructure.GinRouter
}
// NewTodoRoute -> initializes new choice rouets
func NewTodoRoute(
controller controller.TodoController,
handler infrastructure.GinRouter,
) TodoRoute {
return TodoRoute{
Controller: controller,
Handler: handler,
}
}
// Setup -> setups new choice Routes
func (p TodoRoute) Setup() {
todo := p.Handler.Gin.Group("/todos") //Router group
{
todo.GET("/", p.Controller.GetTodos)
todo.POST("/", p.Controller.AddTodo)
todo.GET("/:id", p.Controller.GetTodo)
todo.DELETE("/:id", p.Controller.DeleteTodo)
todo.PUT("/:id", p.Controller.UpdateTodo)
}
}
Chúng ta define 5 endpoint tương ứng với 5 hàm trong todo
controller. Cuối cùng chúng ta add main Router
File: infrastructure/routes.go
package infrastructure
import (
"net/http"
"github.com/gin-gonic/gin"
)
// GinRouter -> Gin Router
type GinRouter struct {
Gin *gin.Engine
}
// NewGinRouter all the routes are defined here
func NewGinRouter() GinRouter {
httpRouter := gin.Default()
httpRouter.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": "Up and Running..."})
})
return GinRouter{
Gin: httpRouter,
}
}
Và cập nhật hàm main
package main
import (
"todolist/api/controller"
"todolist/api/repository"
"todolist/api/routes"
"todolist/api/service"
"todolist/infrastructure"
"todolist/models"
)
func init() {
infrastructure.LoadEnv()
}
func main() {
router := infrastructure.NewGinRouter() //router has been initialized and configured
db := infrastructure.NewDatabase() // databse has been initialized and configured
todoRepository := repository.NewTodoRepository(db) // repository are being setup
todoService := service.NewTodoService(todoRepository) // service are being setup
todoController := controller.NewTodoController(todoService) // controller are being set up
todoRoute := routes.NewTodoRoute(todoController, router) // todo routes are initialized
todoRoute.Setup() // todo routes are being setup
db.DB.AutoMigrate(&models.Todo{}) // migrating todo model to datbase table
router.Gin.Run(":8000") //server started on 8000 port
}
Restart server và chạy lại. Chúng ta sẽ test những API này bằng 1 tools tên là Insomnia
, các bạn có thể xài các tool tương tự như Postman hoặc CURL đều được.
Testing
POST /todos
Send POST request tới /todos
với payload như sau
{
"title": "test todo",
"description": "Test descrkiption"
}
Great!! một Todo đã được tạo thành công
GET /todos
Xem chi tiết một TODO GET /todos/:id
Xoá todo
Gửi một DELETE request
curl --request GET \
--url http://localhost:8000/todos/2 \
--header 'Content-Type: application/json' \
--header 'User-Agent: insomnia/8.6.1'
Nhớ kiểm tra lại bằng cách send GET Request tới:
curl --request GET \
--url http://localhost:8000/todos/2 \
--header 'Content-Type: application/json' \
--header 'User-Agent: insomnia/8.6.1'
Để chắc chắn TODO có ID = 2 đã được xoá
Chỉnh sửa todo
Chúng ta đã test xong cả 5 APIs vừa mới tạo, và trong bài tới, chúng ta sẽ add thêm user model và services, chỉ cho phép xem todo của chính user hiện tại mà thôi. Hẹn gặp lại mọi người ở bài sau.