Module: VirtualBox::AbstractModel::Relatable

Defined in:
lib/virtualbox/abstract_model/relatable.rb

Overview

Provides simple relationship features to any class. These relationships can be anything, since this module makes no assumptions and doesn't differentiate between "has many" or "belongs to" or any of that.

The way it works is simple:

  1. Relationships are defined with a relationship name and a class of the relationship objects.
  2. When #populate_relationships is called, populate_relationship is called on each relationship class (example: StorageController.populate_relationship). This is expected to return the relationship, which can be any object.
  3. When #save_relationships is called, save_relationship is called on each relationship class, which manages saving its own relationship.
  4. When #destroy_relationships is called, destroy_relationship is called on each relationship class, which manages destroying its own relationship.

Be sure to read ClassMethods for complete documentation of methods.

Defining Relationships

Every relationship has two mandatory parameters: the name and the class.

relationship :bacons, Bacon

In this case, there is a relationship bacons which refers to the Bacon class.

Accessing Relationships

Relatable offers up dynamically generated accessors for every relationship which simply returns the relationship data.

relationship :bacons, Bacon

# Accessing through an instance "instance"
instance.bacons # => whatever Bacon.populate_relationship created

Settable Relationships

It is often convenient that relationships become "settable." That is, for a relationship foos, there would exist a foos= method. This is possible by implementing the set_relationship method on the relationship class. Consider the following relationship:

relationship :foos, Foo

If Foo has the set_relationship method, then it will be called by foos=. It is expected to return the new value for the relationship. To facilitate this need, the set_relationship method is given three parameters: caller, old value, and new value. An example implementation, albeit a silly one, is below:

class Foo
  def self.set_relationship(caller, old_value, new_value)
    return "Changed to: #{new_value}"
  end
end

In this case, the following behavior would occur:

instance.foos # => assume "foo"
instance.foos = "bar"
instance.foos # => "Changed to: bar"

If the relationship class does not implement the set_relationship method, then a Exceptions::NonSettableRelationshipException will be raised if a user attempts to set that relationship.

Dependent Relationships

By setting :dependent => :destroy on relationships, AbstractModel will automatically call #destroy_relationships when AbstractModel#destroy is called.

This is not a feature built-in to Relatable but figured it should be mentioned here.

Lazy Relationships

Often, relationships are pretty heavy things to load. Data may have to be retrieved, classes instantiated, etc. If a class has many relationships, or many relationships within many relationships, the time and memory required for relationships really begins to add up. To address this issue, lazy relationships are available. Lazy relationships defer loading their content until the last possible moment, or rather, when a user requests the data. By specifing the :lazy => true option to relationships, relationships will not be loaded immediately. Instead, when they're first requested, load_relationship will be called on the model, with the name of the relationship given as a parameter. It is up to this method to call #populate_relationship at some point with the data to setup the relationship. An example follows:

class SomeModel
  include VirtualBox::AbstractModel::Relatable

  relationship :foos, Foo, :lazy => true

  def load_relationship(name)
    if name == :foos
      populate_relationship(name, get_data_for_a_long_time)
    end
  end
end

Using the above class, we can use it like so:

model = SomeModel.new

# This initial load takes awhile as it loads...
model.foos

# Instant! (Just a hash lookup. No load necessary)
model.foos

One catch: If a model attempts to destroy a lazy relationship, it will first load the relationship, since destroy typically depends on some data of the relationship.

Defined Under Namespace

Modules: ClassMethods

Class Method Summary

Instance Method Summary

Class Method Details

+ (Object) included(base)



122
123
124
# File 'lib/virtualbox/abstract_model/relatable.rb', line 122

def self.included(base)
  base.extend ClassMethods
end

Instance Method Details

- (Object) destroy_relationship(name, *args)

Destroys only a single relationship. Any arbitrary args may be added to the end and they will be pushed through to the class's destroy_relationship method.

Parameters:

  • (Symbol) name — The name of the relationship


245
246
247
248
249
250
251
252
253
254
# File 'lib/virtualbox/abstract_model/relatable.rb', line 245

def destroy_relationship(name, *args)
  options = self.class.relationships_hash[name]
  return unless options && options[:klass].respond_to?(:destroy_relationship)

  # Read relationship, which forces lazy relationships to load, which is
  # probably necessary for destroying
  read_relationship(name)

  options[:klass].destroy_relationship(self, relationship_data[name], *args)
end

- (Object) destroy_relationships(*args)

Calls destroy_relationship on each of the relationships. Any arbitrary args may be added and they will be forarded to the relationship's destroy_relationship method.



