Module: VirtualBox::AbstractModel::Relatable

Includes:
VersionMatcher
Included in:
VirtualBox::AbstractModel
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, VirtualBox::AbstractModel will automatically call #destroy_relationships when #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 collapse

Instance Method Summary collapse

Methods included from VersionMatcher

#assert_version_match, #split_version, #version_match?

Class Method Details

.included(base) ⇒ Object



126
127
128
# File 'lib/virtualbox/abstract_model/relatable.rb', line 126

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

Instance Method Details

#destroy_relationship(name, *args) ⇒ Object

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:

  • name (Symbol)

    The name of the relationship



259
260
261
262
263
264
265
266
267
268
# File 'lib/virtualbox/abstract_model/relatable.rb', line 259

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

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

  relationship_class(name).destroy_relationship(self, relationship_data[name], *args)
end

#destroy_relationships(*args) ⇒ Object

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.



248
249
250
251
252
# File 'lib/virtualbox/abstract_model/relatable.rb', line 248

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

#has_relationship?(key) ⇒ Boolean

Returns boolean denoting if a relationship exists.

Returns:

  • (Boolean)


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

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

#lazy_relationship?(key) ⇒ Boolean

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

Returns:

  • (Boolean)


288
289
290
291
# File 'lib/virtualbox/abstract_model/relatable.rb', line 288

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

#loaded_relationship?(key) ⇒ Boolean

Returns boolean denoting if a relationship has been loaded.

Returns:

  • (Boolean)


294
295
296
# File 'lib/virtualbox/abstract_model/relatable.rb', line 294

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

#populate_relationship(name, data) ⇒ Object

Populate a single relationship.



238
239
240
241
242
243
# File 'lib/virtualbox/abstract_model/relatable.rb', line 238

def populate_relationship(name, data)
  options = self.class.relationships_hash[name]
  return unless relationship_class(name).respond_to?(:populate_relationship)
  return if options[:version] && !version_match?(options[:version], VirtualBox.version)
  relationship_data[name] = relationship_class(name).populate_relationship(self, data)
end

#populate_relationships(data) ⇒ Object

The equivalent to Attributable#populate_attributes, but with relationships.



231
232
233
234
235
# File 'lib/virtualbox/abstract_model/relatable.rb', line 231

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

#read_relationship(name) ⇒ Object

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



187
188
189
190
191
192
193
194
195
196
# File 'lib/virtualbox/abstract_model/relatable.rb', line 187

def read_relationship(name)
  options = self.class.relationships_hash[name.to_sym]
  assert_version_match(options[:version], VirtualBox.version) if options[:version]

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

  relationship_data[name.to_sym]
end

#relationship_class(key) ⇒ Class

Returns the class for a given relationship. This method handles converting a string/symbol into the proper class.

Returns:

  • (Class)


302
303
304
305
306
307
# File 'lib/virtualbox/abstract_model/relatable.rb', line 302

def relationship_class(key)
  options = self.class.relationships_hash[key.to_sym]
  klass = options[:klass]
  klass = Object.module_eval("#{klass}") unless klass.is_a?(Class)
  klass
end

#relationship_dataHash

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

Returns:

  • (Hash)


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

def relationship_data
  @relationship_data ||= {}
end

#save_relationship(name, *args) ⇒ Object

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.



221
222
223
224
225
226
227
# File 'lib/virtualbox/abstract_model/relatable.rb', line 221

def save_relationship(name, *args)
  options = self.class.relationships_hash[name]
  return if lazy_relationship?(name) && !loaded_relationship?(name)
  return if options[:version] && !version_match?(options[:version], VirtualBox.version)
  return unless relationship_class(name).respond_to?(:save_relationship)
  relationship_class(name).save_relationship(self, relationship_data[name], *args)
end

#save_relationships(*args) ⇒ Object

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.



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

def save_relationships(*args)
  # Can't use `all?` here since it short circuits
  results = self.class.relationships.collect do |data|
    name, options = data
    !!save_relationship(name, *args)
  end

  !results.include?(false)
end

#set_relationship(key, value) ⇒ Object

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:

  • key (Symbol)

    Relationship key.

  • value (Object)

    The new value of the relationship.

Raises:



322
323
324
325
326
327
328
329
# File 'lib/virtualbox/abstract_model/relatable.rb', line 322

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

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