DEV Community

Robin
Robin

Posted on

MONGOREPO Abstraction Using Generic Constructor in Go

CRUD to resource

Typically we have this kind of CRUD endpoints to a resource:

'GET    /{resources}'       # Get a list of resource
'GET    /{resources}/{id}'  # Get one resource based on its ID
'POST   /{resource}'        # Create a new resource
'PUT    /{resource}/{id}'   # Update an existing resource based on its ID
'DELETE /{resource/{id}'    # Flag an existing resource as virtually deleted based on its ID

Which then brings us to the next point

Repository Pattern

Imagine a scenario where we have database object named Person

type Person struct {
    ID   primitive.ObjecID `bson:"_id,omitempty"`
    Name string            `bson:"name"`
}

With Repository Pattern typically we'll define a repository struct which mimics our CRUD operations:

type PersonRepo struct {}

func(r *PersonRepo) Get(ctx context.Context) ([]*Person, error) {}
func(r *PersonRepo) GetOne(ctx context.Context, id string) (*Person, error) {}
func(r *PersonRepo) Create(ctx context.Context, p *Person) error {}
func(r *PersonRepo) Update(ctx context.Context, p *Person) error {}
func(r *PersonRepo) Delete(ctx context.Context, id string) error {}

Abstraction, or the lack thereof

Imagine a scenario where we have more (let's say N amount) database object.
Each of them have similar CRUD signature and need their own repository.

In C# or Java who have generic, we can usually code something like this:

public class Repo<T> {
    public T Get() {}
    public T GetOne(string id) {}
    public void Create(T obj) {}
    public void Update(T obj) {}
    public void Delete(string id) {}
}

Where Repo<T> can be instantiated at runtime (or compile time) for diferrent type of dbo:

var personRepo = new Repo<Person>();
var enemyRepo = new Repo<Enemy>();
// etc, yada-yada, whatever

But here in Golang:

  • We don't have generic
  • Not OOP

We (read: software engineer/developer/programmer/coder drones/highly trained monkeys/whatever) often need to type type {Resource}Repo struct{...} N amount of times.

One solution is to learn meta programming but that's not the topic I'll touch today...


Now how do we abstract Repo<T> in Go?

First, we define the MongoRepo struct along with its factory

package mongorepo

// MongoRepo is repository that connects to MongoDB
// one instance of MongoRepo is responsible for one type of collection & data type
type MongoRepo struct {
    collection  *mongo.Collection
    constructor func() interface{}
}

// New creates a new instance of MongoRepo
func New(coll *mongo.Collection, cons func() interface{}) *MongoRepo {
    return &MongoRepo{
        collection: coll,
        constructor: cons,
    }
}

Here MongoRepo have 2 fields:

  • collection: is the mongodb collection which the MongoRepo have access to
  • constructor: is the constructor/factory of object which we want to abstract

So, think of the constructor as generic type T.
But instead of storing the type information, we are storing the function on how to create a new object T.

To see how constructor works, we move on to the CRUD implementation

var (
    virtualDelete = bson.M{"$set": bson.M{"deleted": true}}
)

// Get a list of resource
// The function is simply getting all entries in r.collection for the sake of example simplicity
func (r *MongoRepo) Get(ctx context.Context) ([]interface{}, error) {
    cur, err := r.collection.Find(ctx, bson.M{})
    if err != nil {
        return nil, err
    }

    var result []interface{}
    defer cur.Close(ctx)
    for cur.Next(ctx) {
        entry := r.constructor() // call to constructor
        if err = cur.Decode(entry); err != nil {
            return nil, err
        }

        result = append(result, entry)
    }

    return result, nil
}


// GetOne resource based on its ID
func (r *MongoRepo) GetOne(ctx context.Context, id string) (interface{}, error) {
    _id, _ := primitive.ObjectIDFromHex(id)
    res := r.collection.FindOne(ctx, bson.M{"_id": _id})
    dbo := r.constructor()
    err := res.Decode(dbo)
    return dbo, err
}

// Create a new resource
func (r *MongoRepo) Create(ctx context.Context, obj interface{}) error {
    _, err := r.collection.InsertOne(ctx, obj)
    if err != nil {
        return err
    }

    return nil
}

// Update a resource
func (r *MongoRepo) Update(ctx context.Context, id string, obj interface{}) error {
    _id, _ := primitive.ObjectIDFromHex(id)
    _, err := r.collection.UpdateOne(ctx, bson.M{"_id": _id}, obj)
    if err != nil {
        return err
    }

    return nil
}

// Delete a resource, virtually by marking it as {"deleted": true}
func (r *MongoRepo) Delete(ctx context.Context, id string) error {
    _id, _ := primitive.ObjectIDFromHex(id)
    _, err := r.collection.UpdateOne(ctx, bson.M{"_id": _id}, virtualDelete)
    if err != nil {
        return err
    }

    return nil
}

Notice @line#9 of Get method, we have:

entry := r.constructor()

Or @line#3 of GetOne method:

dbo := r.constructor()

This is where we trick the abstraction of T in Go, which I termed as Generic Constructor (CMIIW), just because Go doesn't have generic.

So what's the point of all of this, bro? Why all the shenanigan?

So:

  • We can freely pass any generic constructor function
  • To not having to type / make {Resource}Repo N number of times

e.g:

type Person struct{}

type Enemy struct{}

// Initialize mongo connection
ctx := context.Background()
conn := os.Getenv("MONGO_CONN")
mongoopt := options.Client().ApplyURI(conn)
mongocl, _ := mongo.Connect(ctx, mongoopt)
mongodb := mongocl.Database("dbname")

// personRepo, points to 'person' collection
personRepo := mongorepo.New(
    mongodb.Collection("person"),
    func() interface{} {
        return &Person{}
    }))

// enemyRepo, points to 'enemy' collection
enemyRepo := mongorepo.New(
    mongodb.Collection("enemy"),
    func() interface{} {
        return &Enemy{}
    }))

Compare it to language with generic:

var personRepo = new Repo<Person>();
var enemyRepo = new Repo<Enemy>();

I think it's already quite similar. ✌️

Conclusion

+ If we have lots of database object with similar CRUD operation, I think this can save us lots of time.

- We now rely on interface{} which defeats the purpose of strongly typed language.

Ironic, how lack of generic makes code even more unsafe when we try to do abstraction around it...

Top comments (0)