gokv: Go database interface
A TDD-approved persistence-layer abstraction
SQL is hard. Dealing with persistence in general is hard and error-prone. Sometimes the functionality one needs is very little over CRUD: in such cases, it may be interesting to have a library deal with the database-specific instructions.
ORMs are here for simplifying the process of wiring our code to a persistence layer, but most of the time they come with a fundamental flaw: they propose themselves as a generic implementation that will work for this and that database.
Instead of providing a generic implementation,
gokv
tries a different approach: it aims at defining a generic interface.
Defining a Store interface
The gokv/store.Store
interface defines a basic set of high level methods, starting from the basic Set, Get, Update, Delete.
Not every store implementation will, nor should, implement every defined method. A Redis store will easily implement SetWithTimeout, but it might not be a goal for it to implement FindWord (still in the works, not pictured above). The consumer code will define a subset of this interface only featuring its needed methods.
In the PostgreSQL implementation, a Store represents a database table. It is instantiated with a *sql.DB
and a table name. In Redis, a Store might represent a namespaced group of entries, possibly spread across different types.
The Add
behaviour is defined as: a method that persists a new key-value pair, erroring if the key is already present.
The interface tells nothing about how the implementation will comply to this.
Moreover, the interface tells nothing about the tradeoffs. The same behaviour might be implemented very efficiently in PostgreSQL, and less efficiently in Redis… or vice-versa. The implementation’s documentation is expected to provide insight. The bright side is: benchmarking the different stores with your specific use case might be a matter of changing a couple lines of code.
JSON as an interchange format
The gokv
interface is based on JSON marshalability. In order to avoid reflection and type assertion, the methods for persisting new data receive json.Marshaler
, and the methods for fetching data accept json.Unmarshaler
.
The methods for fetching multiple results leverage the Collection
interface, defined as:
type Collection interface {
New() json.Unmarshaler
}
In this slice-based implementation of a Book
collection, New appends a new Book to the slice and returns a pointer to it as a json.Unmarshaler
:
The Store
implementation, while looping through the results from the DB, will unmarshal them into the Unmarshaler returned by New. Here is how the Collection is used in the gokv/postgres
implementation of GetAll
:
Consumer code is easy to test
Test-driven developers will find another advantage to gokv
: the responsibility of dealing with the actual (possibly external) persistence is totally on the Store implementation. This means that on the consumer side, we can test every single line of code without any painful mocking. An example of 100% test coverage:
Looks cool! How can I contribute?
The project is currently little more than an experiment.
Implementations of the gokv
interface exist for in-memory storage and PostgreSQL. A Redis implementation is in the works and I’m planning on a Redis-stream store. None of them is production-ready. The gokv/store
interface itself lacks many important method definitions.
If you want to get involved, get in touch! Or just file a pull request. My main focus is now on defining sane interfaces for searching, but you might have another idea?