Go

How to Do CRUD Transactions in MongoDB with Go

Jun 25, 2021
hackajob Staff

You asked, we listened and so we're adding more Go tutorials to our blog! We're back with how to do CRUD transactions in Mongo DB with Go. Buckle up, we're about to dive in. Today we'll use MongoDB Compass to view the database. Feel free to access it here.

First, let's create a database named example and a collection named books by clicking the Create Database button.

first

Then go to example and click the Create Collection button to create a new collection named authors.

second

We created the database and collections we need, now we can go to the #Go side. (We apologise in hindsight for that dreadful joke).
We'll use mongo-go-driver as the driver. You can access the repo here.
First, let's connect with MongoDB:

import (
	"context"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"log"
)

var (
	BooksCollection     *mongo.Collection
	AuthorsCollection   *mongo.Collection
	Ctx                 = context.TODO()
)

/*Setup opens a database connection to mongodb*/
func Setup() {
	host := "127.0.0.1"
	port := "27017"
	connectionURI := "mongodb://" + host + ":" + port + "/"
	clientOptions := options.Client().ApplyURI(connectionURI)
	client, err := mongo.Connect(Ctx, clientOptions)
	if err != nil {
		log.Fatal(err)
	}

	err = client.Ping(Ctx, nil)
	if err != nil {
		log.Fatal(err)
	}

	db := client.Database("example")
	BooksCollection = db.Collection("books")
	AuthorsCollection = db.Collection("authors")
}

Here, we provide the connection by giving the ConnectionURI string to the ApplyURI function and calling Connect. We test whether the connection with client.Ping is established or not, if not, we stop the application with a fatal error. Then we set our database and collections.Since we will need to use AuthorsCollection and BooksCollection objects and context objects in our crud operations, we have defined them as global.

Let's define our documents and structs that we will add to collections:

type Book struct {
	Name    	string	`bson:"name"`
	Author  	string  `bson:"author"`
	PageCount	int     `bson:"page_count"`
}

type Author struct {
	FullName string		`bson:"full_name"`
}

Here we give our variables the bson tag. In MongoDB, documents are kept in the form of binary JSON and the bson tag means binary JSON in the driver we use.

Now that we have defined our models, let's write 6 functions for our crud operations and let them do the following:

  • CreateBook() -> Adds a new record to books collection
  • GetBook() -> Returns the record from books collection according to the id parameter
  • GetBooks() -> Returns all records
  • UpdateBooks() -> Updates the record according to the id parameter
  • FindAuthorBooks() -> Fetches all books by an author
  • DeleteBook() -> Deletes the record with the id parameter

CreateBook :

func CreateBook(b Book) (string, error) {
	result, err := BooksCollection.InsertOne(Ctx, b)
	if err != nil {
		return "0", err
	}
	return 	fmt.Sprintf("%v", result.InsertedID), err
}

The CreateBook function takes a book object and returns the id and error of the object added back. Since we added records to the Books collection, we used the BooksCollection object. We added two records and MongoDB automatically created unique IDs of type unique ObjectID for them. If we want to create an id and give it ourselves, it will be sufficient to create the Id field in the structs and set _id as the tag.

third

GetBook :

func GetBook(id string) (Book, error) {
	var b Book
	objectId, err := primitive.ObjectIDFromHex(id)
	if err != nil{
		return b,err
	}
	
	err = BooksCollection.
		FindOne(Ctx, bson.D).
		Decode(&b)
	if err != nil {
		return b, err
	}
	return b, nil
}

Since we'll search from ObjectId type here, we convert the id we entered as a string to ObjectId type with the function primitive.ObjectIDFromHex.
We can also do this as a separate function and convert the GetBook() function to one that takes ObjectId directly as a parameter, so we also provide the single responsibility where condition in SQL corresponds to filter here and we provide it with bson.D object.

GetBooks:

func GetBooks() ([]Book, error) {
	var book Book
	var books []Book

	cursor, err := BooksCollection.Find(Ctx, bson.D{})
	if err != nil {
		defer cursor.Close(Ctx)
		return books, err
	}

	for cursor.Next(Ctx) {
		err := cursor.Decode(&book)
		if err != nil {
			return books,err
		}
		books = append(books, book)
	}

	return books, nil
}

While finding all the records, we do a similar job to the GetBook function.
The only difference is that this time we use the Find function instead of the FindOne function, and this function returns a cursor for us.
We return by saying cursor.Next in a for loop and append each record to the Book array.

UpdateBook:

func UpdateBook(id primitive.ObjectID, pageCount int) error {
	filter := bson.D
	update := bson.D}}
	_, err := BooksCollection.UpdateOne(
		Ctx,
		filter,
		update,
	)
	return err
}

Our UpdateBook function takes a primitive.ObjectId type id, we mentioned this in the GetBook function. With UpdateOne, we update the page_count of the record that matches the id we gave as parameter. We're not doing anything very interesting, we more or less got the logic from the previous functions :)

FindAuthorBook:

As you know, we can establish a relationship between two tables by using JOIN in a SQL database and ensure that all books belonging to an author are listed, but since MongoDB is a NoSQL database, we cannot establish a relationship like JOIN between collections. Instead, we need to use aggregate. For this, we create a new struct called AuthorsBook.
We set the fields of the Author struct and the []Book array as variables.

matchStage: We expect the Authors Collection to match the full_name.

lookupStage: We expect records matching full_name in Authors Collection to match author in Books Collection.

type AuthorBooks struct {
	FullName string		`bson:"full_name"`
	Books []Book
}

func FindAuthorBooks(fullName string) ([]Book, error) {
	matchStage := bson.D}}

	lookupStage := bson.D}}

	showLoadedCursor, err := AuthorsCollection.Aggregate(Ctx,
		mongo.Pipeline{matchStage, lookupStage})
	if err != nil {
		return nil, err
	}

	var a []AuthorBooks
	if err = showLoadedCursor.All(Ctx, &a); err != nil {
		return nil, err

	}
	return a[0].Books, err
}

DeleteBook:

func DeleteBook(id primitive.ObjectID) error {
	_, err := BooksCollection.DeleteOne(Ctx, bson.D)
	if err != nil {
		return err
	}
	return nil
}

Resources
If you enjoyed this, then check out some more resources here:
https://www.mongodb.com/languages/golang
https://kb.objectrocket.com/mongo-db/how-to-get-mongodb-documents-using-golang-446

Like what you've read or want more like this? Let us know! Email us here or DM us: Twitter, LinkedIn, Facebook, we'd love to hear from you.