CRUD with MongoDB and Go - part #1

Author: Carmelo C.
Published: Oct 29, 2024
mongodb go golang crud docker containers

I’ve always been fascinated by DataBases. Such a long history and, nowadays, so many different flavours.

Here’s my attempt to put together a simple application to Create, Read, Update, and Delete (hence, CRUD) records in a DB.

Let’s start small, we’ll run an instance of MongoDB locally. In this article we’ll interact with the DB manually to get an idea of its inner operations.

MongoDB is a non-relational (a.k.a. NoSQL) document database which can run locally from within a Docker container.

NOTE: the installation of Docker will not be discussed.


1. Baby steps

Quick-run MongoDB with Docker:

$ docker pull mongo:8


$ docker run -d \    # run in detached mode
  --rm \             # remove container after use
  -p 27017:27017 \   # ports to be published
  --name mongodb \   # container name
  mongo:8            # base image

Let’s access the running container:

$ docker exec -it mongodb bash


root@0dc1acb0408e:/# mongosh
Current Mongosh Log ID:	671f4a9fddb211ec5659139d
Connecting to:		mongodb://127.0.0.1:27017/?directConnection=true&serverSelectionTimeoutMS=2000&appName=mongosh+2.3.2
Using MongoDB:		8.0.3
Using Mongosh:		2.3.2
...


test> db
test


test> show dbs
admin   40.00 KiB
config  12.00 KiB
local   40.00 KiB


test> quit

NOTE: show dbs displays any existing databases. At the moment, only the default DBs can be found.

Yup, that worked. But we can make things a bit more sophisticated. Docker lets us optionally specify a network, and a volume:

$ docker run -d \
  --rm \
  -p 27017:27017 \
  --network backend \                # `network` adds segmentation to the containers
  --volume mongodb_data:/data/db \   # `volume` adds persistence of the data
  --name mongodb \
  mongo:8

2. Access our data

Let’s see how we can store and retrieve data. To make things a bit more realistic let’s do that from a separate container:

$ docker run -it --rm --network backend --name mongoclient mongo:8 bash


root@aa12acad8d12:/# mongosh mongodb://mongodb:27017/Movies
Current Mongosh Log ID:	6720bf7fa46babd44c59139d
Connecting to:		mongodb://mongodb:27017/test?directConnection=true&appName=mongosh+2.3.2
Using MongoDB:		8.0.3
Using Mongosh:		2.3.2
...


Movies>

NOTE: it’s important that mongoclient be connected to the same network as mongodb.

Cool! That was a nice proof-of-concept, and very quick to implement too. A tad too imperative though… let’s make it more declarative by means of Docker Compose and YAML configuration files.


3. Declarative approach

Clean-up first!

$ docker kill mongodb; docker network rm backend; docker volume rm mongodb_data

Copy and paste the following text into a file named docker-compose.yaml:

services:
  mongodb:
    image: mongo:8
    container_name: mongodb
    hostname: mongodb
    ports:
      - "27017:27017"
    volumes:
      - mongodb_data:/data/db
    networks:
      - backend
    environment:
      MONGO_INITDB_ROOT_USERNAME: root      <<< root credentials
      MONGO_INITDB_ROOT_PASSWORD: example   <<< root credentials

volumes:
  mongodb_data:
  name: mongodb_data

networks:
  backend:
    name: backend

NOTE: this file contains all the info we’d specified before in our CLI commands with the sole exception of MONGO_INITDB_ROOT_USERNAME and MONGO_INITDB_ROOT_PASSWORD.

The run command is replaced by:

$ docker compose up -d
[+] Running 3/3
 ✔ Network backend        Created
 ✔ Volume "mongodb_data"  Created
 ✔ Container mongodb      Started

Docker Compose takes care of creating the network, the volume, and to start the container.

A project gets created taking the name of the source directory, a service whose name is declared in the configuration and, finally, a container.


4. Let’s start again

Again, let’s try and access the DB from a separate container:

$ docker run -it --rm --network backend --name mongoclient mongo:8 bash


root@9d4655d5754f:/# mongosh mongodb://mongodb:27017/Movies
...


Movies> show dbs
MongoServerError[Unauthorized]: Command listDatabases requires authentication

