Saint will manage all associations found on given model.

You can decide what associations to be managed and which to be ignored.

@example: manage only :author relation

# DataMapper model
class PageModel
    # basic setup
    belongs_to :author
    has n, :menus
    has n, :visits
end

# Saint setup
class Page
    include Saint::Api
    saint.model PageModel do
        relations :author
    end
end

@example: manage all relations but :visits

class Page
    include Saint::Api
    saint.model PageModel do
        relations_ignored :visits
    end
end

To ignore tree-related associations, use tree_ignored inside #model block:

class Page
    include Saint::Api
    saint.model PageModel do
        tree_ignored true
    end
end

Automatically defined associations can be fine-tuned later.

@example: manage :visits relations in readonly mode

class Page
    include Saint::Api
    saint.model PageModel
    saint.has_n :visits, VisitsModel, readonly: true
end

Manually defining associations

Saint does support "belongs to", "has N" and "has N through" associations.

Belongs to

To define a "belongs to" assoc, use saint.belongs_to, passing as first argument the relation name and remote ORM model as second argument.

Example: game belongs to edition

class Game
    saint.belongs_to :edition, Model::Edition
end

Has N

To define a "has N" assoc, use saint.has_n, passing as first argument the relation name and remote ORM model as second argument.

Example: game has N goals

class Game
    saint.has_n :goals, Model::Goal
end

Has N through

To define a "has N through" assoc, use saint.has_n, passing as first argument the relation name, remote ORM model as second arg and middle model as third arg.

Example: game has many scorers(players), through PlayersScorers model

class Game
    saint.has_n :scorers, Model::Player, Model::PlayersScorers
end

Tree

There are also tree association, when some item may belong to another item of same model.

Example: any page may have unlimited children pages

class Pages
    saint.model Model::Page
    saint.is_tree
end

Options

column

When rendering association UI, Saint will display only first non-id column of remote model.

Use column inside block to instruct Saint about what columns to include and how to render them.

The syntax is same as when defining Saint columns by saint.column

Example: display name and email when rendering authors assoc UI

class Pages
    saint.belongs_to :author, Model::Author do
        column :name
        column :email
    end
end

Example: display page name plus an extra(non ORM) column

class Authors
    saint.has_n :pages, Model::Page do
        column :name
        column :views do
            value { row.views }
        end
    end
end

remote_node

Saint needs only remote model for association to work.

However, there are 3 obvious benefits if associated model is managed by Saint as well:

To declare remote node, simply use remote_node inside block.

In example below, Pages is a Saint node and it is associated with authors model. Saint will build an assoc UI without linking to authors pages, cause authors are not managed by Saint, i.e. there are no class including Saint::Api and using Model::Author as model.

class Pages
    include Saint::Api

    saint.belongs_to :author, Model::Author
end

In next example, both Pages and Authors are valid Saint nodes and remote_node used accordingly, so assoc UI will have filters and links, but not "Create New" button.

class Authors
    include Saint::Api

    saint.model Model::Author
end

class Pages
    include Saint::Api

    saint.belongs_to :author, Model::Author do
        remote_node Authors
    end
end

To have "Create New" buttons, simply set second argument of remote_node to true:

saint.belongs_to :author, Model::Author do
    remote_node Authors, true
end

filter

By default, assoc UI will display all remote items.

Though it comes with a pager(and filters if remote node provided), it is yet ineffective to loop through pages and use filters to find needed items.

More effective would be to display only needed items.

Saint associations comes with a handy #filter method, that allow to build static and dynamic filters.

For static filters, simply pass a hash of columns with values:

Example: display only active authors:

class Page
    saint.belongs_to :author, Model::Author do
        filter active: 1
    end
end

For dynamic filters, pass a proc. Proc will receive back the current local item, so you can create a hash using its data.

Example: display only teams of same region as current game:

class Game
    saint.belongs_to :team, Model::Team do
        filter do |game|
            {region_id: game.edition.competition.region_id}
        end
    end
end

To combine static and dynamic filters, pass both a hash and a proc. If static and dynamic filters has same keys, dynamic filters will override the static ones.

order

Use order with a column and direction to modify the default extracting order.

By default, if remote node defined, items will be extracted by order defined at remote node,
otherwise, items will be extracted by remote primary key, in descending order.

Example: order games by date, newest first

class Team
    saint.has_n :games, Model::Game do
        order :date, :desc
    end
end

Example: order authors by name

class Page
    saint.belongs_to :author, Model::Author do
        order :name
    end
end

items_per_page

Defaulted to 10

Example: display 100 games per page

class Team
    saint.has_n :games, Model::Game do
        items_per_page 100
        # or
        ipp 100
    end
end

label

By default, saint will build the assoc label from provided name.

Example: here Saint will use "Pages" label

class Author
    saint.has_n :pages, Model::Page
end

Example: set custom label

class Author
    saint.has_n :pages, Model::Page do
        label 'CMS Pages'
    end
end

readonly

Example: prohibit attaching / detaching remote items

class Author
    saint.has_n :pages, Model::Page do
        readonly true
    end
end

callbacks

Allow to execute a callback before/after assoc updated

Association callbacks has an eventless aspect, meant you can not define an callback for each event.

You can define a single callback and it will fire on any event, being it create, update or delete.

Example: execute a callback BEFORE assoc created/updated/deleted

saint.has_n :pages, Model::Page do
    before do
        # some logic
    end
end

Example: execute a callback AFTER assoc created/updated/deleted

saint.has_n :pages, Model::Page do
    after do
        # some logic
    end
end

Keys

local_key

The column on local model that should match the primary key of remote model.

Example:

# scenario:
#   relation: belongs_to
#   local model: Model::City
#   remote model: Model::Country

class City
  saint.model Model::City
  saint.belongs_to :country, Model::Country
end

# this assoc expects Model::City to respond to #country_id,
# as Model::City#country_id will be compared to Model::Country#id.
# using #local_key to override this:

class City
  saint.model Model::City
  saint.belongs_to :country, Model::Country do
    local_key :cntr_id
  end
end
# now Model::City#cntr_id will be compared to Model::Country#id

On has_n_through relations, local key is defaulted to name of local model suffixed by _id.

Example:

# scenario:
#   relation: has_n_through
#   local model: Model::Page
#   remote model: Model::Menu

class Page
  saint.model Model::Page
  saint.has_n :menus, Model::Menu, Model::MenuPage
end

# this assoc expects Model::MenuPage to respond to #page_id,
# as Model::Page#id will be compared to Model::MenuPage#page_id.
# using #local_key to override this:

class Page
  saint.model Model::Page
  saint.has_n :menus, Model::Menu, Model::MenuPage do
    local_key :pid
  end
end
# now Model::Page#id will be compared to Model::MenuPage#pid

remote_key

The column on remote model that should match the primary key of local model.

Example:

# scenario:
#   relation: has_n
#   local model: Model::Author
#   remote model: Model::Page

class Author
  saint.model Model::Author
  saint.has_n :pages, Model::Page
end

# this assoc expects Model::Page to respond to :author_id,
# as Model::Author#id will be compared to Model::Page#author_id.
# using #remote_key to override this:

class Author
  saint.model Model::Author
  saint.has_n :pages, Model::Page do
    remote_key :auid
  end
end
# now Model::Author#id will be compared to Model::Page#auid

On has_n_through relations, remote key is defaulted to name of remote model suffixed by _id.

Example:

# scenario:
#   relation: has_n_through
#   local model: Model::Page
#   remote model: Model::Menu

class Page
  saint.model Model::Page
  saint.has_n :menus, Model::Menu, Model::MenuPage
end

# this assoc expects Model::MenuPage to respond to #menu_id,
# as Model::Menu#id will be compared to Model::MenuPage#menu_id.
# using #local_key to override this:

class Page
  saint.model Model::Page
  saint.has_n :menus, Model::Menu, Model::MenuPage do
    remote_key :mid
  end
end
# now Model::Menu#id will be compared to Model::MenuPage#mid