Class: Array

Inherits:
Object
  • Object
show all
Defined in:
lib/simply_couch/include_relation.rb

Overview

This file is a tool extension to simply stored. Its intention is to not change anything of an existing implementation, but to hugely speedup existing implementations. I will illustrate this given the example relation types:

Person has_many posts and belongs to a group, Post has many comments, Comment belongs to Writer

If for a reason you have one page where you want to display all of these objects (Person, Post, Comment) you can ofcourse create a view returning all these objects with a smart key for some handy selection. This will probably end up in a controller implementation:

@persons = view_result.select{|r| r.is_a?(Person)}
@posts = view_result.select{|r| r.is_a?(Post)}
@comments = view_result.select{|r| r.is_a?(Comment)}
@writers = view_result.select{|r| r.is_a?(Writer)}

This probably is the recommended way of solving problems in most cases, but sometimes because you are lazy or some other obscure reason, you want to use the standard SimplyCouch behaviour, but not wait too long. For example, if I have a list of 40 persons, all having 5 posts that all have 10 comments belonging to a writer, getting all these through their standard relations:

@persons.each{ |person| person.posts.each{ |post| post.comments.each{ |comment| puts person.group.name + comment.writer.name } } }

This will result in 40 * 5 * 10 + 40 = 2040 queries to the database. Doing exactly the same thing using this script will look like:

@persons = Person.all.include_relation( :group, posts: { conmments: :writer } )

The useless script above will not take 2040 queries but:

1 (persons) + 1 (group) + 1 (posts) + 1 (comments) + 1 (write) = 5 queries

This makes a difference. Issues:

  • Supported relation types:

    • has_many

    • belongs_to

  • belongs_to relations, that have no value (nil) will be queried again. That would make the calculation above: 5 + number of persons without a group + number of comments without a writer

  • Little test coverage

Instance Method Summary collapse

Instance Method Details

#include_relation(*relations_arg) ⇒ Object

Raises:

  • (ArgumentError)


33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/simply_couch/include_relation.rb', line 33

def include_relation(*relations_arg)
  return self if empty?
  relations = {}
  database = nil
  database = relations_arg.last.delete(:database) if relations_arg.last.is_a?(Hash) and relations_arg.last.has_key?(:database)
  database ||= self.first&.class&.database
  raise ArgumentError, "Cannot include relations without a database — pass :database option or ensure models respond to .database" unless database

  # Make sure relations is a Hash, process up to two levels for recursion
  # keys with value nil will not have a followup
  relations_arg.each do |arg|
    if arg.is_a?(Symbol)
      relations[arg] = nil
    elsif arg.is_a?(Hash)
      arg.each{|k, v| relations[k] = v}
    elsif arg.is_a?(Array)
      arg.each do |v|
        if arg.is_a?(Symbol)
          relations[v] = nil
        elsif arg.is_a?(Hash)
          arg.each{|k, v| relations[k] = v}
        end
      end
    end
  end

  # For now, assume an array of only one datatype
  klass = first.class

  relations.each do |relation, followup|
    property = klass.properties.find{|p| p.name == relation}
    unless property
      warn "Attempt to include_relations #{relation} on #{klass.name} but does not have supporting relation", uplevel: 1
      next
    end
    case property
    when SimplyCouch::Model::HasMany::Property then
      other_class = property.options[:class_name].constantize
      other_property = other_class.properties.find{|p| p.is_a?(SimplyCouch::Model::BelongsTo::Property) && p.options[:class_name] == klass.name}
      #TODO riase when soft_delete is enabled
      view_name = "by_#{other_property.name}_id"
      raise "Cannot include has_many relation #{other_class.name.underscore.pluralize} on #{klass.name} when view :#{view_name}, key: :#{other_property.name}_id is not defined on #{other_class.name}" unless other_class.views[view_name].present?
      relation_objects = other_class.database.view(other_class.send(view_name, keys: collect(&:id))) #not working yet
      if followup # deeper nested including
        case followup
        when Hash
          then relation_objects.include_relation(followup.merge(database: database))
        else
          relation_objects.include_relation(*(Array.wrap(followup) + [{database: database}]))
        end
      end

      for obj in self
        found_relation_objects = relation_objects.select{|r| r.send("#{other_property.name}_id") == obj.id}

        # Make sure every object has a cached value, no more loading is done
        obj.instance_variable_set("@#{relation}", {all: []}) unless obj.instance_variable_get("@#{relation}").try('[]', :all)
        if found_relation_objects.any?
          obj.instance_variable_get("@#{relation}")[:all] |= found_relation_objects
          if reverse_property_name = other_class.properties.find{|p| p.is_a?(SimplyCouch::Model::BelongsTo::Property) && p.options[:class_name] == klass.name }.try(:name)
            found_relation_objects.each{|relation_object| relation_object.instance_variable_set("@#{reverse_property_name}", obj)}
          end
        end
      end
    when SimplyCouch::Model::BelongsTo::Property then
      key = "#{relation}_id"
      # Collect keys for all objects
      keys = []
      each do |obj|
        next unless obj.is_a?(SimplyCouch::Model) && obj.respond_to?(key)
        keys << obj.send(key)
      end

      # Get from the database
      relation_objects = database.couchrest_database.bulk_load(keys.compact.uniq)
      relation_objects = Array.wrap(relation_objects['rows']).map{|r| r['doc']}.compact if relation_objects.is_a?(Hash)
      relation_objects ||= [] # Ensure array datatype
      if followup # deeper nested including
        case followup
        when Hash
          then relation_objects.include_relation(followup.merge(database: database))
        else
          relation_objects.include_relation(*(Array.wrap(followup) + [{database: database}]))
        end
      end

      # Set to attributes
      each do |obj|
        obj.instance_variable_set("@#{relation}", relation_objects.find{|o| o.id == obj.send(key)})
      end
    when SimplyCouch::Model::HasAndBelongsToMany::Property
      if property.options[:storing_keys]
        key = "#{relation.to_s.singularize}_ids"
        # Collect relation ids for all objects
        relation_ids = []
        each do |obj|
          next unless obj.is_a?(SimplyCouch::Model) && obj.respond_to?(key) && obj.send(key).present?
          relation_ids += obj.send(key)
        end
        # Create unique list of ids, this will optimize stuff and synchronize the object ids
        relation_ids = relation_ids.flatten.compact.uniq

        # Get from the database
        relation_objects = database.couchrest_database.bulk_load(relation_ids)
        relation_objects = Array.wrap(relation_objects['rows']).map{|r| r['doc']}.compact if relation_objects.is_a?(Hash)
        relation_objects ||= [] # Ensure array datatype
        each do |obj|
          obj.instance_variable_set("@#{relation}", {all: relation_objects.select{|o| Array.wrap(obj.send(key)).include?(o.id)}})
        end
        if followup # deeper nested including
          case followup
          when Hash
            then relation_objects.include_relation(followup.merge(database: database))
          else
            relation_objects.include_relation(*(Array.wrap(followup) + [{database: database}]))
          end
        end
      end
    end
  end
  self
end

#include_relations(*args) ⇒ Object

Alias method as plural form



157
158
159
# File 'lib/simply_couch/include_relation.rb', line 157

def include_relations(*args)
  include_relation(*args)
end