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)