Lessons
How to Query for Random Document from Mongo with Go
In this tutorial, I will show you how to query for a random document in a Mongo database instance with Go code. I will be on local and provide you with all steps.
Setup
I'm assuming you have Mongo installed on your local machine. I also recommend installing Mongo Compass to make it easier to see your data.
I am assuming you have Go installed and have successful compiled at least one Go file. This would confirm your environment works. We will be using my Go boierplate code to save time on setting up our code structure. It follows a handlers
, models
, repositories
, services
, and utils
structure.
In short:
handlers
handles the inbound request.models
contains all structs that represent data.repositories
has all methods for interacting with the database.services
contains the business logic of the app.utils
has any helper methods.
Download the Go boilerplate. Please give it a star on Github so I know people are still using and enjoying it. Move the files into your new project root. Open the files up in a text editor.
You need to do one thing for this boilerplate to work. Open services/user_service.go
and within the IsValidPassword
function. You will need to switch the variable to true
. Do NOT use this code in production without fixing it. I leave encryption and password requirements up to you. This will store a plain text password till you add code to GetEncryptedPassword
and IsValidPassword
. For demo and dev purposes, this is fine.
You will need data in your database and checkout the insert tutorial.
Query Code
This code has NOT be included in the boilerplate. I used as the initial response to my home page suggestion response. We are going to add a new repository method, a new service method, a new handler method and add a route. Starting with the database layer and working towards the API, in repositories/car_repository.go
add:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
func (c *CarsRepository) GetRandom(email string, query models.ListCarQuery) ([]models.Car, error) { maxNum := 1 // The number of response attributes. Returns only one random car. filters := query.Filter(email) var cars []models.Car cursor, err := c.db.Collection("cars").Aggregate(context.Background(), []bson.M{bson.M{"$match": filters}, bson.M{"$sample": bson.M{"size": maxNum}}}) if err != nil { return []models.Car{}, err } for cursor.Next(context.Background()) { car := models.Car{} err := cursor.Decode(&car) if err != nil { //handle err } else { cars = append(cars, car) } } return cars, nil }
This call to the database using the Mongo driver grabs a "sample" of the data. You can see one of the options we are passing through is maxNum
. This represents the maximum number of elements you want to sample. We use ListCarQuery
as a filter. This allows us to continue to filter by model, make, year, status and email. This method does not use the page and limit attributes.
Next, we will add the service. This will be very similar to GetAll
except it returns only one element. In services/car_service.go
, add:
1 2 3 4 5 6 7 8 9 10
func (c *CarsService) GetRandom(session models.Session, query models.ListCarQuery) (models.Car, error) { cars, err := c.carsRepository.GetRandom(session.Email, query) if err != nil { return models.Car{}, err } if (len(cars) != 1) { return models.Car{}, errors.New("error: please add a car first") } return cars[0], nil }
In handlers/cars_handler.go
, add GetRandom
. Again, it's very similar to the GetAll
. This is for calling the GetRandom
service.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
func (u *CarsHandler) GetRandom(c *gin.Context) { session, exists := u.GetSession(c) if !exists { c.JSON(403, gin.H{"message": "error: unauthorized"}) return } page := c.DefaultQuery("page", "1") limit := c.DefaultQuery("limit", "25") make := c.DefaultQuery("make", "") model := c.DefaultQuery("model", "") year := c.DefaultQuery("year", "0") yearInt, err := strconv.Atoi(year) if err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } pageInt, err := strconv.Atoi(page) if err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } limitInt, err := strconv.Atoi(limit) if err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } if (pageInt < 1) { c.JSON(400, gin.H{"message": "error: page must be greater than one"}) return } if (limitInt < 1 || limitInt > 30) { c.JSON(400, gin.H{"message": "error: limit must be be between 1 and 30"}) return } query := models.ListCarQuery{ Page: pageInt, Limit: limitInt, Make: make, Model: model, Year: yearInt, } v := validator.New() if err := v.Struct(query); err != nil { fmt.Print("Validation failed.") c.JSON(400, gin.H{"message": err.Error()}) return } car, err := u.CarsService.GetRandom(session, query) if err != nil { c.JSON(400, gin.H{"message": err.Error()}) return } c.JSON(200, gin.H{"message": "Random car retrieved", "car": car}) return }
Lastly, we need to add a new endpoint to call the handler. Since we have structured an endpoint to be /cars/:id
. We cannot use /cars/random
or any other extension that doesn't require an ID. We will add a whole new one. I don't know if I completely agree with this but I will leave it up to your API design decision.
In main.go
:
1 2 3 4
specialAPI := router.Group("/special") { specialAPI.GET("/cars/random", ValidateAuth(userRepository), carsHandler.GetRandom) }
Demo
Now, let's test this all out. First verify you have data in your database to pull from.
You will then need to sign up or sign into an account. We want to get a token
from the response for authentication purposes. My request looks like:
Or the curl command:
1 2 3 4 5 6
curl --location --request POST 'http://localhost:8080/user/signup/' \ --header 'Content-Type: application/json' \ --data-raw '{ "email": "me@keithweaver.ca", "password": "demodemo1" }'
I capture the token. I'm going to use it with my next request. We add a GET
request at /special/cars/random
. It should return a single random car. My request in POSTman:
Or the curl command:
1 2 3
curl --location --request GET 'http://localhost:8080/special/cars/random' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer 5f8506728861d706c00df190'
I'm going to make the request again to confirm that the randomize feature works.
As you can see I got back a different car. Thanks for reading!