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 constructorfunction - To not having to type / make
{Resource}RepoNnumber 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)