Runtime Generated, Typesafe, and Declarative - Pick Any Three
Jon Bodner
Liveblog by Matt King
About the speaker
Jon Bodner is part of the Technology Fellows Program at Capital One, currently working on a fork of the LGTM project with the hopes of open sourcing it soon. His team is helping to transform Capital One through introduction and integration of new technologies, working to shorten release cycles, and generally pushing forward an "open source first" culture.
He is a software engineer, lead developer, and architect. He's worked in just about every corner of the software industry including on-line commerce, education, finance, government, healthcare, and internet infrastructure.
His favorite member of One Direction is the auto-tune box.
Overview
Go code is sometimes called repetitive. By adopting techniques like tags, reflection, and runtime function generation, you can focus on the algorithm and not the boilerplate. Jon shows off Proteus, which uses these ideas to implement a runtime generated, type-safe, SQL-injection-proof DAO layer.
We brag about language features. Take these examples:
"My language has immutability"
"My language has generics"
"My language has unreadible syntax"
I'm not sure why we brag about features because that last example is unreadable. Could you imagine if you were making a change to a critical path and every change was packed with complex features? To me that would be awful.
One of the best features of Go is it's lack of features.
To start off let's define the requirements so we can implement a runtime generated, type-safe, SQL-injection-proof DAO layer similar to the Java implementation.
Requirements
Specify the queries inline with the code.
Generate the code to implement the queries.
Ensure type safety.
Prevent SQL injection attacks.
Make sure performance is reasonable.
Most importantly, it needs to feel like Go. We want a library not a framework. It should work with standard sql libraries.
Proteus
This talk is based on Proteus is a simple tool for generating an application's data access layer.
Under the Hood
Let's start with a simple init and main function that is responsible for setting up the PostgresDB
Before we dive into writing a full implementation of DoPersonStuff it's important to understand the reflect package.
Reflection
We can use the reflect package to get the type of a variable, the kind of a variable, the type of a pointer, and create a type token.
In addition to getting the type you can use reflect package to find the values.
First create a reflect.Value and in order to modify it we need to make it a pointer.
Reading the value of a pointer is straightforward as well:
To modify the value we will use the pointer created earlier:
varVal.Elem().Set(varVal2)
If we wanted to create a new pointer value so we can modify it we will use reflect.New
newVarVal := reflect.New(varVal2.Type())
Now that we've covered the basics lets dive into the first problem.
How do you store the metadata?
We can use struct tags. (Note: In case you missed it, Fatih Arslan gave an amazing talk on writing a go tool to parse and modify struct tags. The tool, gomodifytags is very popular.)
We use the struct tags to get the fields and the values so that we can store our queries.
How do you turn a SQL string into runnable code?
To turn a SQL string into runnable code we generate functions at runtime using the reflect package. For example:
So let's say we have these two functions that take advantage of our memoization:
This has lots of overhead if you were to use these generated functions, however in the database implementation this is okay.
Struct fields can be functions
We use the struct to hold our generated queries. Let's start to put the above concepts together by creating a struct with fields that are functions.
When you use these, it looks like standard Go code.
Running the implementation we can see our stubbed DAO implementation for create, get, update, and delete.
This is good because it shows that the basic concept works. So now we need to get it to run database queries.
Next we want to integrate this with Go SQL libraries.
sql.DB is a database handle representing a pool of zero or more underlying connections. The sql package automatically creates and frees connections automatically; it also maintains a free pool of idle connections.
sql.TX is an in-progress database transaction. A transaction must end with a call to Commit or Rollback.
Queries can be broken into two types:
Read
Add, update, or delete
We need to write a querier that returns rows from the datastore and an executor that runs queries that modify the datastore.
A problem that arises is returning sql.Rows != returning Rows where Rows is defined as
The solution is to create an adapter struct and a factory function.
Now we can improve our stubbed implementation of makeImplementation to use the executor and querier.
Two new functions were introduced depending on which case statement is used:
We also define a new interface Wrapper that is a wrapper for our querier and executor interfaces.
Going back to our stubbed client code, we modify PersonDao struct to include SQL commands.
When you run the code you'll see that we are actually talking to the database, there are errors because this is not valid SQL syntax. Again, this confirms we are talking to the database and getting errors back from the database.
Next, associate the parameters to query placeholders.
Function: Update func(e Executor, id int, name string, age int)
Query: UPDATE PERSON SET name = :name:, age=:age: where id=:id:
Since Go doesn't save parameters names we cannot use reflect to get them at runtime. So we use a second struct tag key/value pair to map the names to their position prop:"id,name,age". It's then necessary to define a ParamAdapter because different bases use different parameter notation.
From Struct Tags to SQL
Struct Tag proq: UPDATE PERSON SET name = :name:, age=:age: where id=:id:
Struct Tag prop: id,name,age
Convert the prop to map[string]int
Struct Tag proq: UPDATE PERSON SET name = :name:, age=:age: where id=:id:
nameOrderMap: {“id”:1, “name”:2, “age”:3}
Convert proq and nameordermap to query and paramOrder
SQL Query: UPDATE PERSON SET name = $1, age=$2 where id=$3
The executor type will be an int64 and an error. The executor will first look at the sql.Result and error returned. If there is an error we handle it. Otherwise, we get the number of rows modified and once again handle the error if necessary. Lastly, we return the number of rows and a nil error.
Our client code is modified as:
However, returning the count of the rows isn't really what we are looking for. We want to return back data from our queries. To do this we need to implement our queries.
Return back values
To return back real data we need to define a struct with struct tags to map fields to query results.
The return type will be a pointer to a struct and an error
Let's update our makeQuerierImplementation to return a value back from the database:
Build a mapper:
Populate the return value:
Lastly, map the row:
Now we are returning real values back from our datastore, but this is only for single rows. In the real world, we typically want more than just one value to be returned.
Returning multiple values
To do this we use a slice of a struct and an error for the return type. The function looks like:
We follow the same implementation as before, but we do this for multiple rows instead of a single row.
Go can be used to create declaration-driven code. By combining struct tags, function generation, reflection, and templating we can increase productivity and provide the functionality in other languages. All while keeping type safety and performance. And most importantly we can write code that feels like Go.
Proteus is not limited to SQL and adding support for NoSQL is simple, just implement the necessary adapter functions and Proteus will take care of the rest.
Get Cody, the AI coding assistant
Cody makes it easy to write, fix, and maintain code.