Database Storage in Go with Structable

Aug 15 2014

I'm not a huge fan of ORMs. Don't get me wrong, I'm not about to start a campaign against them. I know that many people find them to be a great abstraction layer on their database. But I usually feel more productive with my query writing when I can just write SQL.

Okay, not always. Writing plain old CRUD (Create, Read, Update, Delete) queries is a mindless chore equivalent to scrubbing the toilet.

So I wrote a little library called structable that takes care of the boilerplate CRUD for me, but makes it nice and easy to build my own queries when it's time to move beyond simple operations.

Note: Structable 3.0 is new, and breaks backward compatibility with Structable 1.0. This article covers 3.0.

Struct-Table Mapping

At the core of Structable is a simple mapper that reads an annotated struct and maps it to a table in a relational database. I call it "struct-table mapping" (thus the name StrucTable).

With a few annotations, I can make a simple map:

type User struct {
    Id int `stbl:"id,PRIMARY_KEY,SERIAL"`
    Name string `stbl:"name"`
    Email string `stbl:"email"`
  NotMapped string // This will not be mapped to a DB record at all.

The stbl annotation tells Structable about a field on the struct. For example, the Name string is mapped to the column name via the stbl annotation. Structable assumes that you designed your database in such a way that you will be able to store a string in that column.

The Id field has some additional stuff in the stbl tag:

Id int `stbl:"id,PRIMARY_KEY,SERIAL"`

The PRIMARY_KEY keyword tells structable that the Id field is the primary key for the table. The SERIAL (or AUTOINCREMENT if you prefer) tells Structable that this field's value can be assumed to be managed by the database.

Record vs. Recorder

The struct above is a record. It contains data, but it doesn't know how to do anything with that data. Structable provides a recorder to do something with the data.

In it's simplest form, using a recorder is a matter of attaching a record struct to a recorder:

db = //... we'll get to this in a minute.
dbFlavor = "postgres"
user := new(User)
userRecorder = structable.New(db, dbFlavor).Bind("users_table", user)

A recorder needs a few things. First, it needs a handle to the database. I'll come back to that, but in a nutshell, Structable uses my personal favorite database tool: squirrel.

With a database connection in place, we can Bind() (attach) a specific table and struct to the recorder. Now we've essentially said that a User struct is stored in the users_table, and is managed by our new userRecorder.

From here we can use the recorder to insert, update, load, test, or delete a record:

user.Name = "Matt"

In theory, this is similar to the design pattern known as DAO. But Structable's most elegant expression is found using the Active Record pattern.

Using Structable for Active Records

An Active Record is a record object that is its own recorder. We can do that easily with Structable with just two small additions to our User struct:

type User struct {
    builder squirrel.StatementBuilderType

    Id int `stbl:"id,PRIMARY_KEY,SERIAL"`
    Name string `stbl:"name"`
    Email string `stbl:"email"`

// NewUser creates a new Structable wrapper for a user.
// Of particular importance, watch how we intialize the Recorder.
func NewUser(db squirrel.DBProxyBeginner, dbFlavor string) *User {
    u := new(User)
    u.Recorder = structable.New(db, dbFlavor).Bind("user_table", u)
    return u

The User struct now has an anonymous structable.Recorder. In Go, this allows us to "inherit" the Recorder's methods. For the sake of extensibility, I also always store a squirrel.StatementBuilderType on my records so that I can later add new database methods.

Now let's look at the NewUser method. Instead of creating an external recorder, it initializes its internal anonymous Recorder. Now I can use instances of my User struct like this:

user := NewUser(db, dbFlavor)
user.Name = "Matt"

anotherUser := NewUser(db, dbFlavor)
anotherUser.Id = 123
anotherUser.Load() // This loads because Id is a PRIMARY_KEY field.

A Quick Word about the Database Connection

You can take a look at Squirrel to get a better feeling for how to manage connections. It's intuitive and very powerful. But here's how I set that up for the examples above:

func main() {

    // Boilerplate DB setup.
    // First, we need to know the database driver.
    driver := "postgres"
    // Second, we need a database connection.
    con, _ := sql.Open(driver, "dbname=structable_test sslmode=disable")
    // Third, we wrap in a prepared statement cache for better performance.
    cache := squirrel.NewStmtCacheProxy(con)

    // Create an empty new user and give it some properties.
    user := NewUser(cache, driver)
    user.Name = "Matt"
    user.Email = ""

Essentially, I create a new Postgres database connection (though it works fine with MySQL and probably other DB flavors, too). Then I wrap it in Squirrel's statement cache so that I can transparently re-use prepared statements. From there, I'm off to the races.