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)