使用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、测试创建博客的接口:
2、测试获取所有博客接口:
3、测试获取特定博客接口:
4、测试更新接口:
5、测试删除接口: