Working MongoDB with Golang
burak
Posted on November 11, 2021
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
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.
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...")
}
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
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
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
}
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
}
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"})
}
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
}
config
package utils
type Configuration struct {
Database DatabaseSetting
Server ServerSettings
}
type DatabaseSetting struct {
Url string
DbName string
Collection string
}
type ServerSettings struct {
Port string
}
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 + "")
}
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})
}
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
}
Deployment
docker compose up
Thank you,
Posted on November 11, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.