15/03/2021 - GO, MONGODB
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.
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"})
[
{
"_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"
}
]
[
{
"_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"
}
]
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"`
}
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",
// },
// },
// }
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"`
}
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)
}
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"
}
]
}