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.

Screenshot of Github Workflow Running

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:

Screenshot of Github Workflow Running

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:

Screenshot of Github Workflow Running

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.

Screenshot of Github Workflow Running

As you can see I got back a different car. Thanks for reading!