We get an error but that’s totally expected since we’ve added some authentication info inside the YAML file. Let’s see how to authenticate as root, then create an ordinary user:

Movies> db = db.getSiblingDB("admin")
admin


admin> db.auth("root", "example")
{ ok: 1 }


admin> db.createUser({
         user: "dbuser",
         pwd: "pass123",
         roles: [{ role: "userAdminAnyDatabase", db: "admin" },
                 { role: "readWrite", db: "Movies" } ],
         mechanisms: ["SCRAM-SHA-1"],
       });
{ ok: 1 }


admin> db.auth("dbuser", "pass123")
{ ok: 1 }


admin> db = db.getSiblingDB("Movies")
Movies


Movies> show dbs
admin   100.00 KiB
config   12.00 KiB
local    72.00 KiB

Good! User dbuser is now able to operate on the Movies database. We’re ready to start our first CRUD operations.

Insert one document

Movies> db.moviesList.insertOne({title: "Dune", year: 1984, director: "David Lynch"})
{
  acknowledged: true,
  insertedId: ObjectId('6720c52b7bbbcd418059139e')
}

NOTE: MongoDB, being a NoSQL database, is schema-less. There’s no need to define tables and their schemas, sweet! Therefore we define a new collection (= moviesList) by simply using it.

Insert many documents

Movies> db.moviesList.insertMany([{title: "Avatar", year: 9999, director: "James Cameron"}, {title: "The Hobbit", year: 2012, director: "Peter Jackson"}, {title: "Arrival", year: 2016, director: "Denis Villeneuve"}, {title: "B Movie", year: 1234, director: "Unknown Person"}])
{
  acknowledged: true,
  insertedIds: {
    '0': ObjectId('6720c57c7bbbcd418059139f'),
    '1': ObjectId('6720c57c7bbbcd41805913a0'),
    '2': ObjectId('6720c57c7bbbcd41805913a1'),
    '3': ObjectId('6720c57c7bbbcd41805913a2')
  }
}

Retrieve (all) documents

Movies> db.moviesList.find()
[
  {
    _id: ObjectId('6720c52b7bbbcd418059139e'),
    title: 'Dune',
    year: 1984,
    director: 'David Lynch'
  },
  ...
]

Retrieve one document

Movies> db.moviesList.find({ title: 'Avatar' })
[
  {
    _id: ObjectId('6720c57c7bbbcd418059139f'),
    title: 'Avatar',
    year: 9999,
    director: 'James Cameron'
  }
]

Update one document

Movies> db.moviesList.updateOne({ year: 9999}, {$set: { year: 2009 } })
{
  acknowledged: true,
  insertedId: null,
  matchedCount: 1,
  modifiedCount: 1,
  upsertedCount: 0
}


Movies> db.moviesList.find({ title: 'Avatar' })
[
  {
    _id: ObjectId('6720c57c7bbbcd418059139f'),
    title: 'Avatar',
    year: 2009,
    director: 'James Cameron'
  }
]

Delete one document

Movies> db.moviesList.deleteOne({ director: 'Unknown Person' })
{ acknowledged: true, deletedCount: 1 }


Movies> db.moviesList.countDocuments()
4

5. Final test

Logout, shutdown the project, restart and, behold, our data are still there (hint: thank you volume):

$ docker compose down
[+] Running 2/2
 ✔ Container mongodb  Removed
 ✔ Network backend    Removed

NOTE: the network has been removed, the volume hasn’t. Also, when using Docker Compose, there’s no need to clean up after us: unused resources are removed automatically.

$ docker compose up -d
[+] Running 2/2
 ✔ Network backend    Created
 ✔ Container mongodb  Started


$ docker run -it --rm --network backend --name mongoclient mongo:8 bash


root@ae9e94a8fdea:/# mongosh mongodb://mongodb:27017/Movies


Movies> db = db.getSiblingDB("admin")
admin


admin> db.auth("dbuser", "pass123")
{ ok: 1 }


admin> db = db.getSiblingDB("Movies")
Movies


Movies> show collections
moviesList


Movies> db.moviesList.find({ director: 'Denis Villeneuve' })
[
  {
    _id: ObjectId('6720c57c7bbbcd41805913a1'),
    title: 'Arrival',
    year: 2016,
    director: 'Denis Villeneuve'
  }
]