structable

package module
v0.0.0-...-0ac4967 Latest Latest
Warning

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

Go to latest
Published: Mar 26, 2015 License: MIT Imports: 4 Imported by: 0

README

Structable: Struct-Table Mapping for Go

Warning: Structable 3.0.0 has new interfaces, and is incompatible with older versions of Structable.

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
    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...

$ go get github.com/technosophos/structable

And import it via:

import "github.com/technosophos/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() // 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

Tested On
  • MySQL (5.5)
  • PostgreSQL (9.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/lann/squirrel"
	"github.com/technosophos/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 AfterInserter

type AfterInserter interface {
	AfterInsert() error
}

Called after Insert()

type AfterLoader

type AfterLoader interface {
	AfterLoad() error
}

Called after Load() and LoadWhere()

type AfterUpdater

type AfterUpdater interface {
	AfterUpdate() error
}

Called after Update()

type BeforeDeleter

type BeforeDeleter interface {
	BeforeDelete() error
}

Called before Delete()

type BeforeInserter

type BeforeInserter interface {
	BeforeInsert() error
}

Called before Insert()

type BeforeUpdater

type BeforeUpdater interface {
	BeforeUpdate() error
}

Called before Update()

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) 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) 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)

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) 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 differance that it loads the entire object (it does not skip keys used to do the lookup).

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.

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 implements John Duns Scotus.

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

	Loader
	Haecceity
	Saver
}

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.

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
}

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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