[gin]简单的gin-mongo

前言

基于Gin框架编写的Web API,实现简单的CRUD功能,数据存放在MongoDB,并设置Redis缓存。

代码需要简单的分模块组织。

go mod init buildginapp

代码参考自《Building Distributed Application in Gin》

定义数据模型

  • 代码文件:buildginapp/models/recipe.go
package models

import (
	"time"
	"go.mongodb.org/mongo-driver/bson/primitive"
)

type Recipe struct {
	ID           primitive.ObjectID `json:"id"           bson:"_id"`
	Name         string             `json:"name"         bson:"name"`
	Tags         []string           `json:"tags"         bson:"tags"`
	Ingredients  []string           `json:"ingredients"  bson:"ingredients"`
	Instructions []string           `json:"instructions" bson:"instructions"`
	PublishedAt  time.Time          `json:"publishedAt"  bson:"publishedAt"`
}

设计API

http method resource description
GET /recipes 返回一列recipe数据
POST /recipes 创建新食谱
PUT /recipes/{id} 更新一个已存在的食谱
DELETE /recipes/{id} 删除一个已存在的食谱
GET /recipes/search?tag=X 根据标签查询食谱

编写API方法

  • 代码文件:buildapp/handlers/handler.go
package handlers

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis"
	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/primitive"
	"go.mongodb.org/mongo-driver/mongo"

	"buildginapp/models"
)

type RecipesHandler struct {
	collection  *mongo.Collection
	ctx         context.Context
	redisClient *redis.Client
}

func NewRecipesHandler(ctx context.Context, collection *mongo.Collection, redisClient *redis.Client) *RecipesHandler {
	return &RecipesHandler{
		collection:  collection,
		ctx:         ctx,
		redisClient: redisClient,
	}
}

// GET /recipes
func (handler *RecipesHandler) ListRecipesHandler(c *gin.Context) {
	// 先查redis, 无则查MongoDB
	val, err := handler.redisClient.Get("recipes").Result()
	if err == redis.Nil {
		log.Println("Request to MongoDB")

		cur, err := handler.collection.Find(handler.ctx, bson.M{})
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{
				"error": err.Error(),
			})
			return
		}
		defer cur.Close(handler.ctx)

		recipes := make([]models.Recipe, 0)
		for cur.Next(handler.ctx) {
			var recipe models.Recipe
			cur.Decode(&recipe)
			recipes = append(recipes, recipe)
		}
		// 将查询结果存到redis中, 过期时间为1小时
		// 数据量很多的时候,这会是一个大Key,可能有一定的性能隐患
		data, _ := json.Marshal(recipes)
		handler.redisClient.Set("recipes", string(data), 3600*time.Second)
		c.JSON(http.StatusOK, recipes)
	} else if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
		return
	} else {
		// 如果从redis中查询到了,那么直接返回redis的查询结果
		log.Println("Request to redis")
		recipes := make([]models.Recipe, 0)
		json.Unmarshal([]byte(val), &recipes)
		c.JSON(http.StatusOK, recipes)
	}
}

// POST /recipes
func (handler *RecipesHandler) NewRecipeHandler(c *gin.Context) {
	var recipe models.Recipe
	if err := c.ShouldBindJSON(&recipe); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": err.Error(),
		})
		return
	}

	recipe.ID = primitive.NewObjectID()
	recipe.PublishedAt = time.Now()
	_, err := handler.collection.InsertOne(handler.ctx, recipe)
	if err != nil {
		fmt.Println(err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": "Error while inserting a new recipe",
		})
		return
	}

	log.Println("RRemove data from redis")
	handler.redisClient.Del("recipes")

	c.JSON(http.StatusOK, recipe)
}

