Assume that you have a "users" and "logs" collections. A user can have many logs. As you can imagine, this is a mutable operation and you cannot guarantee how many logs a user could potentially have. It may be 1 or a million! In such cases it is ideal to store parent collection's reference in child collection. So in this case the log contains user reference in it. See example below.


Database content


Preparation


db.createCollection("users")
db.users.createIndex({"uuid":1},{unique:true,name:"UQ_uuid"})

db.createCollection("logs")
db.logs.createIndex({"uuid":1},{unique:true,name:"UQ_uuid"})
db.logs.createIndex({"user_uuid":1},{name:"IX_user_uuid"})
db.logs.createIndex({"created_at":1},{name:"IX_created_at"})

Users data


[
{
"_id": {
"$oid": "604fb496d42b77e01e7f03e0"
},
"uuid": "1ee18d53-a3fc-4493-9ca6-029a4890437e",
"name": "John"
},
{
"_id": {
"$oid": "604fb496d42b77e01e7f03e1"
},
"uuid": "a977db4a-7007-4147-b7bd-8d9565ef3dd7",
"name": "Andy"
},
{
"_id": {
"$oid": "604fb496d42b77e01e7f03e2"
},
"uuid": "40507244-d612-4fa4-b93f-c9c692f3b0fc",
"name": "Robert"
}
]

Logs data


[
{
"_id": {
"$oid": "604fb7f2d42b77e01e7f03e3"
},
"uuid": "0e5e800d-e8fb-4bfe-aaee-29d3c3b480ea",
"action": "login",
"created_at": {
"$date": "2021-03-15T19:39:30.252Z"
},
"user_uuid": "1ee18d53-a3fc-4493-9ca6-029a4890437e"
},
{
"_id": {
"$oid": "604fb7f2d42b77e01e7f03e4"
},
"uuid": "318277c2-c4a8-4c3a-8dc9-dc170d2cf6fc",
"action": "login",
"created_at": {
"$date": "2021-03-15T19:39:30.252Z"
},
"user_uuid": "40507244-d612-4fa4-b93f-c9c692f3b0fc"
},
{
"_id": {
"$oid": "604fb7f2d42b77e01e7f03e5"
},
"uuid": "dc10a79b-1b47-4b13-944e-9c9bb31fdb8c",
"action": "logout",
"created_at": {
"$date": "2021-03-15T19:39:31.252Z"
},
"user_uuid": "40507244-d612-4fa4-b93f-c9c692f3b0fc"
}
]

Storage


Models


package storage

import "context"

type UserStorer interface {
Find(ctx context.Context, uuid string) (UserRead, error)
}

type UserRead struct {
ID string `bson:"_id"`
UUID string `bson:"uuid"`
Name string `bson:"name"`
Logs []Log `bson:"logs"`
}

type Log struct {
ID string `bson:"_id"`
UUID string `bson:"uuid"`
Action string `bson:"action"`
CreatedAt time.Time `bson:"created_at"`
UserUUID string `bson:"user_uuid"`
}

Storer


package mongodb

import (
"context"
"log"
"time"

"github.com/you/mongo/internal/pkg/domain"
"github.com/you/mongo/internal/pkg/storage"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)

var _ storage.UserStorer = UserStorage{}

type UserStorage struct {
Database *mongo.Database
Timeout time.Duration
}

func (u UserStorage) Find(ctx context.Context, uuid string) (storage.UserRead, error) {
ctx, cancel := context.WithTimeout(ctx, u.Timeout)
defer cancel()

// Logs are desc sorted here!
qry := []bson.M{
{
"$match": bson.M{
"uuid": uuid,
},
},
{
"$lookup": bson.M{
// Define the logs collection for the join.
"from": "logs",
"pipeline": []bson.M{
// Select only the relevant logs from the logs collection.
{
"$match": bson.M{
"user_uuid": uuid,
},
},
// Sort logs by their created_at field in desc. 1 = asc
{
"$sort": bson.M{
"created_at": -1,
},
},
},
// Use logs as the field name to match struct field.
"as": "logs",
},
},
}

cur, err := u.Database.Collection("users").Aggregate(ctx, qry)
if err != nil {
log.Println(err)

return storage.UserRead{}, domain.ErrInternal
}

var usr []storage.UserRead

if err := cur.All(context.Background(), &usr); err != nil {
log.Println(err)

return storage.UserRead{}, domain.ErrInternal
}
defer cur.Close(context.Background())

if err := cur.Err(); err != nil {
log.Println(err)

return storage.UserRead{}, domain.ErrInternal
}

if len(usr) == 0 {
return storage.UserRead{}, domain.ErrNotFound
}

return usr[0], nil
}

// This is for listing.
// qry := []bson.M{
// {
// "$lookup": bson.M{
// "from": "logs",
// "let": bson.M{
// "uuid": "$uuid",
// },
// "pipeline": []bson.M{
// {
// "$match": bson.M{
// "$expr": bson.M{
// "$eq": []interface{}{
// "$user_uuid",
// "$$uuid",
// },
// },
// },
// },
// {
// "$sort": bson.M{
// "created_at": -1,
// },
// },
// },
// "as": "logs",
// },
// },
// }

HTTP Router


Models


package user

import "time"

type Response struct {
ID string `json:"id"`
UUID string `json:"uuid"`
Name string `json:"name"`
Logs []Log `json:"logs"`
}

type Log struct {
ID string `json:"id"`
UUID string `json:"uuid"`
Action string `json:"action"`
CreatedAt time.Time `json:"created_at"`
}

Controller


As you can see this file is doing a lot and there isn't really a proper request validation. You should refactor it as per your needs.


package user

import (
"encoding/json"
"net/http"

"github.com/you/mongo/internal/pkg/domain"
"github.com/you/mongo/internal/pkg/storage"
"github.com/julienschmidt/httprouter"
)

type Controller struct {
Storage storage.UserStorer
}

// GET /api/v1/users/:uuid
func (c Controller) Find(w http.ResponseWriter, r *http.Request) {
id := httprouter.ParamsFromContext(r.Context()).ByName("uuid")

usr, err := c.Storage.Find(r.Context(), id)
if err != nil {
switch err {
case domain.ErrNotFound:
w.WriteHeader(http.StatusNotFound)
default:
w.WriteHeader(http.StatusInternalServerError)
}
return
}

res := Response{
ID: usr.ID,
UUID: usr.UUID,
Name: usr.Name,
Logs: make([]Log, len(usr.Logs)),
}
for i, log := range usr.Logs {
res.Logs[i].ID = log.ID
res.Logs[i].UUID = log.UUID
res.Logs[i].Action = log.Action
res.Logs[i].CreatedAt = log.CreatedAt
}

body, err := json.Marshal(res)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}

w.Header().Set("Content-Type", "application/json; charset=utf-8")
_, _ = w.Write(body)
}

Test


curl --request GET 'http://localhost:3000/api/v1/users/40507244-d612-4fa4-b93f-c9c692f3b0fc'

{
"id": "604fb496d42b77e01e7f03e2",
"uuid": "40507244-d612-4fa4-b93f-c9c692f3b0fc",
"name": "Robert",
"logs": [
{
"id": "604fb7f2d42b77e01e7f03e5",
"uuid": "dc10a79b-1b47-4b13-944e-9c9bb31fdb8c",
"action": "logout",
"created_at": "2021-03-15T19:39:31.252Z"
},
{
"id": "604fb7f2d42b77e01e7f03e4",
"uuid": "318277c2-c4a8-4c3a-8dc9-dc170d2cf6fc",
"action": "login",
"created_at": "2021-03-15T19:39:30.252Z"
}
]
}