Skip to main content

ORM-like behaviour with `db.Record`, `db.Store` and hooks

upper/db provides two complementary interfaces that can help you model apps in a more ORM-ish way: Record and Store

Using Record for single entries

Record is an interface that can be satisfied by structs that represent single records from a collection:

type Record interface {
Store(sess Session) Store
}

If you have a Book struct that looks like this:

type Book struct {
Title string `db:"title"`
}

you can make it compatible with Record by adding a Store() method:

type Book struct {
// ...
}

func (b *Book) Store(sess db.Session) Store {
return sess.Collection("books")
}

var _ = db.Record(&Book{})

Records can be used with Session methods:

// Retrieving a record
sess.Get(&book, 123)

// Creating or updating a record
sess.Save(&book)

// Delete a record
sess.Delete(&book)

See the tour example on Record.

Using Store for collections

Store is an interface that can be satisfied by collections:

type Store interface {
Collection
}

Let's suppose we want to create a Store for book records, we'd name it BookStore:

type BooksStore struct {
db.Collection
}

var _ = db.Store(&BooksStore{})

You can extend Store structs with custom methods. The following method returns a book that matches a given title:

func (books *BooksStore) FindByTitle(title string) (*Book, error) {
var book Book
if err := books.Find(db.Cond{"title": title}).One(&book); err != nil {
return nil, err
}
return &book, nil
}

A recommended pattern for stores is creating a function to enclose the store's initialization:

func Books(sess db.Session) *BooksStore {
return &BooksStore{sess.Collection("books")}
}

Use the Books() method instead of Collection("books"):

err := Books(sess).Find(...).All(...)

See the tour example on Store.

Using Record and Store

The Record and Store interfaces do not depend on each other but can be mixed together. See the following example:

type BooksStore struct {
db.Collection
}

func (books *BooksStore) FindByTitle(title string) (*Book, error) {
// ...
}

// Books initializes a BooksStore
func Books(sess db.Session) *BooksStore {
return &BooksStore{sess.Collection("books")}
}

type Book struct {
Title string `db:"title"`
}

func (b *Book) Store(sess db.Session) Store {
// Note that we're using the Books function defined above instead
// of sess.Collection.
return Books(sess)
}

var _ = db.Store(&BooksStore{})
var _ = db.Record(&Book{})

Using Record hooks

Hooks are tasks to be performed before or after a specific action happens on a record. You can add hooks to models by defining unique methods like BeforeCreateAfterUpdate, or Validate that satisfy specific signatures:

type User struct {
// ...
}

func (u *User) Store(sess db.Session) db.Store {
// ...
}

// BeforeCreate hook
func (u *User) BeforeCreate(sess db.Session) error {
// ...
}

// Validate hook
func (u *User) Validate() error {
// ...
}

// Interface checks
var _ = interface{
db.Record
db.BeforeCreateHook
db.Validator
}(&User{})

Hooks are only executed when using methods that explicitly require db.Record, such a sess.Getsess.Save or sess.Delete:

// Hooks will be executed
sess.Save(&user)

// Hooks won't be executed
sess.Collection(...).Find().Update(&user)

Validate() hook

The Validate() hook is called before creating or updating a record.

If Validate() returns a non-nil error, the operation is aborted.

The purpose of this method is for models to run preliminary checks on their own data before executing a query.

If you're using hooks, make sure your model satisfies the db.Validator interface at compile time:

var _ = db.Validator(&User{})

BeforeCreate​

The BeforeCreate(db.Session) error hook is called before inserting a record into a collection. If BeforeCreate() returns a non-nil error, the operation is canceled and rolled back.

The purpose of this method is for models to run specific tasks before changing the state of a collection.

func (user *User) BeforeCreate(sess db.Session) error {
// Check if the e-mail was already registered by another user.
c, err := user.Store(sess).
Find(db.Cond{"email": user.Email}).
Count()
if err != nil {
return err
}
if c > 0 {
return errors.New("e-mail already exists")
}

return nil
}

Make sure your model satisfies the db.BeforeCreateHook interface at compile time:

var _ = db.BeforeCreateHook(&User{})

AfterCreate

The AfterCreate(db.Session) error hook is called after having inserted a record into a collection. If AfterCreate() returns a non-nil error, then the whole operation is canceled and rolled back.

The purpose of this method is for models to run specific tasks after changing the state of a collection.

func (user *User) AfterCreate(sess db.Session) error {
// Send log to somewhere else.
events.Log("Item has been inserted.")
return nil
}

Make sure your model satisfies the db.AfterCreateHook interface at compile time:

var _ = db.AfterCreateHook(&User{})

BeforeUpdate

The BeforeUpdate(db.Session) error hook is called before updating a record from a collection. If BeforeUpdate() returns a non-nil error, then the whole operation is canceled and rolled back.

The purpose of this method is for models to run specific tasks before changing the state of a collection.

func (user *User) BeforeUpdate(sess db.Session) error {
// Check if the e-mail is already in use.
c, err := user.Store(sess).
Find(db.Cond{
"email": user.Email,
"id": db.NotEq(user.ID),
}).
Count()
if err != nil {
return err
}
if c > 0 {
return errors.New("e-mail is already in use")
}

return nil
}

Make sure your model satisfies the db.BeforeUpdateHook interface at compile time:

var _ = db.BeforeUpdateHook(&User{})

AfterUpdate

The AfterUpdate(db.Session) error hook is called after having updated a record from a collection. If AfterUpdate() returns a non-nil error, then the whole operation is canceled and rolled back.

The purpose of this method is for models to run specific tasks after changing the state of a collection.

func (user *User) AfterUpdate(sess db.Session) error {
// Send log to somewhere
events.Log("Item has been updated.")
return nil
}

Make sure your model satisfies the AfterUpdate interface at compile time:

var _ = db.AfterUpdate(&User{})

BeforeDelete

The BeforeDelete(db.Session) error hook is called before removing a record from a collection. If BeforeDelete() returns a non-nil error, then the whole operation is canceled and rolled back.

The purpose of this method is for models to run specific tasks before changing the state of a collection.

func (post *Post) BeforeDelete(sess db.Session) error {
// Check if the post is unpublished before deletion
if post.Published {
return errors.New("post must be unpublished before deletion")
}
return nil
}

Make sure your model satisfies the db.BeforeDeleteHook interface at compile time:

var _ = db.BeforeDeleteHook(&Post{})

AfterDelete

The AfterDelete(db.Session) error hook is called after having deleted a record from a collection. If AfterDelete() returns a non-nil error, then the whole operation is canceled and rolled back.

The purpose of this method is for models to run specific tasks after changing the state of a collection.

func (post *Post) AfterDelete(sess db.Session) error {
// Update post counter
Stats(sess).Update(...)
return nil
}

Make sure your model satisfies the db.AfterDeleteHook interface at compile time:

var _ = db.AfterDeleteHook(&Post{})