// PUT /recipes/:id
func (handler *RecipesHandler) UpdateRecipeHandler(c *gin.Context) {
	id := c.Param("id")
	var recipe models.Recipe
	if err := c.ShouldBindJSON(&recipe); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": err.Error(),
		})
		return
	}
	objectId, _ := primitive.ObjectIDFromHex(id)
	_, err := handler.collection.UpdateOne(handler.ctx, bson.M{
		"_id": objectId,
	}, bson.D{{"$set", bson.D{
		{"name", recipe.Name},
		{"instructions", recipe.Instructions},
		{"ingredients", recipe.Ingredients},
		{"tags", recipe.Tags},
	}}})
	if err != nil {
		fmt.Println(err)
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"message": "update success",
	})
}

// DELETE /recipes/:id
func (handler *RecipesHandler) DeleteRecipeHandler(c *gin.Context) {
	id := c.Param("id")
	objectId, _ := primitive.ObjectIDFromHex(id)
	_, err := handler.collection.DeleteOne(handler.ctx, bson.M{
		"_id": objectId,
	})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"message": "delete success",
	})
}

// GET /recipes/:id
func (handler *RecipesHandler) GetOneRecipeHandler(c *gin.Context) {
	id := c.Param("id")
	objectId, _ := primitive.ObjectIDFromHex(id)
	cur := handler.collection.FindOne(handler.ctx, bson.M{
		"_id": objectId,
	})
	var recipe models.Recipe
	err := cur.Decode(&recipe)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": err.Error(),
		})
		return
	}
	c.JSON(http.StatusOK, recipe)
}

main.go

  • 代码文件:buildginapp/main.go
package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/go-redis/redis"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/mongo/readpref"

	"buildginapp/handlers"
)

var err error
var client *mongo.Client

var recipesHandler *handlers.RecipesHandler

func init() {
	ctx := context.Background()
	// demo这个数据库需要先创建
	var url string = "mongodb://root:123456@192.168.0.20:27017/demo?authSource=admin&maxPoolSize=20"
	client, err = mongo.Connect(ctx, options.Client().ApplyURI(url))
	if err = client.Ping(context.TODO(), readpref.Primary()); err != nil {
		log.Fatal("connect ot mongodb failed: ", err)
	}
	log.Println("Connected to MongoDB")

	collection := client.Database("demo").Collection("recipes")
	redisClient := redis.NewClient(&redis.Options{
		Addr:     "192.168.0.20:6379",
		Password: "",
		DB:       0,
	})
	status := redisClient.Ping()
	log.Println("redis ping: ", status)
	recipesHandler = handlers.NewRecipesHandler(ctx, collection, redisClient)

}

func main() {
	gin.SetMode(gin.ReleaseMode)
	router := gin.Default()
	router.GET("/recipes", recipesHandler.ListRecipesHandler)
	router.POST("/recipes", recipesHandler.NewRecipeHandler)
	router.PUT("/recipes/:id", recipesHandler.UpdateRecipeHandler)
	router.DELETE("/recipes/:id", recipesHandler.DeleteRecipeHandler)
	router.GET("/recipes/:id", recipesHandler.GetOneRecipeHandler)

	// 优雅关闭web服务
	srv := &http.Server{
		Addr:    "127.0.0.1:8080",
		Handler: router,
	}

	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			log.Fatal("listen failed, ", err)
		}
	}()

	defer func() {
		if err = client.Disconnect(context.TODO()); err != nil {
			panic(err)
		}
	}()

	quit := make(chan os.Signal, 1)
	signal.Notify(quit, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
	<-quit
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	if err := srv.Shutdown(ctx); err != nil {
		log.Fatalf("server shutdown failed, err: %v\n", err)
	}
	select {
	case <-ctx.Done():
		log.Println("timeout of 2 seconds")
	}
	log.Println("server shutdown")
}

参考

附录

准备数据

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"time"

	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/mongo/readpref"
)

type Recipe struct {
	ID           string    `json:"id"`
	Name         string    `json:"name"`
	Tags         []string  `json:"tags"`
	Ingredients  []string  `json:"ingredients"`
	Instructions []string  `json:"instructions"`
	PublishedAt  time.Time `json:"publishedAt"`
}