234
235
236
237
238
# File 'lib/virtualbox/abstract_model/relatable.rb', line 234

def destroy_relationships(*args)
  self.class.relationships.each do |name, options|
    destroy_relationship(name, *args)
  end
end

- (Boolean) has_relationship?(key)

Returns boolean denoting if a relationship exists.

Returns:

  • (Boolean)


267
268
269
# File 'lib/virtualbox/abstract_model/relatable.rb', line 267

def has_relationship?(key)
  self.class.has_relationship?(key.to_sym)
end

- (Boolean) lazy_relationship?(key)

Returns boolean denoting if a relationship is to be lazy loaded.

Returns:

  • (Boolean)


274
275
276
277
# File 'lib/virtualbox/abstract_model/relatable.rb', line 274

def lazy_relationship?(key)
  options = self.class.relationships_hash[key.to_sym]
  !options.nil? && options[:lazy]
end

- (Boolean) loaded_relationship?(key)

Returns boolean denoting if a relationship has been loaded.

Returns:

  • (Boolean)


280
281
282
# File 'lib/virtualbox/abstract_model/relatable.rb', line 280

def loaded_relationship?(key)
  relationship_data.has_key?(key)
end

- (Object) populate_relationship(name, data)

Populate a single relationship.



225
226
227
228
229
# File 'lib/virtualbox/abstract_model/relatable.rb', line 225

def populate_relationship(name, data)
  options = self.class.relationships_hash[name]
  return unless options[:klass].respond_to?(:populate_relationship)
  relationship_data[name] = options[:klass].populate_relationship(self, data)
end

- (Object) populate_relationships(data)

The equivalent to Attributable#populate_attributes, but with relationships.



218
219
220
221
222
# File 'lib/virtualbox/abstract_model/relatable.rb', line 218

def populate_relationships(data)
  self.class.relationships.each do |name, options|
    populate_relationship(name, data) unless lazy_relationship?(name)
  end
end

- (Object) read_relationship(name)

Reads a relationship. This is equivalent to Attributable#read_attribute, but for relationships.



183
184
185
186
187
188
189
# File 'lib/virtualbox/abstract_model/relatable.rb', line 183

def read_relationship(name)
  if lazy_relationship?(name) && !loaded_relationship?(name)
    load_relationship(name)
  end

  relationship_data[name.to_sym]
end

- (Hash) relationship_data

Hash to data associated with relationships. You should instead use the accessors created by Relatable.

Returns:

  • (Hash)


260
261
262
# File 'lib/virtualbox/abstract_model/relatable.rb', line 260

def relationship_data
  @relationship_data ||= {}
end

- (Object) save_relationship(name, *args)

Saves a single relationship. It is up to the relationship class to determine whether anything changed and how saving is implemented. Simply calls save_relationship on the relationship class.



210
211
212
213
214
# File 'lib/virtualbox/abstract_model/relatable.rb', line 210

def save_relationship(name, *args)
  options = self.class.relationships_hash[name]
  return unless options[:klass].respond_to?(:save_relationship)
  options[:klass].save_relationship(self, relationship_data[name], *args)
end

- (Object) save_relationships(*args)

Saves the model, calls save_relationship on all relations. It is up to the relation to determine whether anything changed, etc. Simply calls save_relationship on each relationship class passing in the following parameters:

  • caller - The class which is calling save
  • data - The data associated with the relationship

In addition to those two args, any arbitrary args may be tacked on to the end and they'll be pushed through to the save_relationship method.



201
202
203
204
205
# File 'lib/virtualbox/abstract_model/relatable.rb', line 201

def save_relationships(*args)
  self.class.relationships.each do |name, options|
    save_relationship(name, *args)
  end
end

- (Object) set_relationship(key, value)

Sets a relationship to the given value. This is not guaranteed to do anything, since "set_relationship" will be called on the class that the relationship is associated with and its expected to return the resulting relationship to set.

If the relationship class doesn't respond to the set_relationship method, then an exception Exceptions::NonSettableRelationshipException will be raised.

This method is called by the "magic" method of relationship=.

Parameters:

  • (Symbol) key — Relationship key.
  • (Object) value — The new value of the relationship.

Raises:



297
298
299
300
301
302
303
304
# File 'lib/virtualbox/abstract_model/relatable.rb', line 297

def set_relationship(key, value)
  key = key.to_sym
  relationship = self.class.relationships_hash[key]
  return unless relationship

  raise Exceptions::NonSettableRelationshipException.new unless relationship[:klass].respond_to?(:set_relationship)
  relationship_data[key] = relationship[:klass].set_relationship(self, relationship_data[key], value)
end