tablestruct

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 20, 2014 License: Apache-2.0 Imports: 11 Imported by: 0

README

tablestruct

Build Status

Maps Go structs to database tables, struct fields to columns.

It provides common functionality for persisting structs to and retrieving structs from the database, including but not limited to:

  • get by ID/primary key
  • insert
  • insert many (in a transaction)
  • update
  • delete
  • find by WHERE clause

tablestruct uses code generation to produce mappers.

Current release: 0.1.0 (July 19, 2014)

Tentative roadmap

  • 0.2: Inspect db to generate initial mapping metadata file.
  • 0.3: Generate structs from metadata.
  • 0.4: Generate db schema from metadata.
  • 0.5: Foreign keys/relationships between structs.

tablestruct documentation

Installation

Requires Go 1.2.

go get github.com/paulsmith/tablestruct/cmd/tablestruct

Quickstart

In the following, let's assume you want to map a Go struct type named Person, in a file named person.go already on disk, to a database table named people.

$ go get github.com/paulsmith/tablestruct/cmd/tablestruct
$ tablestruct support > mapper_support.go # one-time support code generation step
$ tablestruct struct Person < person.go > person_mapper.metadata
$ $EDITOR person_mapper.metadata # tweak to adjust for differences between struct and table, column and field names
$ tablestruct gen < person_mapper.metadata > person_mapper.go

You should also go get your database's driver if you haven't already.

$ go get github.com/lib/pq # for PostgreSQL, only supported db in tablestruct so far ...

Motivation

If you work with Go and databases regularly, and you have Go structs which correspond to or are persisted by database tables, you might find yourself writing a lot of the same code over and over, tedious bits that map struct fields to columns, templatize SQL, perform the equivalent of inserts and updates but on structs instead of rows, and so forth.

You can write much of this code once and rely on reflection, but it has its costs, in loss of compile-time type safety and increased latency at runtime. Reflection-based mappers tend to have complicated implementations as well.

tablestruct is meant to ease the workaday usage and maintenance of structs that map to tables. It is a goal to have a simple implementation, the intermediate steps of which are easily inspectable.

Ambition

tablestruct has low ambition. It does not intend to do that much, for example, it will never be a complete ORM, nor will it ever replace writing SQL inside Go code. It merely wants to be useful for a set of common, low-level data persistence operations.

Theory of operation

tablestruct has three separate stages of operation, but the first one only needs to be run once initially, and the second is optional or otherwise only needs to be run once. The third stage is the code generation step that is the meat of tablestruct.

tablestruct produces mappers, which are structs that handle the back and forth between the main structs in your Go application and the database. Specifically, they handle the mapping between structs and tables, and fields and columns, in SQL for operations like inserts and updates, and in Go code for initializing new structs from query result sets. The mappers are generated as separate files of Go code that reside in the same package as the rest of your code. You can be override this behavior to target any package.

The way to use tablestruct during the normal course of development is to create your main structs and your database tables, run tablestruct as a command to produce the mapper files, and then go run or go build.

You should run tablestruct's code gen stage after any time you modify your main structs or the database tables. This is best handled by making the code generation step a rule in a Makefile, discussed below. Generated Go code should not be checked in to version control.

Current limitations

tablestruct currently only supports PostgreSQL. This is merely due to it being the database it was initially developed against, and is not a limitation of its design. To support other databases, a mild refactoring would be needed that allowed for the code generation template to be parameterized.

Mapping metadata

The information that enables tablestruct to generate the correct mapper code is the mapping metadata. The metadata can be a file that you edit by hand, or it can be piped in from one stage of tablestruct to another with no intermediate files produced, if you are happy with the translation between field names and column names, as well as struct type names and table names.

Mapping metadata is encoded as JSON. You can pass it in as stdin to the code gen stage of tablestruct.

If you are just starting out, you can generate initial metadata from your existing Go structs. Run tablestruct metadata, passing the name of the struct type you want to map as an argument, and pipe the .go file containing the struct as stdin:

$ tablestruct metadata Person < person.go > person.metadata

tablestruct will try to convert exported Go struct type and field names into database table and column names (broadly, going from CamelCase to snake_case). However, you may have less simplistic translations, for example Go struct type Person and table name people, so tablestruct doesn't attempt to try to outsmart you. It is intended that you will edit the metadata file by hand to get the exact name translations right.

Mapper API

Assume T is the name of your main Go struct type that is mapped.

func NewTMapper(db *sql.DB) *TMapper
func (m *TMapper) Insert(t *T) error
func (m *TMapper) InsertMany(t []*T) error
func (m *TMapper) Update(t *T) error
func (m *TMapper) Get(id int64) (*T, error)
func (m *TMapper) All() ([]*T, error)
func (m *TMapper) FindWhere(whereClause string) ([]*T, error)
func (m *TMapper) Delete(t *T) error

Example

Say you've got a struct type that's your model in your application:

// event.go

package mypkg

import "database/sql"

type Event struct {
    ID int64
    Title string
    Description sql.NullString
    UnixTimestamp int64
    Severity float64
    Resolved bool
}

And you have a corresponding table in a database where structs are persisted as rows:

create table event (
    id bigserial primary key,
    title varchar not null,
    description varchar,
    unix_timestamp bigint not null default now,
    severity double precision not null default 0.0,
    check (severity >= 0.0 and severity <= 1.0),
    resolved bool not null default 'f'
);

Generate the mapper:

$ tablestruct -package mypkg support > mapper_support.go
$ tablestruct -package mypkg metadata Event < event.go | tablestruct -pkg mypkg gen > event_mapper.go

Look at the mapper:

// event_mapper.go

// generated mechanically by tablestruct, do not edit!!
package mypkg

import (
	"database/sql"
	"log"
)

type EventMapper struct {
	db   *sql.DB
	sql  map[string]string
	stmt map[string]*sql.Stmt
}

func NewEventMapper(db *sql.DB) *EventMapper {
	m := &EventMapper{
		db:   db,
		sql:  make(map[string]string),
		stmt: make(map[string]*sql.Stmt),
	}
	m.prepareStatements()
	return m
}

// ... lots of code elided ...

Write some application code to use it:

// main.go

package main

import (
    "database/sql"
    "fmt"
    "time"

    "github.com/lib/pq"
    "path/to/mypkg"
)

func main() {
    db, _ := sql.Open("postgres", "")

    mapper := mypkg.NewEventMapper(db)

    event := &mypkg.Event{
        Title: "my severe event",
        UnixTimestamp: time.Now().Unix(),
        Severity: 1.0,
    }

    mapper.Insert(event)

    event.Resolved = true
    event.Description = sql.NullString{String: "investigated and resolved", Valid: true}
    mapper.Update(event)

    fmt.Println(event.ID)
    // Output: 1

    events := []*mypkg.Event{
        {
            Title: "my not severe event",
            UnixTimestamp: time.Now().Unix(),
            Severity: 0.0,
        },
        {
            Title: "my slightly severe event",
            UnixTimestamp: time.Now().Unix(),
            Severity: 0.3,
        },
        {
            Title: "my moderately severe event",
            UnixTimestamp: time.Now().Unix(),
            Severity: 0.5,
        },
    }

    mapper.InsertMany(events)

    events, _ = mapper.FindWhere("severity < 0.5")
    fmt.Println(len(events))
    // Output: 2
}

Tips & tricks

Make

The best way to use tablestruct is via make(1) and Makefiles.

Set up a rule where the target is the generated mapper file and the prerequisite is the Go source file containing the struct type:

PACKAGE=main

event_mapper: event.go
    tablestruct -package=$(PACKAGE) metadata Event < $< | \
    tablestruct -package=$(PACKAGE) gen > $@

You'll need the mapper support file, so good to add that:

.PHONY: mapper_support.go
mapper_support.go:
    tablestruct -package=$(PACKAGE) support > $@

If you have multiple struct files, it is better to make use of make's pattern rules. The only trick is to match the struct type name to the Go file it's in.

STRUCT_event=Event
STRUCT_person=Person

