Working MongoDB with Golang

burrock

burak

Posted on November 11, 2021

Working MongoDB with Golang

Every tutorial has a story. In that tutorial you'll find out different contents that is related to MongoDB, GoLang and working with mock data and deployment. Here is my content.

Project structure

Image description

PS: Here is the one different folder that name is dummy_api. That folder has own main file. What does it mean? When I run the main.go file before I'll add mock data. If you didn't catch up Working with the marshal and unmarshal tutorial you should read it. Another important topics is "context". Essential Go

context.TODO()
TODO returns a non-nil, empty Context. Code should use context.TODO when it’s unclear which Context to use or it is not yet available (because the surrounding function has not yet been extended to accept a Context parameter). TODO is recognized by static analysis tools that determine whether Contexts are propagated correctly in a program.

context.Background()
Background returns a non-nil, empty Context. It is never canceled, has no values, and has no deadline. It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.

Image description

Image description

Another different package is about MongoDb Client however I'll talk about below.

package main

import (
    "context"
    "encoding/json"
    "io/ioutil"
    "os"

    "github.com/bburaksseyhan/contact-api/src/pkg/client/mongodb"
    "github.com/bburaksseyhan/contact-api/src/pkg/model"
    "github.com/sirupsen/logrus"
)

func main() {

    contactsJson, err := os.Open("contacts.json")

    if err != nil {
        logrus.Error("contact.json an error occurred", err)
    }

    defer contactsJson.Close()

    var contacts []model.Contact

    byteValue, _ := ioutil.ReadAll(contactsJson)

    //unmarshall data
    if err := json.Unmarshal(byteValue, &contacts); err != nil {
        logrus.Error("unmarshall an error occurred", err)
    }

    logrus.Info("Data\n", len(contacts))

    //import mongo client
    client := mongodb.ConnectMongoDb("mongodb://localhost:27017")
    logrus.Info(client)

    defer client.Disconnect(context.TODO())

    collection := client.Database("ContactDb").Collection("contacts")

    logrus.Warn("Total data count:", &contacts)

    for _, item := range contacts {
        collection.InsertOne(context.TODO(), item)
    }

    logrus.Info("Data import finished...")
}
Enter fullscreen mode Exit fullscreen mode

Firstly let's open the terminal and goes to dummy_api directory. Another important thing, is database running? Let's have look.

docker compose up

docker container ls

Image description

Working with mock data

I was creating mock data from Mockaroo

Working with MongoDB queries

docker exec -it ad2d44477f28 mongo //connect to mongodb cli

help

show dbs // return all database names

use ContactDb 

show collections // return collection name

db.contacts.find() //return all collections

db.contacts.find({}).count() // return row count

db.contacts.find({}).pretty({}) // return rows with a format

db.contacts.find({"email":""})
db.dropDatabase() // remove database
Enter fullscreen mode Exit fullscreen mode

Image description

Image description

Image description

Image description

Using packages

  • "go get -u go.mongodb.org/mongo-driver/bson"
  • "go get -u go.mongodb.org/mongo-driver/mongo"
  • "go get -u go.mongodb.org/mongo-driver/mongo/options"
  • "go get -u github.com/gin-gonic/gin"
  • "go get -u github.com/sirupsen/logrus"
  • "go get -u github.com/spf13/viper"

Implementation

main.go file

That file read config.yml or .env file after that call the Init function.

package main

import (
    "os"

    "github.com/bburaksseyhan/contact-api/src/cmd/utils"
    "github.com/bburaksseyhan/contact-api/src/pkg/server"
    log "github.com/sirupsen/logrus"
    "github.com/spf13/viper"
)

func main() {

    config := read()
    log.Info("Config.yml", config.Database.Url)

    mongoUri := os.Getenv("MONGODB_URL")

    if mongoUri != "" {
        config.Database.Url = mongoUri
    }

    log.Info("MONGODB_URL", mongoUri)

    server.Init(config.Database.Url)
}

func read() utils.Configuration {
    //Set the file name of the configurations file
    viper.SetConfigName("config")

    // Set the path to look for the configurations file
    viper.AddConfigPath(".")

    // Enable VIPER to read Environment Variables
    viper.AutomaticEnv()

    viper.SetConfigType("yml")
    var config utils.Configuration

    if err := viper.ReadInConfig(); err != nil {
        log.Error("Error reading config file, %s", err)
    }

    err := viper.Unmarshal(&config)
    if err != nil {
        log.Error("Unable to decode into struct, %v", err)
    }

    return config
}

Enter fullscreen mode Exit fullscreen mode

ConnectMongoDb function takes the MongoDB URL parameter so this function opens the connection and check the status. Related Documentation

package mongodb

