程序员社区

使用Gin,MySQL和Docker开发博客(Part 2)

使用Gin,MySQL和Docker开发博客(Part 2)插图

原文地址
欢迎来到使用Gin, MySQL和Docker开发博客项目的第二部分。确保你看已经阅读了第一部分请点击链接。

架构

我们将在这个博客项目中遵循整洁架构。整洁的架构是一种以分层的方式编写软件应用程序的艺术。请阅读这篇文章以获得更详细的信息,对所有的层(存储库,控制器等)都有解释。以下是遵循整洁架构的项目概述,这是您将要遵循的。

├── api 
│   ├── controller
│   │   └── post.go
│   ├── repository
│   │   └── post.go
│   ├── routes
│   │   └── post.go
│   └── service
│       └── post.go
├── docker-compose.yml
├── Dockerfile
├── go.mod
├── go.sum
├── infrastructure
│   ├── db.go
│   ├── env.go
│   └── routes.go
├── main
├── main.go
├── models
│   └── post.go
└── util
    └── response.go

开始:设计models

在项目目录中创建一个模models件夹。在models文件夹中创建blog.go文件,进入文件并添加以下代码:

package models
import "time"

//Post Post Model
type Post struct {
    ID        int64     `gorm:"primary_key;auto_increment" json:"id"`
    Title     string    `gorm:"size:200" json:"title"`
    Body      string    `gorm:"size:3000" json:"body" `
    CreatedAt time.Time `json:"created_at,omitempty"`
    UpdatedAt time.Time `json:"updated_at,omitempty"`
}

// TableName method sets table name for Post model
func (post *Post) TableName() string {
    return "post"
}

//ResponseMap -> response map method of Post
func (post *Post) ResponseMap() map[string]interface{} {
    resp := make(map[string]interface{})
    resp["id"] = post.ID
    resp["title"] = post.Title
    resp["body"] = post.Body
    resp["created_at"] = post.CreatedAt
    resp["updated_at"] = post.UpdatedAt
    return resp
}
我们正在定义Blog模型,它稍后将被转换为数据库表(gorm为我们做了这一点)。TableName方法将为blog结构体在数据库中创建名为blog的表。ResponseMap用于从successfull API调用返回响应。我假设你熟悉go中的Struct和方法。

添加repository层

这一层与数据库交互并执行CRUD操作。在项目目录上创建一个api文件夹。在api文件夹中创建repository文件夹。在repository文件夹中创建blog.go文件。结构层次应该是这样的:api ->repository-> blog.go。您可以随时参考架构部分的项目结构。

package repository
import (
    "blog/infrastructure"
    "blog/models"
)

//PostRepository -> PostRepository
type PostRepository struct {
    db infrastructure.Database
}

// NewPostRepository : fetching database
func NewPostRepository(db infrastructure.Database) PostRepository {
    return PostRepository{
        db: db,
    }
}

//Save -> Method for saving post to database
func (p PostRepository) Save(post models.Post) error {
    return p.db.DB.Create(&post).Error
}

//FindAll -> Method for fetching all posts from database
func (p PostRepository) FindAll(post models.Post, keyword string) (*[]models.Post, int64, error) {
    var posts []models.Post
    var totalRows int64 = 0

    queryBuider := p.db.DB.Order("created_at desc").Model(&models.Post{})

    // Search parameter
    if keyword != "" {
        queryKeyword := "%" + keyword + "%"
        queryBuider = queryBuider.Where(
            p.db.DB.Where("post.title LIKE ? ", queryKeyword))
    }

    err := queryBuider.
        Where(post).
        Find(&posts).
        Count(&totalRows).Error
    return &posts, totalRows, err
}

//Update -> Method for updating Post
func (p PostRepository) Update(post models.Post) error {
    return p.db.DB.Save(&post).Error
}

//Find -> Method for fetching post by id
func (p PostRepository) Find(post models.Post) (models.Post, error) {
    var posts models.Post
    err := p.db.DB.
        Debug().
        Model(&models.Post{}).
        Where(&post).
        Take(&posts).Error
    return posts, err
}

//Delete Deletes Post
func (p PostRepository) Delete(post models.Post) error {
    return p.db.DB.Delete(&post).Error
}

让我们来解释上面的代码:

  • PostRepository: PostRepository结构体有一个db字段,它是infracture.Database类型。实际上是一个gorm数据库类型。infracture的数据库部分已经在第1部分中介绍了。
  • NewPostRepository: NewPostRepository函数以databse为参数,并返回PostRepository。在main.go文件中初始化服务就提供了Database参数。
  • Save/FindAll/Find/Update/Delete:这些函数使用gorm实现对博客在数据库中的增删改查功能。