// 保存recipes
var recipes []Recipe

var ctx context.Context
var err error
var client *mongo.Client

func init() {
	recipes = make([]Recipe, 0)
	// 读取当前目录下的json文件
	file, _ := ioutil.ReadFile("recipes.json")
	_ = json.Unmarshal([]byte(file), &recipes)

	ctx = context.Background()
	// demo这个数据库可能需要先创建
	client, err = mongo.Connect(ctx, options.Client().ApplyURI("mongodb://root:123456@192.168.0.20:27017/demo?authSource=admin"))
	if err = client.Ping(context.TODO(), readpref.Primary()); err != nil {
		log.Fatal("connect ot mongodb failed: ", err)
	}

	log.Println("Connected to MongoDB")
	var listOfRecipes []interface{}
	for _, recipe := range recipes {
		listOfRecipes = append(listOfRecipes, recipe)
	}

	collection := client.Database("demo").Collection("recipes")
	insertManyResult, err := collection.InsertMany(ctx, listOfRecipes)
	if err != nil {
		log.Fatal(err)
	}
	log.Println("Inserted recipes: ", len(insertManyResult.InsertedIDs))
}

func main() {
	fmt.Println("insert many data to mongodb")
}
  • 也可以使用mongoimport将json数据直接插入到数据库中
mongoimport --username admin --password password --authenticationDatabase admin \
    --db demo --collection recipes --file recipes.json --jsonArray

python测试

import requests
import json


def post_test(data):
    url = "http://127.0.0.1:8080/recipes"
    resp = requests.post(url=url, data=json.dumps(data))
    # print("post test")
    print(resp.text)

def get_test():
    url = "http://127.0.0.1:8080/recipes"
    resp = requests.get(url)
    # print("get test")
    print(resp.text)


def put_test(data, id):
    url = f"http://127.0.0.1:8080/recipes/{id}"
    # data["id"] = id
    resp = requests.put(url=url, data=json.dumps(data))
    # print("put test")
    print(json.loads(resp.text))

def delete_test(id):
    url = f"http://127.0.0.1:8080/recipes/{id}"
    resp = requests.delete(url=url)
    print("delete test")
    print(resp.text)

def get_test_search(id):
    url = f"http://127.0.0.1:8080/recipes/{id}"
    resp = requests.get(url=url)
    print("get test search")
    print(resp.text)


if __name__ == "__main__":
    data1 = {
        "name": "Homemade Pizza",
        "tags": ["italian", "pizza", "dinner"],
        "ingredients": [
            "1 1/2 cups (355 ml) warm water (105°F-115°F)",
            "1 package (2 1/4 teaspoons) of active dry yeast",
            "3 3/4 cups (490 g) bread flour",
            "feta cheese, firm mozzarella cheese, grated",
        ],
        "instructions": [
            "Step 1.",
            "Step 2.",
            "Step 3.",
        ],
    }
    data2 = {
        "name": "西红柿炒鸡蛋",
        "tags": ["家常菜", "新手必会"],
        "ingredients": [
            "2个鸡蛋",
            "1个番茄, 切片",
            "葱花, 蒜瓣等",
        ],
        "instructions": [
            "步骤1",
            "步骤2",
            "步骤3",
        ],
    }
    data3 = {
        "name": "蒸蛋",
        "tags": ["家常菜", "新手必会"],
        "ingredients": [
            "2个鸡蛋",
            "葱花, 蒜瓣等",
        ],
        "instructions": [
            "步骤1",
            "步骤2",
            "步骤3",
            "步骤4",
        ],
    }
    # post_test(data1)
    # post_test(data2)
    # put_test(data2, id="62b7d298bb2ffa932f0d213d")
    # get_test_search(id="62b6e5746202e6a3c26b0afb")
    # delete_test(id="123456")
    get_test()
posted @ 2022-06-27 00:05  花酒锄作田  阅读(1129)  评论(0编辑  收藏  举报