import (
    "context"

    log "github.com/sirupsen/logrus"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

func ConnectMongoDb(url string) *mongo.Client {

    clientOptions := options.Client().ApplyURI(url)

    // Connect to MongoDB
    client, err := mongo.Connect(context.TODO(), clientOptions)

    if err != nil {
        log.Fatal(err)
    }

    // Check the connection
    err = client.Ping(context.TODO(), nil)

    if err != nil {
        log.Fatal(err)
    }

    log.Info("MongoClient connected")

    return client
}


Enter fullscreen mode Exit fullscreen mode

unfinished contact_handler.go file. HealthCheck function is not only health check. That function checks the MongoDb database status with a timeout. If any cancellation comes from the server, context will be triggered and response will be un-health. Let's think the opposite result will be pong.

package handler

import (
    "context"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/sirupsen/logrus"
    "go.mongodb.org/mongo-driver/mongo"
)

type ContactHandler interface {
    GetAllContacts(*gin.Context)
    GetContactByCity(*gin.Context)
    HealthCheck(*gin.Context)
}

type contactHandler struct {
    client *mongo.Client
}

func NewContactHandler(client *mongo.Client) ContactHandler {
    return &contactHandler{client: client}
}

func (ch *contactHandler) GetAllContacts(c *gin.Context) {
    _, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
    defer ctxErr()

    //request on repository

    c.JSON(http.StatusOK, gin.H{"contacts": "pong"})
}

func (ch *contactHandler) GetContactByCity(c *gin.Context) {
    _, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
    defer ctxErr()

    //request on repository

    c.JSON(http.StatusOK, gin.H{"contacts": "pong"})
}

func (ch *contactHandler) HealthCheck(c *gin.Context) {

    ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
    defer ctxErr()

    if ctxErr != nil {
        logrus.Error("somethig wrong!!!", ctxErr)
    }

    if err := ch.client.Ping(ctx, nil); err != nil {
        c.JSON(http.StatusOK, gin.H{"status": "unhealty"})
    }

    c.JSON(http.StatusOK, gin.H{"status": "pong"})
}

Enter fullscreen mode Exit fullscreen mode

Image description

Image description

Image description

Image description

Image description

Completed codes

main

package main

import (
    "os"

    "github.com/bburaksseyhan/contact-api/src/cmd/utils"
    "github.com/bburaksseyhan/contact-api/src/pkg/server"
    log "github.com/sirupsen/logrus"
    "github.com/spf13/viper"
)

func main() {

    config := read()
    log.Info("Config.yml", config.Database.Url)

    mongoUri := os.Getenv("MONGODB_URL")
    serverPort := os.Getenv("SERVER_PORT")
    dbName := os.Getenv("DBNAME")
    collection := os.Getenv("COLLECTION")

    if mongoUri != "" {
        config.Database.Url = mongoUri
        config.Server.Port = serverPort
        config.Database.DbName = dbName
        config.Database.Collection = collection
    }

    log.Info("MONGODB_URL", mongoUri)

    server.Init(config)
}

func read() utils.Configuration {
    //Set the file name of the configurations file
    viper.SetConfigName("config")

    // Set the path to look for the configurations file
    viper.AddConfigPath(".")

    // Enable VIPER to read Environment Variables
    viper.AutomaticEnv()

    viper.SetConfigType("yml")
    var config utils.Configuration

    if err := viper.ReadInConfig(); err != nil {
        log.Error("Error reading config file, %s", err)
    }

    err := viper.Unmarshal(&config)
    if err != nil {
        log.Error("Unable to decode into struct, %v", err)
    }

    return config
}

Enter fullscreen mode Exit fullscreen mode

config

package utils

type Configuration struct {
    Database DatabaseSetting
    Server   ServerSettings
}

type DatabaseSetting struct {
    Url        string
    DbName     string
    Collection string
}

type ServerSettings struct {
    Port string
}

Enter fullscreen mode Exit fullscreen mode

server

package server

import (
    "github.com/bburaksseyhan/contact-api/src/cmd/utils"
    "github.com/bburaksseyhan/contact-api/src/pkg/client/mongodb"
    "github.com/bburaksseyhan/contact-api/src/pkg/handler"
    repository "github.com/bburaksseyhan/contact-api/src/pkg/repository/mongodb"

    "github.com/gin-gonic/gin"
    log "github.com/sirupsen/logrus"
)

func Init(config utils.Configuration) {

    // Creates a gin router with default middleware:
    // logger and recovery (crash-free) middleware
    router := gin.Default()

    client := mongodb.ConnectMongoDb(config.Database.Url)

    repo := repository.NewContactRepository(&config, client)
    handler := handler.NewContactHandler(client, repo)

    router.GET("/", handler.GetAllContacts)
    router.GET("/contacts/:email", handler.GetContactByEmail)
    router.POST("/contact/delete/:id", handler.DeleteContact)

    router.GET("/health", handler.HealthCheck)

    log.Info("port is :8080\n", config.Database.Url)

    // PORT environment variable was defined.
    router.Run(":" + config.Server.Port + "")
}

Enter fullscreen mode Exit fullscreen mode

handler

package handler

import (
    "context"
    "net/http"
    "strconv"
    "time"

    "github.com/bburaksseyhan/contact-api/src/pkg/model"
    db "github.com/bburaksseyhan/contact-api/src/pkg/repository/mongodb"

    "github.com/gin-gonic/gin"
    "github.com/sirupsen/logrus"
    "go.mongodb.org/mongo-driver/mongo"
)

type ContactHandler interface {
    GetAllContacts(*gin.Context)
    GetContactByEmail(*gin.Context)
    DeleteContact(*gin.Context)

    HealthCheck(*gin.Context)
}

type contactHandler struct {
    client     *mongo.Client
    repository db.ContactRepository
}

func NewContactHandler(client *mongo.Client, repo db.ContactRepository) ContactHandler {
    return &contactHandler{client: client, repository: repo}
}

func (ch *contactHandler) GetAllContacts(c *gin.Context) {

    ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
    defer ctxErr()

    var contactList []*model.Contact

    //request on repository
    if result, err := ch.repository.Get(ctx); err != nil {
        logrus.Error(err)
    } else {
        contactList = result
    }

    c.JSON(http.StatusOK, gin.H{"contacts": &contactList})
}

func (ch *contactHandler) GetContactByEmail(c *gin.Context) {

    ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
    defer ctxErr()

    var contactList *model.Contact

    //get parameter
    email := c.Param("email")

    //request on repository
    if result, err := ch.repository.GetContactByEmail(email, ctx); err != nil {
        logrus.Error(err)
    } else {
        contactList = result
    }

    c.JSON(http.StatusOK, gin.H{"contacts": contactList})
}

func (ch *contactHandler) HealthCheck(c *gin.Context) {

    ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
    defer ctxErr()

    if ctxErr != nil {
        logrus.Error("somethig wrong!!!", ctxErr)
    }

    if err := ch.client.Ping(ctx, nil); err != nil {
        c.JSON(http.StatusOK, gin.H{"status": "unhealty"})
    }

    c.JSON(http.StatusOK, gin.H{"status": "pong"})
}

func (ch *contactHandler) DeleteContact(c *gin.Context) {

    ctx, ctxErr := context.WithTimeout(c.Request.Context(), 30*time.Second)
    defer ctxErr()

    //get parameter
    id, err := strconv.Atoi(c.Param("id"))
    if err != nil {
        logrus.Error("Can not convert to id", err)
    }

    //request on repository
    result, err := ch.repository.Delete(id, ctx)
    if err != nil {
        logrus.Error(err)
    }

    c.JSON(http.StatusOK, gin.H{"deleteResult.DeletedCount": result})
}
Enter fullscreen mode Exit fullscreen mode

repository

package repository

import (
    "context"

    "github.com/bburaksseyhan/contact-api/src/cmd/utils"
    "github.com/bburaksseyhan/contact-api/src/pkg/model"
    "github.com/sirupsen/logrus"
    log "github.com/sirupsen/logrus"
    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/bson/primitive"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

type ContactRepository interface {
    Get(ctx context.Context) ([]*model.Contact, error)
    GetContactByEmail(email string, ctx context.Context) (*model.Contact, error)
    Delete(id int, ctx context.Context) (int64, error)
}

type contactRepository struct {
    client *mongo.Client
    config *utils.Configuration
}

func NewContactRepository(config *utils.Configuration, client *mongo.Client) ContactRepository {
    return &contactRepository{config: config, client: client}
}

func (c *contactRepository) Get(ctx context.Context) ([]*model.Contact, error) {

    findOptions := options.Find()
    findOptions.SetLimit(100)

    var contacts []*model.Contact

    collection := c.client.Database(c.config.Database.DbName).Collection(c.config.Database.Collection)

    // Passing bson.D{{}} as the filter matches all documents in the collection
    cur, err := collection.Find(ctx, bson.D{{}}, findOptions)
    if err != nil {
        log.Fatal(err)
        return nil, err
    }

    // Finding multiple documents returns a cursor
    // Iterating through the cursor allows us to decode documents one at a time
    for cur.Next(context.TODO()) {
        // create a value into which the single document can be decoded
        var elem model.Contact
        if err := cur.Decode(&elem); err != nil {
            log.Fatal(err)
            return nil, err
        }

        contacts = append(contacts, &elem)
    }

    cur.Close(ctx)

    return contacts, nil
}

func (c *contactRepository) GetContactByEmail(email string, ctx context.Context) (*model.Contact, error) {

    findOptions := options.Find()
    findOptions.SetLimit(100)

    var contacts *model.Contact

    collection := c.client.Database(c.config.Database.DbName).Collection(c.config.Database.Collection)

    filter := bson.D{primitive.E{Key: "email", Value: email}}

    logrus.Info("Filter", filter)

    collection.FindOne(ctx, filter).Decode(&contacts)

    return contacts, nil
}

func (c *contactRepository) Delete(id int, ctx context.Context) (int64, error) {

    collection := c.client.Database(c.config.Database.DbName).Collection(c.config.Database.Collection)

    filter := bson.D{primitive.E{Key: "id", Value: id}}

    deleteResult, err := collection.DeleteOne(ctx, filter)
    if err != nil {
        log.Fatal(err)

        return 0, err
    }

    return deleteResult.DeletedCount, nil
}

Enter fullscreen mode Exit fullscreen mode

Deployment

docker compose up

Image description

Repository

Thank you,

💖 💪 🙅 🚩
burrock
burak

Posted on November 11, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Working MongoDB with Golang
go Working MongoDB with Golang

November 11, 2021