添加service层

该层管理内层和外层(存储库层和控制器层)之间的通信。更多细节查看这里。在api文件夹中创建service文件夹。在service文件夹中创建blog.go文件。整个结构应该看起来像这样:api -> service -> blog.go。关于架构,请参考架构部分。

package service

import (
    "blog/api/repository"
    "blog/models"
)

//PostService PostService struct
type PostService struct {
    repository repository.PostRepository
}

//NewPostService : returns the PostService struct instance
func NewPostService(r repository.PostRepository) PostService {
    return PostService{
        repository: r,
    }
}

//Save -> calls post repository save method
func (p PostService) Save(post models.Post) error {
    return p.repository.Save(post)
}

//FindAll -> calls post repo find all method
func (p PostService) FindAll(post models.Post, keyword string) (*[]models.Post, int64, error) {
    return p.repository.FindAll(post, keyword)
}

// Update -> calls postrepo update method
func (p PostService) Update(post models.Post) error {
    return p.repository.Update(post)
}

// Delete -> calls post repo delete method
func (p PostService) Delete(id int64) error {
    var post models.Post
    post.ID = id
    return p.repository.Delete(post)
}

// Find -> calls post repo find method
func (p PostService) Find(post models.Post) (models.Post, error) {
    return p.repository.Find(post)
}

让我们来解释上面的代码:

  • PostService: PostService结构体具有repository字段,该字段是postRepository类型,允许访问postRepository方法。
  • NewPostService: NewPostService接受PostRepository作为参数并返回包含所有PostRepository方法的PostService实例。
  • ** Save/FindAll/Find/Update/Delete **:调用相应的repository方法。

增加Controller层

此层用于获取用户输入并处理它们或将它们传递给其他层。更多关于控制器说明请查看。但是在为控制器层添加代码之前,让我们添加一些实用工具函数,用于在成功调用API时返回。

添加utils

在项目目录中创建util文件夹,并创建reponse.go文件。目录结构应该看起来这样: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})
}
  • Response: Response是返回json格式的消息并带有Struct数据,这里是Blog数据。
  • ErrorJSON:ErrorJSON用于返回JSON格式的错误响应。
  • SuccessJSON:SuccessJSON用于返回JSON格式的成功消息。
    在api文件夹创建一个controller文件夹,在文件夹里面创建blog.go文件。项目结构层次关系为:api ->controller-> blog.go。
package controller

import (
    "blog/api/service"
    "blog/models"
    "blog/util"
    "net/http"
    "strconv"

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

//PostController -> PostController
type PostController struct {
    service service.PostService
}

//NewPostController : NewPostController
func NewPostController(s service.PostService) PostController {
    return PostController{
        service: s,
    }
}

// GetPosts : GetPosts controller
func (p PostController) GetPosts(ctx *gin.Context) {
    var posts models.Post

    keyword := ctx.Query("keyword")

    data, total, err := p.service.FindAll(posts, 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: "Post result set",
        Data: map[string]interface{}{
            "rows":       respArr,
            "total_rows": total,
        }})
}

// AddPost : AddPost controller
func (p *PostController) AddPost(ctx *gin.Context) {
    var post models.Post
    ctx.ShouldBindJSON(&post)

    if post.Title == "" {
        util.ErrorJSON(ctx, http.StatusBadRequest, "Title is required")
        return
    }
    if post.Body == "" {
        util.ErrorJSON(ctx, http.StatusBadRequest, "Body is required")
        return
    }
    err := p.service.Save(post)
    if err != nil {
        util.ErrorJSON(ctx, http.StatusBadRequest, "Failed to create post")
        return
    }
    util.SuccessJSON(ctx, http.StatusCreated, "Successfully Created Post")
}

//GetPost : get post by id
func (p *PostController) GetPost(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 post models.Post
    post.ID = id
    foundPost, err := p.service.Find(post)
    if err != nil {
        util.ErrorJSON(c, http.StatusBadRequest, "Error Finding Post")
        return
    }
    response := foundPost.ResponseMap()

    c.JSON(http.StatusOK, &util.Response{
        Success: true,
        Message: "Result set of Post",
        Data:    &response})

}

//DeletePost : Deletes Post
func (p *PostController) DeletePost(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)
}

