vlambda博客
学习文章列表

使用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)


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

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


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

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


4、测试更新接口:



5、测试删除接口: