structable

package module
v0.0.0-...-a1a302e Latest Latest
Warning

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

Go to latest
Published: Apr 7, 2017 License: MIT Imports: 4 Imported by: 10

README

Structable: Struct-Table Mapping for Go

Warning: This is the Structable 4 development branch. For a stable release, use version 3.1.0. Structable development happens very slowly.

This library provides basic struct-to-table mapping for Go.

It is based on the Squirrel library.

What It Does

Structable maps a struct (Record) to a database table via a structable.Recorder. It is intended to be used as a back-end tool for building systems like Active Record mappers.

It is designed to satisfy a CRUD-centered record management system, filling the following contract:

  type Recorder interface {
    Bind(string, Record) Recorder // link struct to table
    Interface() interface{}  // Get the struct that has been linked
    Insert() error // INSERT just one record
    Update() error // UPDATE just one record
    Delete() error // DELETE just one record
    Exists() (bool, error) // Check for just one record
    ExistsWhere(cond interface{}, args ...interface{}) (bool, error)
    Load() error  // SELECT just one record
    LoadWhere(cond interface{}, args ...interface{}) error // Alternate Load()
  }

Squirrel already provides the ability to perform more complicated operations.

How To Install It

The usual way...

$ glide get github.com/Masterminds/structable
$ # or...
$ go get github.com/Masterminds/structable

And import it via:

import "github.com/Masterminds/structable"

How To Use It

GoDoc

Structable works by mapping a struct to columns in a database.

To annotate a struct, you do something like this:

  type Stool struct {
    Id		 int	`stbl:"id, PRIMARY_KEY, AUTO_INCREMENT"`
    Legs	 int    `stbl:"number_of_legs"`
    Material string `stbl:"material"`
    Ignored  string // will not be stored. No tag.
  }

To manage instances of this struct, you do something like this:

  stool := new(Stool)
  stool.Material = "Wood"
  db := getDb() // Get a sql.Db. You're on  the hook to do this part.

  // Create a new structable.Recorder and tell it to
  // bind the given struct as a row in the given table.
  r := structable.New(db, "mysql").Bind("test_table", stool)

  // This will insert the stool into the test_table.
  err := r.Insert()

And of course you have Load(), Update(), Delete() and so on.

The target use case for Structable is to use it as a backend for an Active Record pattern. An example of this can be found in the structable_test.go file

Most of Structable focuses on individual objects, but there are helpers for listing objects:

// Get a list of things that have the same type as object.
stool := new(Stool)
items, err := structable.List(stool, offset, limit)

// Customize a list of things that have the same type as object.
fn = func(object structable.Describer, sql squirrel.SelectBuilder) (squirrel.SelectBuilder, error) {
  return sql.Limit(10), nil
}
items, err := structable.ListWhere(stool, fn)

For example, here is a function that uses ListWhere to get collection of definitions from a table described in a struct named Table:

func (s *SchemaInfo) Tables() ([]*Table, error) {

  // Bind a new recorder. We use an empty object just to get the field
  // data for that struct.
	t := &Table{}
	st := structable.New(s.Queryer, s.Driver).Bind(t.TableName(), t)

  // We want to return no more than 10 of these.
	fn := func(d structable.Describer, q squirrel.SelectBuilder) (squirrel.SelectBuilder, error) {
		return q.Limit(10), nil
	}

  // Fetch a list of Table structs.
	items, err := structable.ListWhere(st, fn)
	if err != nil {
		return []*Table{}, err
	}

  // Because we get back a []Recorder, we need to get the original data
  // back out. We have to manually convert it back to its real type.
	tables := make([]*Table, len(items))
	for i, item := range items {
		tables[i] = item.Interface().(*Table)
	}
	return tables, nil
}
Tested On
  • MySQL (5.5)
  • PostgreSQL (9.3, 9.4, 9.6)
  • SQLite 3

What It Does Not Do

It does not...

  • Create or manage schemas.
  • Guess or enforce table or column names. (You have to tell it how to map.)
  • Provide relational mapping.
  • Handle bulk operations (use Squirrel for that)

LICENSE

This software is licensed under an MIT-style license. See LICENSE.txt

Documentation

Overview

Structable is a struct-to-table mapper for databases.

Structable makes a loose distinction between a Record (a description of the data to be stored) and a Recorder (the thing that does the storing). A Record is a simple annotated struct that describes the properties of an object.

Structable provides the Recorder (an interface usually backed by a *DbRecorder). The Recorder is capable of doing the following:

  • Bind: Attach the Recorder to a Record
  • Load: Load a Record from a database
  • Insert: Create a new Record
  • Update: Change one or more fields on a Record
  • Delete: Destroy a record in the database
  • Has: Determine whether a given Record exists in a database
  • LoadWhere: Load a record where certain conditions obtain.

Structable is pragmatic in the sense that it allows ActiveRecord-like extension of the Record object to allow business logic. A Record does not *have* to be a simple data-only struct. It can have methods -- even methods that operate on the database.

Importantly, Structable does not do any relation management. There is no magic to convert structs, arrays, or maps to references to other tables. (If you want that, you may prefer GORM or GORP.) The preferred method of handling relations is to attach additional methods to the Record struct.

Structable uses Squirrel for statement building, and you may also use Squirrel for working with your data.

Basic Usage

The following example is taken from the `example/users.go` file.

package main

import (
	"github.com/Masterminds/squirrel"
	"github.com/Masterminds/structable"
	_ "github.com/lib/pq"

	"database/sql"
	"fmt"
)

// This is our struct. Notice that we make this a structable.Recorder.
type User struct {
	structable.Recorder
	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(UserTable, u)
	return u
}

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 = "matt@example.com"

	// Insert this as a new record.
	if err := user.Insert(); err != nil {
		panic(err.Error())
	}
	fmt.Printf("Initial insert has ID %d, name %s, and email %s\n", user.Id, user.Name, user.Email)

	// Now create another empty User and set the user's Name.
	again := NewUser(cache, driver)
	again.Id = user.Id

	// Load a duplicate copy of our user. This loads by the value of
	// again.Id
	again.Load()

	again.Email = "technosophos@example.com"
	if err := again.Update(); err != nil {
		panic(err.Error())
	}
	fmt.Printf("Updated user has ID %d and email %s\n", again.Id, again.Email)

	// Delete using the built-in Deleter. (delete by Id.)
	if err := again.Delete(); err != nil {
		panic(err.Error())
	}
	fmt.Printf("Deleted user %d\n", again.Id)
}

The above pattern closely binds the Recorder to the Record. Essentially, in this usage Structable works like an ActiveRecord.

It is also possible to emulate a DAO-type model and use the Recorder as a data access object and the Record as the data description object. An example of this method can be found in the `example/fence.go` code.

The Stbl Tag

The `stbl` tag is of the form:

stbl:"field_name [,PRIMARY_KEY[,AUTO_INCREMENT]]"

The field name is passed verbatim to the database. So `fieldName` will go to the database as `fieldName`. Structable is not at all opinionated about how you name your tables or fields. Some databases are, though, so you may need to be careful about your own naming conventions.

`PRIMARY_KEY` tells Structable that this field is (one of the pieces of) the primary key. Aliases: 'PRIMARY KEY'

`AUTO_INCREMENT` tells Structable that this field is created by the database, and should never be assigned during an Insert(). Aliases: SERIAL, AUTO INCREMENT

Limitations

Things Structable doesn't do (by design)

  • Guess table or column names. You must specify these.
  • Handle relations between tables.
  • Manage the schema.
  • Transform complex struct fields into simple ones (that is, serialize fields).

However, Squirrel can ease many of these tasks.

Index

Constants

View Source
const StructableTag = "stbl"

'stbl' is the main tag used for annotating Structable Records.

Variables

This section is empty.

Functions

This section is empty.

Types

type DbRecorder

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

Implements the Recorder interface, and stores data in a DB.

func New

func New(db squirrel.DBProxyBeginner, flavor string) *DbRecorder

New creates a new DbRecorder.

(The squirrel.DBProxy interface defines the functions normal for a database connection or a prepared statement cache.)

func (*DbRecorder) Bind

func (s *DbRecorder) Bind(tableName string, ar Record) Recorder

Bind binds a DbRecorder to a Record.

This takes a given structable.Record and binds it to the recorder. That means that the recorder will track all changes to the Record.