//UpdatePost : get update by id
func (p PostController) UpdatePost(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 post models.Post
    post.ID = id

    postRecord, err := p.service.Find(post)

    if err != nil {
        util.ErrorJSON(ctx, http.StatusBadRequest, "Post with given id not found")
        return
    }
    ctx.ShouldBindJSON(&postRecord)

    if postRecord.Title == "" {
        util.ErrorJSON(ctx, http.StatusBadRequest, "Title is required")
        return
    }
    if postRecord.Body == "" {
        util.ErrorJSON(ctx, http.StatusBadRequest, "Body is required")
        return
    }

    if err := p.service.Update(postRecord); err != nil {
        util.ErrorJSON(ctx, http.StatusBadRequest, "Failed to store Post")
        return
    }
    response := postRecord.ResponseMap()

    ctx.JSON(http.StatusOK, &util.Response{
        Success: true,
        Message: "Successfully Updated Post",
        Data:    response,
    })
}

让我们来解释上面的代码:

  • PostController: PostController结构体有service字段,该字段是PostService类型,允许访问PostService方法。
  • NewPostController:NewPostController以PostService为参数返回PostController,允许在controller上使用所有的PostController方法。
  • GetPosts/AddPost/GetPost/DeletePost/UpdatePost:用户输入被获取/验证/处理/服务层被调用(调用Repository方法;执行数据库操作),响应由实用工具函数返回。

添加路由(Routes)

到目前为止,我们已经创建了api的基础部分。让我们通过添加路由来创建EndPoint(Rest API入口)。

package routes

import (
    "blog/api/controller"
    "blog/infrastructure"
)

//PostRoute -> Route for question module
type PostRoute struct {
    Controller controller.PostController
    Handler    infrastructure.GinRouter
}

//NewPostRoute -> initializes new choice rouets
func NewPostRoute(
    controller controller.PostController,
    handler infrastructure.GinRouter,

) PostRoute {
    return PostRoute{
        Controller: controller,
        Handler:    handler,
    }
}

//Setup -> setups new choice Routes
func (p PostRoute) Setup() {
    post := p.Handler.Gin.Group("/posts") //Router group
    {
        post.GET("/", p.Controller.GetPosts)
        post.POST("/", p.Controller.AddPost)
        post.GET("/:id", p.Controller.GetPost)
        post.DELETE("/:id", p.Controller.DeletePost)
        post.PUT("/:id", p.Controller.UpdatePost)
    }
}

让我们来解释上面的代码:

  • PostRoute:PostRoute结构体包含Controller和Handler属性。Controller是PostController类型,Handler是GinRouter类型。这里的GinRouter用于创建路由组,之后创建endpoint
  • NewPostRoute:NewPostRoute以Controller和handler为参数返回PostRoute实例,可以访问PostController和GinRouter的方法。
  • Setup:该方法用于配制博客API的入口即EndPoint。

Main Router

让我们创建一个函数并返回Gin Router。在infrastructure目录下创建route.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,
    }
}

上面的代码配置并返回一个Default Gin Router实例。

整合所有模块

基础部分现在已经完成。剩下的就是把东西整合在一起。编辑main.go文件:

package main

import (
    "blog/api/controller"
    "blog/api/repository"
    "blog/api/routes"
    "blog/api/service"
    "blog/infrastructure"
    "blog/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
    postRepository := repository.NewPostRepository(db) // repository are being setup
    postService := service.NewPostService(postRepository) // service are being setup
    postController := controller.NewPostController(postService) // controller are being set up
    postRoute := routes.NewPostRoute(postController, router) // post routes are initialized
    postRoute.Setup() // post routes are being setup

    db.DB.AutoMigrate(&models.Post{}) // migrating Post model to datbase table
    router.Gin.Run(":8000") //server started on 8000 port
}

这是main.go的完整内容。

测试APIs

通过下面的命令使用Docker Compose启动服务器,在docker-composer.yml目录下执行:

docker-compose up --build

使用你喜欢的API测试工具,这里我使用Insomnia
1、测试创建博客的接口:

使用Gin,MySQL和Docker开发博客(Part 2)插图1

2、测试获取所有博客接口:

使用Gin,MySQL和Docker开发博客(Part 2)插图2

3、测试获取特定博客接口:

使用Gin,MySQL和Docker开发博客(Part 2)插图3

4、测试更新接口:

使用Gin,MySQL和Docker开发博客(Part 2)插图4

5、测试删除接口:

使用Gin,MySQL和Docker开发博客(Part 2)插图5

赞(0) 打赏
未经允许不得转载:IDEA激活码 » 使用Gin,MySQL和Docker开发博客(Part 2)

一个分享Java & Python知识的社区