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



270
271
272
273
274
275
276
277
278
279
# File 'lib/virtualbox/abstract_model/relatable.rb', line 270

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.



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

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)


292
293
294
# File 'lib/virtualbox/abstract_model/relatable.rb', line 292

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)


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

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)


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

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

#populate_relationship(name, data) ⇒ Object

Populate a single relationship.



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

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.



242
243
244
245
246
# File 'lib/virtualbox/abstract_model/relatable.rb', line 242

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.



198
199
200
201
202
203
204
205
206
207
# File 'lib/virtualbox/abstract_model/relatable.rb', line 198

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)


313
314
315
316
317
318
# File 'lib/virtualbox/abstract_model/relatable.rb', line 313

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)


285
286
287
# File 'lib/virtualbox/abstract_model/relatable.rb', line 285

def relationship_data
  @relationship_data ||= {}
end

#reset_relationshipsObject

Resets a relationship. This resets all lazy relationships.



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

def reset_relationships
  to_delete = []
  relationship_data.each do |key, value|
    to_delete << key if lazy_relationship?(key)
  end

  # Delete them!
  to_delete.each { |key| relationship_data.delete(key) }
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.



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

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.



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

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:



333
334
335
336
337
338
339
340
# File 'lib/virtualbox/abstract_model/relatable.rb', line 333

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