%_mapper.go: %.go
    tablestruct -package=$(PACKAGE) metadata $(STRUCT_$(basename $<)) < $< | \
    tablestruct -package=$(PACKAGE) gen > $@

Then gather up all the mapper files as a separate target:

struct_files := event.go person.go
mapper_files := $(patsubst %.go,%_mapper.go,$(struct_files))

all: $(mapper_files)

The final Makefile looks something like:

PACKAGE=main

STRUCT_event=Event
STRUCT_person=Person

struct_files := event.go person.go
mapper_files := $(patsubst %.go,%_mapper.go,$(struct_files))

all: $(mapper_files) mapper_support.go

%_mapper.go: %.go
    tablestruct -package=$(PACKAGE) metadata $(STRUCT_$(basename $<)) < $< | \
    tablestruct -package=$(PACKAGE) gen > $@

.PHONY: mapper_support.go
mapper_support.go:
    tablestruct -package=$(PACKAGE) support > $@

Then run make:

$ make PACKAGE=mypkg
tablestruct -package=mypkg metadata Event < event.go | \
	tablestruct -package=mypkg gen > event_mapper.go
tablestruct -package=mypkg metadata Person < person.go | \
	tablestruct -package=mypkg gen > person_mapper.go
tablestruct -package=mypkg support > mapper_support.go

Documentation

Overview

Package tablestruct maps Go structs to database tables, and provides functionality for common persistance operations and queries.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FieldToColumn

func FieldToColumn(field string) string

FieldToColumn converts a Go struct field name to a database table column name. It is mainly CamelCase -> snake_case, with some special cases, and is overridable.

func GenSupport

func GenSupport(w io.Writer, pkg string)

GenSupport generates the support Go code for all tablestruct mappers.

func StructToTable

func StructToTable(strct string) string

StructToTable converts a Go struct name to a database table name. It is mainly CamelCase -> snake_case, with some special cases, and is overridable.

Types

type Code

type Code struct {
	// contains filtered or unexported fields
}

Code generates Go code that maps database tables to structs.

func NewCode

func NewCode() *Code

NewCode creates a new code generator.

func (*Code) Gen

func (c *Code) Gen(mapper *Map, pkg string, out io.Writer)

Gen generates Go code for a set of table mappings.

type ColumnMap

type ColumnMap struct {
	Field      string `json:"field"`
	Column     string `json:"column"`
	Type       string `json:"type"`
	Null       bool   `json:"null"`
	PrimaryKey bool   `json:"pk"`
}

ColumnMap describes a mapping between a Go struct field and a database column.

type Map

type Map []TableMap

Map describes a mapping between database tables and Go structs.

func NewMap

func NewMap(in io.Reader) (*Map, error)

NewMap constructs a new mapping object.

func (*Map) Imports

func (m *Map) Imports() []importSpec

Imports generates list of import specs required by generated code.

type TableMap

type TableMap struct {
	Struct  string      `json:"struct"`
	Table   string      `json:"table"`
	Columns []ColumnMap `json:"columns"`
	// AutoPK is whether the table is set up to automatically generate new
	// values for the primary key column. `false` means the application must
	// supply them.
	AutoPK bool `json:"auto_pk"`
}

TableMap describes a mapping between a Go struct and database table.

func (TableMap) ColumnList

func (t TableMap) ColumnList() string

ColumnList produces SQL for the column expressions in a SELECT statement.

func (TableMap) Fields

func (t TableMap) Fields() []string

Fields returns the list of field names of the Go struct being mapped.

func (TableMap) InsertFields

func (t TableMap) InsertFields() []string

InsertFields returns a list of struct fields to be used as values in an insert statement.

func (TableMap) InsertList

func (t TableMap) InsertList() string

InsertList produces SQL for the placeholders in the value expression portion of an INSERT statement.

func (TableMap) PrimaryKey

func (t TableMap) PrimaryKey() *ColumnMap

PrimaryKey returns the column mapping for the primary key field/column.

func (TableMap) UpdateList

func (t TableMap) UpdateList() string

UpdateList produces SQL for the column-placeholder pairs in a UPDATE statement.

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL