module Tenacity::ClassMethods

Associations are a set of macro-like class methods for tying objects together through their ids. They express relationships like “Project has one Project Manager” or “Project belongs to a Portfolio”. Each macro adds a number of methods to the class which are specialized according to the collection or association symbol and the options hash. It works much the same way as Ruby's own attr* methods.

class Project
  include SupportedDatabaseClient
  include Tenacity

  t_belongs_to    :portfolio
  t_has_one       :project_manager
  t_has_many      :milestones
end

The project class now has the following methods (and more) to ease the traversal and manipulation of its relationships:

Cardinality and associations

Tenacity associations can be used to describe one-to-one and one-to-many relationships between models. Each model uses an association to describe its role in the relation. The t_belongs_to association is always used in the model that has the foreign key.

One-to-one

Use t_has_one in the base, and t_belongs_to in the associated model.

class Employee < ActiveRecord::Base
  include Tenacity
  t_has_one :office
end

class Office
  include MongoMapper::Document
  include Tenacity
  t_belongs_to :employee     # foreign key - employee_id
end

One-to-many

Use t_has_many in the base, and t_belongs_to in the associated model.

class Manager < ActiveRecord::Base
  include Tenacity
  t_has_many :employees
end

class Employee
  include MongoMapper::Document
  include Tenacity
  t_belongs_to :manager     # foreign key - manager_id
end

Is it a t_belongs_to or t_has_one association?

Both express a 1-1 relationship. The difference is mostly where to place the foreign key, which is owned by the class declaring the t_belongs_to relationship. Example:

class Employee < ActiveRecord::Base
  include Tenacity
  t_has_one :office
end

class Office
  include MongoMapper::Document
  include Tenacity
  t_belongs_to :employee
end

In this example, the foreign key, employee_id, would belong to the Office class. If possible, tenacity will define the property to hold the foreign key. When it cannot, it assumes that the foreign key has been defined. See the documentation for the respective database client extension to see if tenacity will declare the foreign_key property.

Unsaved objects and associations

You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be aware of, mostly involving the saving of associated objects.

Unless you set the :autosave option on a t_has_one, t_belongs_to, or t_has_many association. Setting it to true will always save the members, whereas setting it to false will never save the members.

One-to-one associations

Collections

Polymorphic Associations

Polymorphic associations on models are not restricted on what types of models they can be associated with. Rather, they specify an interface that a t_has_many association must adhere to.

class Asset < ActiveRecord::Base
  include Tenacity
  t_belongs_to :attachable, :polymorphic => true
end

class Post
  include MongoMapper::Document
  include Tenacity
  t_has_many :assets, :as => :attachable         # The :as option specifies the polymorphic interface to use.
end

@asset.attachable = @post

This works by using a type field/column in addition to a foreign key to specify the associated record. In the Asset example, you'd need an attachable_id field and an attachable_type string field on your model.

IDs for polymorphic associations are always stored as strings in the database, since we cannot determine the true type of the id when the association is defined.

Caching

All of the methods are built on a simple caching principle that will keep the result of the last query around unless specifically instructed not to. The cache is even shared across methods to make it even cheaper to use the macro-added methods without worrying too much about performance at the first go.

project.milestones             # fetches milestones from the database
project.milestones.size        # uses the milestone cache
project.milestones.empty?      # uses the milestone cache
project.milestones(true).size  # fetches milestones from the database
project.milestones             # uses the milestone cache

Public Instance Methods

_tenacity_associations() click to toggle source
# File lib/tenacity/class_methods.rb, line 429
def _tenacity_associations
  @_tenacity_associations || []
end
t_belongs_to(name, options={}) click to toggle source

Specifies a one-to-one association with another class. This method should only be used if this class contains the foreign key. If the other class contains the foreign key, then you should use t_has_one instead.

Methods will be added for retrieval and query for a single associated object, for which this object holds an id:

association(force_reload = false)

Returns the associated object. nil is returned if none is found.

association=(associate)

Assigns the associate object, extracts the primary key, and sets it as the foreign key.

(association is replaced with the symbol passed as the first argument, so t_belongs_to :author would add among others author.nil?.)

Example

A Post class declares t_belongs_to :author, which will add:

  • Post#author (similar to Author.find(author_id))

  • Post#author=(author) (similar to post.author_id = author.id)

Supported options

:class_name

Specify the class name of the association. Use it only if that name can't be inferred from the association name. So t_belongs_to :manager will by default be linked to the Manager class, but if the real class name is Person, you'll have to specify it with this option.

:foreign_key

Specify the foreign key used for the association. By default this is guessed to be the name of the association with an “_id” suffix. So a class that defines a t_belongs_to :person association will use “person_id” as the default :foreign_key. Similarly, t_belongs_to :favorite_person, :class_name => "Person" will use a foreign key of “favorite_person_id”.

:dependent

If set to :destroy, the associated object is deleted when this object is, calling all delete callbacks. If set to :delete, the associated object is deleted without calling any of its delete callbacks. This option should not be specified when t_belongs_to is used in conjuction with a t_has_many relationship on another class because of the potential to leave orphaned records behind.

:readonly

If true, the associated object is readonly through the association.

:autosave

If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.

:polymorphic

Specify this association is a polymorphic association by passing true. (Note: IDs for polymorphic associations are always stored as strings in the database.)

:disable_foreign_key_constraints

If true, bypass foreign key constraints, like verifying the target object exists when the relationship is created. Defaults to false.

Option examples:

t_belongs_to :project_manager, :class_name => "Person"
t_belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id"
t_belongs_to :project, :readonly => true
t_belongs_to :attachable, :polymorphic => true
# File lib/tenacity/class_methods.rb, line 271
def t_belongs_to(name, options={})
  extend(Associations::BelongsTo::ClassMethods)
  association = _t_create_association(:t_belongs_to, name, options)
  initialize_belongs_to_association(association)

  define_method(association.name) do |*params|
    get_associate(association, params) do
      belongs_to_associate(association)
    end
  end

  define_method("#{association.name}=") do |associate|
    set_associate(association, associate) do
      set_belongs_to_associate(association, associate)
    end
  end
end
t_has_many(name, options={}) click to toggle source

Specifies a one-to-many association.

The following methods for retrieval and query of collections of associated objects will be added:

collection(force_reload = false)

Returns an array of all the associated objects. An empty array is returned if none are found.

collection<<(object, …)

Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.

collection.push(object, …)

Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.

collection.concat(other_array)

Adds the objects in the other array to the collection by setting their foreign keys to the collection's primary key.

collection.delete(object, …)

Removes one or more objects from the collection by setting their foreign keys to NULL. Objects will be in addition deleted and callbacks called if they're associated with :dependent => :destroy, and deleted and callbacks skipped if they're associated with :dependent => :delete_all.

collection.destroy_all

Removes all objects from the collection, and deletes them from their respective database. If the deleted objects have any delete callbacks defined, they will be called.

collection.delete_all

Removes all objects from the collection, and deletes them from their respective database. No delete callbacks will be called, regardless of whether or not they are defined.

collection=objects

Replaces the collections content by setting it to the list of specified objects.

collection_singular_ids

Returns an array of the associated objects' ids

collection_singular_ids=ids

Replace the collection with the objects identified by the primary keys in ids.

collection.clear

Removes every object from the collection. This deletes the associated objects and issues callbacks if they are associated with :dependent => :destroy, deletes them directly from the database without calling any callbacks if :dependent => :delete_all, otherwise sets their foreign keys to NULL.

collection.empty?

Returns true if there are no associated objects.

collection.size

Returns the number of associated objects.

(Note: collection is replaced with the symbol passed as the first argument, so t_has_many :clients would add among others clients.empty?.)

Example

Example: A Firm class declares t_has_many :clients, which will add:

  • Firm#clients (similar to Clients.find :all, :conditions => ["firm_id = ?", id])

  • Firm#clients<<

  • Firm#clients.delete

  • Firm#clients=

  • Firm#client_ids

  • Firm#client_ids=

  • Firm#clients.clear

  • Firm#clients.empty? (similar to firm.clients.size == 0)

  • Firm#clients.size (similar to Client.count "firm_id = #{id}")

Supported options

:class_name

Specify the class name of the association. Use it only if that name can't be inferred from the association name. So t_has_many :products will by default be linked to the Product class, but if the real class name is SpecialProduct, you'll have to specify it with this option.

:foreign_key

Specify the foreign key used for the association. By default this is guessed to be the name of this class in lower-case and “_id” suffixed. So a Person class that makes a t_has_many association will use “person_id” as the default :foreign_key.

:dependent

If set to :destroy all the associated objects are deleted alongside this object in addition to calling their delete callbacks. If set to :delete_all all associated objects are deleted without calling their delete callbacks. If set to :nullify all associated objects' foreign keys are set to NULL without calling their save backs.

:readonly

If true, all the associated objects are readonly through the association.

:limit

An integer determining the limit on the number of rows that should be returned. Results are ordered by a string representation of the id.

:offset

An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows. Results are ordered by a string representation of the id.

:autosave

If true, always save any loaded members and destroy members marked for destruction, when saving the parent object. Off by default.

:as

Specifies a polymorphic interface (See t_belongs_to).

:disable_foreign_key_constraints

If true, bypass foreign key constraints, like verifying no other objects are storing the key of the source object before deleting it. Defaults to false.

Option examples:

t_has_many :products, :class_name => "SpecialProduct"
t_has_many :engineers, :foreign_key => "project_id"  # within class named SecretProject
t_has_many :tasks, :dependent => :destroy
t_has_many :reports, :readonly => true
t_has_many :tags, :as => :taggable
# File lib/tenacity/class_methods.rb, line 383
def t_has_many(name, options={})
  extend(Associations::HasMany::ClassMethods)
  association = _t_create_association(:t_has_many, name, options)
  initialize_has_many_association(association)

  define_method(association.name) do |*params|
    get_associate(association, params) do
      has_many_associates(association)
    end
  end

  define_method("#{association.name}=") do |associates|
    _t_mark_dirty if respond_to?(:_t_mark_dirty)
    set_associate(association, associates) do
      set_has_many_associates(association, associates)
    end
  end

  define_method("#{ActiveSupport::Inflector.singularize(association.name.to_s)}_ids") do
    has_many_associate_ids(association)
  end

  define_method("#{ActiveSupport::Inflector.singularize(association.name.to_s)}_ids=") do |associate_ids|
    _t_mark_dirty if respond_to?(:_t_mark_dirty)
    set_has_many_associate_ids(association, associate_ids)
  end

  private

  define_method(:_t_save_without_callback) do
    save_without_callback
  end
end
t_has_one(name, options={}) click to toggle source

Specifies a one-to-one association with another class. This method should only be used if the other class contains the foreign key. If the current class contains the foreign key, then you should use t_belongs_to instead.

The following methods for retrieval and query of a single associated object will be added:

association(force_reload = false)

Returns the associated object. nil is returned if none is found.

association=(associate)

Assigns the associate object, extracts the primary key, sets it as the foreign key, and saves the associate object.

(association is replaced with the symbol passed as the first argument, so t_has_one :manager would add among others manager.nil?.)

Example

An Account class declares t_has_one :beneficiary, which will add:

  • Account#beneficiary (similar to Beneficiary.find(:first, :conditions => "account_id = #{id}"))

  • Account#beneficiary=(beneficiary) (similar to beneficiary.account_id = account.id; beneficiary.save)

Supported options

:class_name

Specify the class name of the association. Use it only if that name can't be inferred from the association name. So t_has_one :manager will by default be linked to the Manager class, but if the real class name is Person, you'll have to specify it with this option.

:foreign_key

Specify the foreign key used for the association. By default this is guessed to be the name of this class in lower-case and “_id” suffixed. So a Person class that makes a t_has_one association will use “person_id” as the default :foreign_key.

:dependent

If set to :destroy, the associated object is deleted when this object is, and all delete callbacks are called. If set to :delete, the associated object is deleted without calling any of its delete callbacks. If set to :nullify, the associated object's foreign key is set to NULL.

:readonly

If true, the associated object is readonly through the association.

:autosave

If true, always save the associated object or destroy it if marked for destruction, when saving the parent object. Off by default.

:as

Specifies a polymorphic interface (See t_belongs_to).

:disable_foreign_key_constraints

If true, bypass foreign key constraints, like verifying no other objects are storing the key of the source object before deleting it. Defaults to false.

Option examples:

t_has_one :credit_card, :dependent => :destroy  # destroys the associated credit card
t_has_one :credit_card, :dependent => :nullify  # updates the associated records foreign key value to NULL rather than destroying it
t_has_one :project_manager, :class_name => "Person"
t_has_one :project_manager, :foreign_key => "project_id"  # within class named SecretProject
t_has_one :boss, :readonly => :true
t_has_one :attachment, :as => :attachable
# File lib/tenacity/class_methods.rb, line 198
def t_has_one(name, options={})
  extend(Associations::HasOne::ClassMethods)
  association = _t_create_association(:t_has_one, name, options)
  initialize_has_one_association(association)

  define_method(association.name) do |*params|
    get_associate(association, params) do
      has_one_associate(association)
    end
  end

  define_method("#{association.name}=") do |associate|
    set_associate(association, associate) do
      set_has_one_associate(association, associate)
    end
  end
end

Private Instance Methods

_t_create_association(type, name, options) click to toggle source
# File lib/tenacity/class_methods.rb, line 435
def _t_create_association(type, name, options) #:nococ:
  association = Association.new(type, name, self, options)
  @_tenacity_associations ||= []
  @_tenacity_associations << association
  association
end