CRUD with MongoDB and Go - part #1
Author: Carmelo C.
Published: Oct 29, 2024mongodb
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'
}
]