Generating the repository pattern in Go

Posted on Dec 11, 2020

repository pattern

Contents

Introduction

The repository pattern is a way to organize and abstract the data-access layer code. In Go, we do this by definig an interface that describes how we want to interact with it. Below is a typical example of a repository interface.

type ProductRepository interface {
    Create(Product) error
    Get(productID uint) (*Product, error)
    Delete(productID uint) error
    List(limit, offset uint) ([]Product, error)
}

This interface defines a basic CRUD (Create, Read, Update ,Delete) methods.

The downside

The repository pattern is convenient because it enables us to interact with the interface (e.g. creating a product) without concerning ourselves with the implementation details. For example, we can write implementations that uses different backends like in-memory, file, postgres, mysql, boltdb etc. and client code will still be the same.

The only downside (if you’re lazy, like me) is that the process of writing the implementations is often boring, repetitive, tedious and sometimes error-prone. Depending on your implementation, it’s often difficult to make a change (e.g. adding a new field) because you end-up changing a lot of things in different places.

Surely, if there’s only a way, I will very much want automate this task.

In search for solutions

In hope to find a solution, we went to search and the best that we can find are SQLBoiler and ent. These projects bothe uses code generation, which means we almost don’t need to write code at all. They are almost exact match for our use-case. But, we wanted more control over the generated code and to easily integrate it with our existing codebase, with minimal to no change.

Since no solution exactly matches our use-case, we’ve decided to experiment on a small library.

Enter nero!

After a few weeks of experimentation, we’ve produced a small library called nero that enables us to do what we want: to generate the exact same code that we’re writing manually, but better. This library uses-code generation similar to the libraries above.

How to use this library?

Given an (existing) model called Product.

type Product struct {
	ID        int64
	Name      string
	CreatedAt *time.Time
	UpdatedAt *time.Time
}

We need to define the schema.

import (
    "github.com/sf9v/nero"
)
...

func (p *Product) Schema() *nero.Schema {
	return &nero.Schema{
        // Package name of the generated code
        Pkg:        "repository",
        // Collection/table name (for relational databases)
        Collection: "products",
        // List of columns
		Columns: []*nero.Column{
			nero.NewColumn("id", p.ID).StructField("ID").Ident().Auto(),
			nero.NewColumn("name", p.Name),
			nero.NewColumn("created_at", p.CreatedAt).Auto(),
			nero.NewColumn("updated_at", p.UpdatedAt),
		},
	}
}

From above, we defined the package name of the generated code, the collection or table name (for relational databases) and the list of columns. We didn’t need to define the column types since the library already infers the type. These are the only information that the library needs to start generating.

The library didn’t (yet) have a CLI, so we have to import it to create our own generator.

import (
    // import the gen package
    "github.com/sf9v/nero/gen"

    "github.com/sf9v/nero-example/model"
)
...

// 1. Generate the files
files, err := gen.Generate(new(model.Product))
...

// 2. Create the destination directory (i.e. nero-example/repository)
basePath := "repository"
err = os.MkdirAll(basePath, os.ModePerm)
...

// 3. Finally, render the files
for _, file := range files {
    err = file.Render(basePath)
    ...
}

This CLI tool will generate the repository code in the given directry (i.e. nero-example/repository).

How to use the generated code?

The library is heavily inspired by ent so the resulting API almost looks similar. To start using it, we have to import the generated package and initialize a new repository.

import (
    "database/sql"

    // postgres driver
    _ "github.com/lib/pq"

    "github.com/sf9v/nero-example/repository"
)

// open a postgres db connection
db, err := sql.Open("postgres", dsn)
...

// initialize a repository using the db connection
repo := repository.NewPostgresRepository(db)
...

Below is the demonstration of basic the API.

  • Create
productID, err := repo.Create(ctx, repository.NewCreator().
    Name("Product 1"))
...
  • Query
product, err := repo.QueryOne(ctx, repository.NewQueryer().
    Where(repository.IDEq(productID)))
...
  • Update
_, err = repo.Update(ctx, repository.NewUpdater().
    Name("Updated Product 1").UpdatedAt(&now).
    Where(repository.IDEq(productID)))
  • Delete
_, err = repo.Delete(ctx, repository.NewDeleter().
    Where(repository.IDEq(productID)))

The generated code provides a convenient API to perform the basic CRUD operations. The complete example can be found in this repository.

Conclusion

The repository pattern is a way of organizing the data-access layer code which allows us to interact with the interface without concerning ourselves with the implementation details. Writing the repository layer manually is often tedious and error-prone. To ease this process, we offered a library called nero to automate this boring task. This library provides a convenient API for interacting with the database.

We hope you would find it enjoyable and useful as much as we did!

PS: If you have any suggestions and ideas on how to improve this library, please feel free to open an issue!

References