bongo

package module
v0.10.4 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2015 License: MIT Imports: 11 Imported by: 9

README

What's Bongo?

We couldn't find a good ODM for MongoDB written in Go, so we made one. Bongo is a wrapper for mgo (https://github.com/go-mgo/mgo) that adds ODM, hooks, validation, and cascade support to its raw Mongo functions.

Bongo is tested using the fantasic GoConvey (https://github.com/smartystreets/goconvey)

Build Status

Coverage Status

Stablity

Since we're not yet at a major release, some things in the API might change. Here's a list:

  • Save - stable
  • Find/FindOne/FindById - stable
  • Delete - stable
  • Save/Delete/Find/Validation hooks - stable
  • Cascade - unstable (might need a refactor)
  • Change Tracking - stable
  • Validation methods - stable

Usage

Basic Usage

Import the Library

go get github.com/maxwellhealth/bongo

import "github.com/maxwellhealth/bongo"

And install dependencies:

cd $GOHOME/src/github.com/maxwellhealth/bongo && go get .

Connect to a Database

Create a new bongo.Config instance:

config := &bongo.Config{
	ConnectionString: "localhost",
	Database:         "bongotest",
}

Then just create a new instance of bongo.Connection, and make sure to handle any connection errors:

connection, err := bongo.Connect(config)

if err != nil {
	log.Fatal(err)
}

If you need to, you can access the raw mgo session with connection.Session

Create a Document

Any struct can be used as a document as long as it satisfies the Document interface (SetId(bson.ObjectId), GetId() bson.ObjectId). We recommend that you use the DocumentBase provided with Bongo, which implements that interface as well as the NewTracker and TimeTracker interfaces (to keep track of new/existing documents and created/modified timestamps). If you use the DocumentBase or something similar, make sure you use bson:",inline" otherwise you will get nested behavior when the data goes to your database.

For example:

type Person struct {
	bongo.DocumentBase `bson:",inline"`
	FirstName string
	LastName string
	Gender string
}

You can use child structs as well.

type Person struct {
	bongo.DocumentBase `bson:",inline"`
	FirstName string
	LastName string
	Gender string
	HomeAddress struct {
		Street string
		Suite string
		City string
		State string
		Zip string
	}
}
Hooks

You can add special methods to your document type that will automatically get called by bongo during certain actions. Hooks get passed the current *bongo.Collection so you can avoid having to couple them with your actual database layer. Currently available hooks are:

  • func (s *ModelStruct) Validate(*bongo.Collection) []error (returns a slice of errors - if it is empty then it is assumed that validation succeeded)
  • func (s *ModelStruct) BeforeSave(*bongo.Collection) error
  • func (s *ModelStruct) AfterSave(*bongo.Collection) error
  • func (s *ModelStruct) BeforeDelete(*bongo.Collection) error
  • func (s *ModelStruct) AfterDelete(*bongo.Collection) error
  • func (s *ModelStruct) AfterFind(*bongo.Collection) error
Saving Models

Just call save on a collection instance.

myPerson := &Person{
	FirstName:"Testy",
	LastName:"McGee",
	Gender:"male",
}
err := connection.Collection("people").Save(myPerson)

Now you'll have a new document in the people collection. If there is an error, you can check if it is a validation error using a type assertion:

if vErr, ok := err.(*bongo.ValidationError); ok {
	fmt.Println("Validation errors are:", vErr.Errors)
} else {
	fmt.Println("Got a real error:", err.Error())
}
Deleting Documents

There are three ways to delete a document.

DeleteDocument

Same thing as Save - just call DeleteDocument on the collection and pass the document instance.

err := connection.Collection("people").DeleteDocument(person)

This will run the BeforeDelete and AfterDelete hooks, if applicable.

DeleteOne

This just delegates to mgo.Collection.Remove. It will not run the BeforeDelete and AfterDelete hooks.

err := connection.Collection("people").DeleteOne(bson.M{"FirstName":"Testy"})
Delete

This delegates to mgo.Collection.RemoveAll. It will not run the BeforeDelete and AfterDelete hooks.

changeInfo, err := connection.Collection("people").Delete(bson.M{"FirstName":"Testy"})
fmt.Printf("Deleted %d documents", changeInfo.Removed)
Find by ID
person := &Person{}
err := connection.Collection("people").FindById(bson.ObjectIdHex(StringId), person)

The error returned can be a DocumentNotFoundError or a more low-level MongoDB error. To check, use a type assertion:

if dnfError, ok := err.(*bongo.DocumentNotFoundError); ok {
	fmt.Println("document not found")
} else {
	fmt.Println("real error " + err.Error())
}
Find

Finds will return an instance of ResultSet, which you can then optionally Paginate and iterate through to get all results.


// *bongo.ResultSet
results := connection.Collection("people").Find(bson.M{"firstName":"Bob"})

person := &Person{}

count := 0

for results.Next(person) {
	fmt.Println(person.FirstName)
}

To paginate, you can run Paginate(perPage int, currentPage int) on the result of connection.Find(). That will return an instance of bongo.PaginationInfo, with properties like TotalRecords, RecordsOnPage, etc.

To use additional functions like sort, skip, limit, etc, you can access the underlying mgo Query via ResultSet.Query.

Find One

Same as find, but it will populate the reference of the struct you provide as the second argument.


person := &Person{}

err := connection.Collection("people").FindOne(bson.M{"firstName":"Bob"})

if err != nil {
	fmt.Println(err.Error())
} else {
	fmt.Println("Found user:", person.FirstName)
}

Change Tracking

If your model struct implements the Trackable interface, it will automatically track changes to your model so you can compare the current values with the original. For example:

type MyModel struct {
	bongo.DocumentBase `bson:",inline"`
	StringVal string
	diffTracker *bongo.DiffTracker
}

// Easy way to lazy load a diff tracker
func (m *MyModel) GetDiffTracker() *DiffTracker {
	if m.diffTracker == nil {
		m.diffTracker = bongo.NewDiffTracker(m)
	}

	return m.diffTracker
}

myModel := &MyModel{}

Use as follows:

Check if a field has been modified
// Store the current state for comparison
myModel.GetDiffTracker().Reset()

// Change a property...
myModel.StringVal = "foo"

// We know it's been instantiated so no need to use GetDiffTracker()
fmt.Println(myModel.diffTracker.Modified("StringVal")) // true
myModel.diffTracker.Reset()
fmt.Println(myModel.diffTracker.Modified("StringVal")) // false
Get all modified fields
myModel.StringVal = "foo"
// Store the current state for comparison
myModel.GetDiffTracker().Reset()

isNew, modifiedFields := myModel.GetModified()

fmt.Println(isNew, modifiedFields) // false, ["StringVal"]
myModel.diffTracker.Reset()

isNew, modifiedFields = myModel.GetModified()
fmt.Println(isNew, modifiedFields) // false, []
Diff-tracking Session

If you are going to be checking more than one field, you should instantiate a new DiffTrackingSession with diffTracker.NewSession(useBsonTags bool). This will load the changed fields into the session. Otherwise with each call to diffTracker.Modified(), it will have to recalculate the changed fields.

Cascade Save/Delete

Bongo supports cascading portions of documents to related documents and the subsequent cleanup upon deletion. For example, if you have a Team collection, and each team has an array of Players, you can cascade a player's first name and last name to his or her team.Players array on save, and remove that element in the array if you delete the player.

To use this feature, your struct needs to have an exported method called GetCascade, which returns an array of *bongo.CascadeConfig. Additionally, if you want to make use of the OldQuery property to remove references from previously related documents, you should probably alsotimplement the DiffTracker on your model struct (see above).

You can also leave ThroughProp blank, in which case the properties of the document will be cascaded directly onto the related document. This is useful when you want to cascade ObjectId properties or other references, but it is important that you keep in mind that these properties will be nullified on the related document when the main doc is deleted or changes references.

Also note that like the above hooks, the GetCascade method will be passed the instance of the bongo.Collection so you can keep your models decoupled from your database layer.

Casade Configuration
type CascadeConfig struct {
	// The collection to cascade to
	Collection *mgo.Collection

	// The relation type (does the target doc have an array of these docs [REL_MANY] or just reference a single doc [REL_ONE])
	RelType int

	// The property on the related doc to populate
	ThroughProp string

	// The query to find related docs
	Query bson.M

	// The data that constructs the query may have changed - this is to remove self from previous relations
	OldQuery bson.M

	// Properties that will be cascaded/deleted. Can (should) be in dot notation for nested properties. This is used to nullify properties when there is an OldQuery or if the document is deleted.
	Properties []string

	// The actual data that will be cascade
	Data interface{}
}
Example
type ChildRef struct {
	Id bson.ObjectId `bson:"_id" json:"_id"`
	Name string
}
func (c *Child) GetCascade(collection *bongo.Collection) []*bongo.CascadeConfig {
	connection := collection.Connection
	rel := &ChildRef {
		Id:c.Id,
		Name:c.Name,
	}
	cascadeSingle := &bongo.CascadeConfig{
		Collection:  connection.Collection("parents").Collection(),
		Properties:  []string{"name"},
		Data:rel,
		ThroughProp: "child",
		RelType:     bongo.REL_ONE,
		Query: bson.M{
			"_id": c.ParentId,
		},
	}

	cascadeMulti := &bongo.CascadeConfig{
		Collection:  connection.Collection("parents").Collection(),
		Properties:  []string{"name"},
		Data:rel,
		ThroughProp: "children",
		RelType:     bongo.REL_MANY,
		Query: bson.M{
			"_id": c.ParentId,
		},
	}

	if c.DiffTracker.Modified("ParentId") {

		origId, _ := c.DiffTracker.GetOriginalValue("ParentId")
		if origId != nil {
			oldQuery := bson.M{
				"_id": origId,
			}
			cascadeSingle.OldQuery = oldQuery
			cascadeMulti.OldQuery = oldQuery
		}

	}

	return []*bongo.CascadeConfig{cascadeSingle, cascadeMulti}
}

This does the following:

  1. When you save a child, it will populate its parent's (defined by cascadeSingle.Query) child property with an object, consisting of one key/value pair (name)

  2. When you save a child, it will also modify its parent's (defined by cascadeMulti.Query) children array, either modifying or pushing to the array of key/value pairs, also with just name.

  3. When you delete a child, it will use cascadeSingle.OldQuery to remove the reference from its previous parent.child

  4. When you delete a child, it will also use cascadeMulti.OldQuery to remove the reference from its previous parent.children

Note that the ThroughProp must be the actual field name in the database (bson tag), not the property name on the struct. If there is no ThroughProp, the data will be cascaded directly onto the root of the document.

Documentation

Index

Constants

View Source
const (
	REL_MANY = iota
	REL_ONE  = iota
)

Relation types (one-to-many or one-to-one)

Variables

This section is empty.

Functions

func CascadeDelete

func CascadeDelete(collection *Collection, doc interface{})

Deletes references to a document from its related documents

func CascadeSave

func CascadeSave(collection *Collection, doc Document) error

Cascades a document's properties to related documents, after it has been prepared for db insertion (encrypted, etc)

func GetBsonName

func GetBsonName(field reflect.StructField) string

func GetChangedFields added in v0.10.0

func GetChangedFields(struct1 interface{}, struct2 interface{}, useBson bool) ([]string, error)

func MapFromCascadeProperties

func MapFromCascadeProperties(properties []string, doc Document) map[string]interface{}

If you need to, you can use this to construct the data map that will be cascaded down to related documents. Doing this is not recommended unless the cascaded fields are dynamic.

func ValidateInclusionIn

func ValidateInclusionIn(value string, options []string) bool

func ValidateMongoIdRef

func ValidateMongoIdRef(id bson.ObjectId, collection *Collection) bool

func ValidateRequired

func ValidateRequired(val interface{}) bool

Types

type AfterDeleteHook

type AfterDeleteHook interface {
	AfterDelete(*Collection) error
}

type AfterFindHook

type AfterFindHook interface {
	AfterFind(*Collection) error
}

type AfterSaveHook

type AfterSaveHook interface {
	AfterSave(*Collection) error
}

type BeforeDeleteHook

type BeforeDeleteHook interface {
	BeforeDelete(*Collection) error
}

type BeforeSaveHook

type BeforeSaveHook interface {
	BeforeSave(*Collection) error
}

type CascadeConfig

type CascadeConfig struct {
	// The collection to cascade to
	Collection *Collection

	// The relation type (does the target doc have an array of these docs [REL_MANY] or just reference a single doc [REL_ONE])
	RelType int

	// The property on the related doc to populate
	ThroughProp string

	// The query to find related docs
	Query bson.M

	// The data that constructs the query may have changed - this is to remove self from previous relations
	OldQuery bson.M

	// Should it also cascade the related doc on save?
	Nest bool

	// If there is no through prop, we need to know which properties to nullify if a document is deleted
	// and cascades to the root level of a related document. These are also used to nullify the previous relation
	// if the relation ID is changed
	Properties []string

	// Full data to cascade down to the related document. Note
	Data interface{}

	// An instance of the related doc if it needs to be nested
	Instance Document

	// If this is true, then just run the "remove" parts of the queries, instead of the remove + add
	RemoveOnly bool

	// If this is provided, use this field instead of _id for determining "sameness". This must also be a bson.ObjectId field
	ReferenceQuery []*ReferenceField
}

Configuration to tell Bongo how to cascade data to related documents on save or delete

type CascadeFilter

type CascadeFilter func(data map[string]interface{})

type CascadingDocument

type CascadingDocument interface {
	GetCascade(*Collection) []*CascadeConfig
}

type Collection

type Collection struct {
	Name       string
	Connection *Connection
}

func (*Collection) Collection

func (c *Collection) Collection() *mgo.Collection

func (*Collection) Delete

func (c *Collection) Delete(query bson.M) (*mgo.ChangeInfo, error)

Convenience method which just delegates to mgo. Note that hooks are NOT run

func (*Collection) DeleteDocument added in v0.9.4

func (c *Collection) DeleteDocument(doc Document) error

func (*Collection) DeleteOne added in v0.9.4

func (c *Collection) DeleteOne(query bson.M) error

Convenience method which just delegates to mgo. Note that hooks are NOT run

func (*Collection) Find

func (c *Collection) Find(query interface{}) *ResultSet

This doesn't actually do any DB interaction, it just creates the result set so we can start looping through on the iterator

func (*Collection) FindById

func (c *Collection) FindById(id bson.ObjectId, doc interface{}) error

func (*Collection) FindOne

func (c *Collection) FindOne(query interface{}, doc interface{}) error

func (*Collection) PreSave added in v0.9.5

func (c *Collection) PreSave(doc Document) error

func (*Collection) Save

func (c *Collection) Save(doc Document) error

type Config

type Config struct {
	ConnectionString string
	Database         string
}

type Connection

type Connection struct {
	Config  *Config
	Session *mgo.Session
}

func Connect

func Connect(config *Config) (*Connection, error)

Create a new connection and run Connect()

func (*Connection) Collection

func (m *Connection) Collection(name string) *Collection

func (*Connection) Connect

func (m *Connection) Connect() (err error)

Connect to the database using the provided config

type DiffTracker

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

func NewDiffTracker

func NewDiffTracker(doc interface{}) *DiffTracker

func (*DiffTracker) Clear

func (d *DiffTracker) Clear()

func (*DiffTracker) Compare

func (d *DiffTracker) Compare(useBson bool) (bool, []string, error)

func (*DiffTracker) GetModified

func (d *DiffTracker) GetModified(useBson bool) (bool, []string)

func (*DiffTracker) GetOriginalValue

func (d *DiffTracker) GetOriginalValue(field string) (interface{}, error)

func (*DiffTracker) Modified

func (d *DiffTracker) Modified(field string) bool

func (*DiffTracker) NewSession

func (d *DiffTracker) NewSession(useBsonTags bool) (*DiffTrackingSession, error)

func (*DiffTracker) Reset

func (d *DiffTracker) Reset()

func (*DiffTracker) SetOriginal added in v0.10.1

func (d *DiffTracker) SetOriginal(orig interface{})

type DiffTrackingSession

type DiffTrackingSession struct {
	ChangedFields []string
	IsNew         bool
}

func (*DiffTrackingSession) Modified

func (s *DiffTrackingSession) Modified(field string) bool

type Document

type Document interface {
	GetId() bson.ObjectId
	SetId(bson.ObjectId)
}

type DocumentBase

type DocumentBase struct {
	Id       bson.ObjectId `bson:"_id,omitempty" json:"_id"`
	Created  time.Time     `bson:"_created" json:"_created"`
	Modified time.Time     `bson:"_modified" json:"_modified"`
	// contains filtered or unexported fields
}

func (*DocumentBase) GetId

func (d *DocumentBase) GetId() bson.ObjectId

Satisfy the document interface

func (*DocumentBase) IsNew

func (d *DocumentBase) IsNew() bool

func (*DocumentBase) SetCreated

func (d *DocumentBase) SetCreated(t time.Time)

func (*DocumentBase) SetId

func (d *DocumentBase) SetId(id bson.ObjectId)

func (*DocumentBase) SetIsNew

func (d *DocumentBase) SetIsNew(isNew bool)

Satisfy the new tracker interface

func (*DocumentBase) SetModified

func (d *DocumentBase) SetModified(t time.Time)

type DocumentNotFoundError

type DocumentNotFoundError struct{}

func (DocumentNotFoundError) Error

func (d DocumentNotFoundError) Error() string

type NewTracker

type NewTracker interface {
	SetIsNew(bool)
	IsNew() bool
}

type PaginationInfo

type PaginationInfo struct {
	Current       int `json:"current"`
	TotalPages    int `json:"totalPages"`
	PerPage       int `json:"perPage"`
	TotalRecords  int `json:"totalRecords"`
	RecordsOnPage int `json:"recordsOnPage"`
}

type ReferenceField

type ReferenceField struct {
	BsonName string
	Value    interface{}
}

type ResultSet

type ResultSet struct {
	Query *mgo.Query
	Iter  *mgo.Iter

	Collection *Collection
	Error      error
	Params     interface{}
	// contains filtered or unexported fields
}

func (*ResultSet) Free

func (r *ResultSet) Free() error

func (*ResultSet) Next

func (r *ResultSet) Next(doc interface{}) bool

func (*ResultSet) Paginate

func (r *ResultSet) Paginate(perPage, page int) (*PaginationInfo, error)

Set skip + limit on the current query and generates a PaginationInfo struct with info for your front end

type Stringer added in v0.10.2

type Stringer interface {
	String() string
}

type TimeTracker

type TimeTracker interface {
	SetCreated(time.Time)
	SetModified(time.Time)
}

type Trackable

type Trackable interface {
	GetDiffTracker() *DiffTracker
}

type ValidateHook

type ValidateHook interface {
	Validate(*Collection) []error
}

type ValidationError

type ValidationError struct {
	Errors []error
}

func (*ValidationError) Error

func (v *ValidationError) Error() string

Jump to

Keyboard shortcuts

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