The table name tells the recorder which database table to link this record to. All storage operations will use that table.

func (*DbRecorder) Builder

Builder returns the statement builder for this recorder.

func (*DbRecorder) Columns

func (s *DbRecorder) Columns(includeKeys bool) []string

Columns returns the names of the columns on this table.

If includeKeys is false, the columns that are marked as keys are omitted from the returned list.

func (*DbRecorder) DB

DB returns the database (DBProxyBeginner) for this recorder.

func (*DbRecorder) Delete

func (s *DbRecorder) Delete() error

Delete deletes the record from the underlying table.

The fields on the present record will remain set, but not saved in the database.

func (*DbRecorder) Driver

func (s *DbRecorder) Driver() string

Driver returns the string name of the driver.

func (*DbRecorder) Exists

func (s *DbRecorder) Exists() (bool, error)

Exists returns `true` if and only if there is at least one record that matches the primary keys for this Record.

If the primary key on the Record has no value, this will look for records with no value (or the default value).

func (*DbRecorder) ExistsWhere

func (s *DbRecorder) ExistsWhere(pred interface{}, args ...interface{}) (bool, error)

ExistsWhere returns `true` if and only if there is at least one record that matches one (or multiple) conditions.

Conditions are expressed in the form of predicates and expected values that together build a WHERE clause. See Squirrel's Where(pred, args)

func (*DbRecorder) FieldReferences

func (s *DbRecorder) FieldReferences(withKeys bool) []interface{}

FieldReferences returns a list of references to fields on this object.

If withKeys is true, fields that compose the primary key will also be included. Otherwise, only non-primary key fields will be included.

This is used for processing SQL results:

dest := s.FieldReferences(false)
q := s.builder.Select(s.Columns(false)...).From(s.table)
err := q.QueryRow().Scan(dest...)

func (*DbRecorder) Init

func (d *DbRecorder) Init(db squirrel.DBProxyBeginner, flavor string)

Init initializes a DbRecorder

func (*DbRecorder) Insert

func (s *DbRecorder) Insert() error

Insert puts a new record into the database.

This operation is particularly sensitive to DB differences in cases where AUTO_INCREMENT is set on a member of the Record.

func (*DbRecorder) Interface

func (d *DbRecorder) Interface() interface{}

func (*DbRecorder) Key

func (s *DbRecorder) Key() []string

Key gets the string names of the fields used as primary key.

func (*DbRecorder) Load

func (s *DbRecorder) Load() error

Load selects the record from the database and loads the values into the bound Record.

Load uses the table's PRIMARY KEY(s) as the sole criterion for matching a record. Essentially, it is akin to `SELECT * FROM table WHERE primary_key = ?`.

This modifies the Record in-place. Other than the primary key fields, any other field will be overwritten by the value retrieved from the database.

func (*DbRecorder) LoadWhere

func (s *DbRecorder) LoadWhere(pred interface{}, args ...interface{}) error

LoadWhere loads an object based on a WHERE clause.

This can be used to define alternate loaders:

func (s *MyStructable) LoadUuid(uuid string) error {
	return s.LoadWhere("uuid = ?", uuid)
}

This functions similarly to Load, but with the notable difference that it loads the entire object (it does not skip keys used to do the lookup).

func (*DbRecorder) TableName

func (s *DbRecorder) TableName() string

TableName returns the table name of this recorder.

func (*DbRecorder) Update

func (s *DbRecorder) Update() error

Update updates the values on an existing entry.

This updates records where the Record's primary keys match the record in the database. Essentially, it runs `UPDATE table SET names=values WHERE id=?`

If no entry is found, update will NOT create (INSERT) a new record.

func (*DbRecorder) WhereIds

func (s *DbRecorder) WhereIds() map[string]interface{}

WhereIds gets a list of names and a list of values for all columns marked as primary keys.

type Describer

type Describer interface {
	// Columns gets the columns on this table.
	Columns(bool) []string
	// FieldReferences gets references to the fields on this object.
	FieldReferences(bool) []interface{}
	// WhereIds returns a map of ID fields to (current) ID values.
	//
	// This is useful to quickly generate where clauses.
	WhereIds() map[string]interface{}

	// TableName returns the table name.
	TableName() string
	// Builder returns the builder
	Builder() *squirrel.StatementBuilderType
	// DB returns a DB-like handle.
	DB() squirrel.DBProxyBeginner

	Driver() string

	Init(d squirrel.DBProxyBeginner, flavor string)
}

