Writing an ODM with Generics
I prefer MongoDB over SQL. I like the idea of storing JSON documents in a table (known as collection in MongoDB). I like querying my database with a mutable object. JSON objects could also hold arrays and objects, this made it simple to store and access saved data. The concept was easy to grasp and implement, especially as a beginner back in 2014. The open source community developed libraries to simplify writing MongoDB applications. Amongst those libraries is Mongoose. Mongoose is a NodeJS ODM library. One of my favorite features of Mongoose is the ability to “ enforce a specific schema of desired object at the application layer.” This is a result of MongoDB’s schema-less design. It removed the need to update table schemas to store new data. Thus, I found it ideal to move table schema design to the application layer. When I began to write Go, it was only natural to ask “Where Mongoose?”
The DB Project
Prior to the Mongo Go Models project (https://github.com/Kamva/mgm), the Go MongoDB ODM library market was open season. I tried to take a shot at recreating the magic Mongoose brought to NodeJS. The concept was simple, write a function to read a type’s name and integrate the mgo.V2 driver (this was a while back). As for schema enforcement, Go is already statically-typed. So, step 1, reading a type’s name. I used the reflect package to extract the name. Reflect is the package one would use to inspect objects at runtime. However, reflect presents its set of disadvantages. It can mask possible type errors during compilation and requires more CPU time to accurately detect types. Here is a function reading a type’s name and returning a Mongo collection with said name:
As the code shows above, an mgo collection is returned. This reduced the need to re-write functions for the package, such as basic CRUD operations. This code was written in 2017, Go has evolved since. How would I rewrite this today?
Generics
Generics made its appearance in Go as of v1.18. Since we take risks on this blog, here is an approximation of how the code above will look like with Generics :
The function illustrates how the name of the type passed is determined. The new function no longer requires an empty object to be passed to determine the collection name. This implementation also reduces runtime errors as well. The initial mgo package required users to specify a pointer to return data to. This can result in type errors, because the pointer is referenced as an interface in mgo.V2’s implementation, resulting in no type enforcement on the pointer passed. Although it won’t cause a panic, the wrong data can be returned. Here is a comparison of the Generic and non-Generic implementations :
As you can observe, the first implementation requires me to type results
at least three times. The latter once. The second implementation is direct to the point. It is easy to reason about. The generic function will abstract keeping the type consistent during the request.
Final thoughts
I was not a fan of Generics at first. I was stuck in a nostalgic bubble, refusing to accept the change. It was after writing and compiling code using Generics that I found its utility. The code explained above is a perfect example of how Generics further simplifies writing code with Go.
Sources :