class Relationship

Summay:

This class stores the relationship between any two objects. It will store one of the objects

as :base_type and :base_id, and the other object as :related_type and :related_id. When you create a new relationship it will sort the objects first by type (class name) then by ID, this allows us to validate the uniqueness of the relationship and improves certain types of lookups.

The two main methods for looking up related objects are `related_objects` and `grouped_related_objects`.

You can see the comments above them for more details, but what they return are a collection of RelatedObject objects. I was returning hashes but creating a simple wrapper class better defines what gets returned. Those methods will first grab the relationships for a given object, then convert them into RelatedObjects, then load the actual object they reference into each RelatedObject.

Example of what a RelatedObject is

relationship.related_object(object_a)
=> <RelatedObject
      object_type   = "related_type",
      object_id     = "related_id",
      relationships = <Relationship>
      object        = <object_b>

Constants

CLEAN_TYPES

Attributes

base_object[W]

Public Class Methods

find_by_objects(base_object, related_object) click to toggle source

Lookup a relationship from two given objects

# File lib/buweb/relationship.rb, line 75
def self.find_by_objects(base_object, related_object)
  return nil if base_object.blank? || related_object.blank?
  self.where({
    base_type: base_object.class.to_s,
    base_id: base_object.id.to_s,
    related_type: related_object.class.to_s,
    related_id: related_object.id.to_s
  }).first
end
grouped_relationships(relationships: [], group_by: :related_type) click to toggle source

Returns an array of Relationships grouped by type Example {

"PageEdition": [<Relationship>]

}

# File lib/buweb/relationship.rb, line 113
def self.grouped_relationships(relationships: [], group_by: :related_type)
  Hash.new.tap do |group|
    # First group all the relationships by type and index by id
    relationships.each do |relationship|
      if group_by == :related_type
        type_key, id_key = [relationship.related_type, relationship.related_id]
      elsif group_by == :base_type
        type_key, id_key = [relationship.base_type, relationship.base_id]
      else
        raise "group_by is not valid - #{group_by}"
      end

      group[type_key] ||= {}
      group[type_key][id_key] ||= []
      group[type_key][id_key].push(relationship)
    end

    # Then we want to load the real objects into each related_object.
    # We lookup all the objects for each type by their ID, then loop through
    # each result and assign the object to their related_object in `group`.
    # Going into it `group` will look something like this
    # {
    #   "PageEdition": {
    #     "558c4b507275622fcf000002": [<Relationship>]
    #   },
    #   "Department" : {...}
    # }
    group.each do |type, id_groupings|
      if klass = type.safe_constantize
        klass.where({id: { '$in' => id_groupings.keys }}).each do |real_object|
          Array(group[type][real_object.id.to_s]).each do |relationship|
            if group_by == :related_type
              relationship.related_object = real_object
            elsif group_by == :base_type
              relationship.base_object = real_object
            end
          end
        end
      end
    end

    # Remove the id indexing so there is just an array of relationships
    # for each type
    group.each do |type, id_groupings|
      group[type] = id_groupings.values.flatten
    end
  end
end
load_objects(group_by: :related_type) click to toggle source

By default will load all the related objects into the relationships and return arrays of relationships grouped by related_type Example:

{

"PageEdition": [<Relationship>, <Relationship>],
"Department": [<Relationship>]

}

# File lib/buweb/relationship.rb, line 93
def self.load_objects(group_by: :related_type)
  grouped_relationships(relationships: scoped, group_by: group_by)
end
new_from_objects(base_object, related_object) click to toggle source

Will create a new relationship between two given objects.

# File lib/buweb/relationship.rb, line 64
def self.new_from_objects(base_object, related_object)
  return nil if base_object.blank? || related_object.blank?
  self.new({
    base_type: base_object.class.to_s,
    base_id: base_object.id.to_s,
    related_type: related_object.class.to_s,
    related_id: related_object.id.to_s
  })
end

Public Instance Methods

base_object() click to toggle source
# File lib/buweb/relationship.rb, line 104
def base_object
  @base_object ||= object!(base_type, base_id)
end

Private Instance Methods

object!(klass, id) click to toggle source
# File lib/buweb/relationship.rb, line 164
def object!(klass, id)
  klass = CLEAN_TYPES.find { |k| k == klass }.try(:safe_constantize)
  return unless id && klass
  klass.where(id: id).first
end
validate_different_objects() click to toggle source

This will also ensure that base_object and related_object aren't the same object.

# File lib/buweb/relationship.rb, line 187
def validate_different_objects
  return unless base_type == related_type && base_id == related_id
  errors.add(:base, 'Cannot relate an object to itself')
  false
end
validate_objects_exist() click to toggle source
# File lib/buweb/relationship.rb, line 170
def validate_objects_exist
  return if base_object && related_object
  errors.add(:base, 'Related object doesn\'t exist')
  false
end
validate_relationship_unique() click to toggle source
# File lib/buweb/relationship.rb, line 176
def validate_relationship_unique
  return unless Relationship.where(
    base_type: base_type, base_id: base_id,
    related_type: related_type, related_id: related_id
  ).present?
  errors.add(:base, 'Relationship already exists')
  false
end