Describer is a structable object that can describe its table structure.

type Haecceity

type Haecceity interface {
	// Exists verifies that a thing exists and is of this type.
	// This uses the PRIMARY_KEY to verify that a record exists.
	Exists() (bool, error)
	// ExistsWhere verifies that a thing exists and is of the expected type.
	// It takes a WHERE clause, and it needs to gaurantee that at least one
	// record matches. It need not assure that *only* one item exists.
	ExistsWhere(interface{}, ...interface{}) (bool, error)
}

Haecceity indicates whether a thing exists.

Actually, it is responsible for testing whether a thing exists, and is what we think it is.

type Loader

type Loader interface {
	// Loads the entire Record using the value of the PRIMARY_KEY(s)
	// This will only fetch columns that are mapped on the bound Record. But you can think of it
	// as doing something like this:
	//
	// 	SELECT * FROM bound_table WHERE id=? LIMIT 1
	//
	// And then mapping the result to the currently bound Record.
	Load() error
	// Load by a WHERE-like clause. See Squirrel's Where(pred, args)
	LoadWhere(interface{}, ...interface{}) error
}

type Record

type Record interface{}
Record describes a struct that can be stored.

Example:

type Stool struct {
	Id 		 int 	`stbl:"id PRIMARY_KEY AUTO_INCREMENT"`
	Legs 	 int    `stbl:"number_of_legs"`
	Material string `stbl:"material"`
	Ignored  string // will not be stored.
}

The above links the Stool record to a database table that has a primary key (with auto-incrementing values) called 'id', an int field named 'number_of_legs', and a 'material' field that is a VARCHAR or TEXT (depending on the database implementation).

type Recorder

type Recorder interface {
	// Bind this Recorder to a table and to a Record.
	//
	// The table name is used verbatim. DO NOT TRUST USER-SUPPLIED VALUES.
	//
	// The struct is examined for tags, and those tags are parsed and used to determine
	// details about each field.
	Bind(string, Record) Recorder

	// Interface provides a way of fetching the record from the Recorder.
	//
	// A record is bound to a Recorder via Bind, and retrieved from a Recorder
	// via Interface().
	//
	// This is conceptually similar to reflect.Value.Interface().
	Interface() interface{}

	Loader
	Haecceity
	Saver
	Describer
}

A Recorder is responsible for managing the persistence of a Record. A Recorder is bound to a struct, which it then examines for fields that should be stored in the database. From that point on, a recorder can manage the persistent lifecycle of the record.

func List

func List(d Recorder, limit, offset uint64) ([]Recorder, error)

List returns a list of objects of the given kind.

This runs a Select of the given kind, and returns the results.

func ListWhere

func ListWhere(d Recorder, fn WhereFunc) ([]Recorder, error)

ListWhere takes a Recorder and a query modifying function and executes a query.

The WhereFunc will be given a SELECT d.Colsumns() FROM d.TableName() statement, and may modify it. Note that while joining is supported, changing the column list will have unpredictable side effects. It is advised that joins be done using Squirrel instead.

This will return a list of Recorder objects, where the underlying type of each matches the underlying type of the passed-in 'd' Recorder.

type Saver

type Saver interface {
	// Insert inserts the bound Record into the bound table.
	Insert() error

	// Update updates all of the fields on the bound Record based on the PRIMARY_KEY fields.
	//
	// Essentially, it does something like this:
	// 	UPDATE bound_table SET every=?, field=?, but=?, keys=? WHERE primary_key=?
	Update() error

	// Deletes a Record based on its PRIMARY_KEY(s).
	Delete() error
}

type WhereFunc

type WhereFunc func(desc Describer, query squirrel.SelectBuilder) (squirrel.SelectBuilder, error)

WhereFunc modifies a basic select operation to add conditions.

Technically, conditions are not limited to adding where clauses. It will receive a select statement with the 'SELECT ... FROM tablename' portion composed already.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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