index_tree 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 331b4073422929cabfbb5292d94d11bd1aca0e80
4
+ data.tar.gz: 20958ba592b248ed9137ddac5b72f9f2f1b8cc89
5
+ SHA512:
6
+ metadata.gz: 7f0b87af9ac9a7d5be4921f588464bec77655f1abb75ebefa7f38bf63fc9c4cf14897b940941defdf89b56c6f79565e9fa4f3c5fab398c35542f9fae0bc3aa4d
7
+ data.tar.gz: 18ea0a9f0a72749582ba0bd6713e104d7ff577819afc49526070078de54a1353ab40563812f8349c7457d7aa3a071ec07fd8b8af4b1a073c73e0bcf6df2d16b0
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /.idea
4
+ /Gemfile.lock
5
+ /_yardoc/
6
+ /coverage/
7
+ /doc/
8
+ /pkg/
9
+ /spec/reports/
10
+ /tmp/
11
+ *.bundle
12
+ *.so
13
+ *.o
14
+ *.a
15
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in index-tree.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Natural-Intelligence
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Natural Intelligence
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # IndexTree
2
+
3
+ This Gem eager loads trees by indexing the nodes of the tree. The number of queries needed to load a tree is N,
4
+ when N is the number of different models(ActiveRecords) in the tree.
5
+
6
+ Each inner object in the tree have an index node instance that is connecting it to the root.
7
+ When the root of the tree is loaded, only the objects that are in the tree are fetched(Pruning).
8
+ The index nodes are created when the root element is saved.
9
+
10
+ Example:
11
+
12
+ class Equation
13
+ acts_as_indexed_node :root => true do
14
+ has_many :expressions
15
+ end
16
+
17
+ has_one :not_tree_association_a
18
+ end
19
+
20
+
21
+ class Expression
22
+ acts_as_indexed_node do
23
+ has_many :expressions
24
+ end
25
+
26
+ has_one :not_tree_association_b
27
+ end
28
+
29
+ +----------+ +----------+
30
+ |Equation 1| |Equation 2|
31
+ +-+------+-+ +-+------+-+
32
+ | | | |
33
+ +-------+ +-------+ +-------+ +-------+
34
+ | | | |
35
+ v v v v
36
+ +-----------+ +-----------+ +-----------+ +-----------+
37
+ |Expression1| |Expression2| |Expression5| |Expression6|
38
+ +-----------+ +-+-------+-+ +-----------+ +-+-------+-+
39
+ | | | |
40
+ +-------+ +-------+ +-------+ +-------+
41
+ | | | |
42
+ v v v v
43
+ +-----------+ +-----------+ +-----------+ +-----------+
44
+ |Expression3| |Expression4| |Expression7| |Expression8|
45
+ +-----------+ +-----------+ +-----------+ +-----------+
46
+
47
+ The following statement fetches only the objects in the Equation1 tree in two queries:
48
+
49
+ Equation.find(1).preload_tree
50
+
51
+ One query to fetch Equations, and the second query is to fetch Expressions(Doesn't matter how deep is the tree it is still one query)
52
+
53
+
54
+ ## Installation
55
+
56
+ Add this line to your application's Gemfile:
57
+
58
+ ```ruby
59
+ gem 'index_tree'
60
+ ```
61
+
62
+ And then execute:
63
+
64
+ $ bundle
65
+ $ rake db:migrate
66
+
67
+ There is a migration which creates index_tree_index_node table
68
+
69
+ ## Declaration
70
+
71
+ All the models should be loaded in the Rails application, before using the preload_tree.
72
+
73
+ class RootNode < ActiveRecord::Base
74
+ acts_as_indexed_node :root => true do
75
+ has_many :child_nodes, dependent: :destroy
76
+ end
77
+ end
78
+
79
+ ### The following associations are supported:
80
+
81
+ belongs_to
82
+ belongs_to :class, polymorphic: true
83
+ has_one
84
+ has_many
85
+
86
+ ### The following types of inheritance are supported:
87
+
88
+ STI
89
+ Polymorphic associations
90
+
91
+ ### Options:
92
+
93
+ :root Used to declare a root model(default is false)
94
+
95
+ ## Usage
96
+
97
+ RootModel.find(1).preload_tree
98
+ RootModel.all.preload_tree
99
+ RootModel.where(color: 'red').all.preload_tree
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -0,0 +1,7 @@
1
+ # Used to for indexing the nodes in tree
2
+ module IndexTree
3
+ class IndexNode < ActiveRecord::Base
4
+ belongs_to :root_element, polymorphic: true
5
+ belongs_to :node_element, polymorphic: true
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ class CreateIndexTreeIndexNodes < ActiveRecord::Migration
2
+ def change
3
+ create_table :index_tree_index_nodes do |t|
4
+ t.references :root_element, polymorphic: true, :null => false
5
+ t.references :node_element, polymorphic: true, :null => false
6
+ t.timestamps
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ class AddIndexToTreeIndexNodes < ActiveRecord::Migration
2
+ def change
3
+ add_index :index_tree_index_nodes, [:root_element_id, :root_element_type], :name => 'index_index_tree_index_nodes_on_root_element'
4
+ end
5
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'index_tree/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "index_tree"
8
+ spec.version = IndexTree::VERSION
9
+ spec.authors = ["Alex Stanovsky"]
10
+ spec.email = %w(info@naturalint.com)
11
+ spec.homepage = 'http://www.naturalint.com'
12
+
13
+ spec.description = %q{Eager loads trees by indexing the nodes of the tree. The number of queries needed to load a tree is N,
14
+ when N is number of different models(ActiveRecords) in the tree}
15
+
16
+ spec.summary = %q{This Gem eager loads trees by indexing the nodes of the tree. The number of queries needed to load a tree is N,
17
+ when N is number of different models(ActiveRecords) in the tree.
18
+ Each inner object in the tree have an index node instance that is connecting it to the root.
19
+ When the root of the tree is loaded, only the objects that are in the tree are fetched(Pruning).
20
+ The index nodes are created when the root element is saved.}
21
+
22
+ spec.files = `git ls-files`.split($/)
23
+ spec.require_paths = %w(lib app)
24
+
25
+ spec.add_dependency "activerecord"
26
+ end
@@ -0,0 +1,20 @@
1
+ module IndexTree
2
+ module ActsAsIndexedNode
3
+ def acts_as_indexed_node(options={}, &block)
4
+ if options[:root]
5
+ include IndexTree::RootElement
6
+ else
7
+ include IndexTree::NodeElement
8
+ end
9
+
10
+ # Find what associations were defined in the given block
11
+ # And set the child nodes
12
+ if block_given?
13
+ current_associations = reflections.keys
14
+ yield
15
+ associations_in_block = reflections.keys - current_associations
16
+ child_nodes *associations_in_block
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ # Rails Engine copies the migration to the Rails app
2
+ module IndexTree
3
+ class Engine < Rails::Engine
4
+ isolate_namespace IndexTree
5
+
6
+ initializer :append_migrations do |app|
7
+ unless app.root.to_s.match root.to_s
8
+ config.paths["db/migrate"].expanded.each do |expanded_path|
9
+ app.config.paths["db/migrate"] << expanded_path
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,33 @@
1
+ # Adds a preload_tree method to the model
2
+ # Usage example:
3
+ #
4
+ # Model.find(1).preload_tree
5
+ #
6
+ module IndexTree
7
+ module FinderMethods
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ def preload_tree
12
+ return IndexTree::TreePreloader.preload_entities(self)
13
+ end
14
+
15
+ end
16
+ end
17
+ end
18
+
19
+
20
+ # Add a preload_tree method to the ActiveRecord_Relation
21
+ # Usage example:
22
+ #
23
+ # Model.where(level: 2).all.preload_tree
24
+ # Model.all.preload_tree
25
+ #
26
+ module ActiveRecord
27
+ class Relation
28
+ def preload_tree
29
+ load unless loaded?
30
+ return IndexTree::TreePreloader.preload_entities(@records)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,59 @@
1
+ require 'active_support/concern'
2
+
3
+ module IndexTree
4
+ module NodeElement
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Pointer to the index node
9
+ has_one :index_tree_index_node, :class_name => 'IndexTree::IndexNode', as: :node_element
10
+
11
+ # Creates index node for current tree node, and invokes index node creation for each defined association
12
+ # @param [root_element] Id of the root
13
+ def create_index_node(root_element)
14
+ IndexTree::IndexNode.create!(:root_element => root_element, :node_element => self)
15
+ create_index_nodes_for_children(root_element)
16
+ end
17
+
18
+ private
19
+
20
+ class_attribute :child_nodes
21
+
22
+ # Empty default value
23
+ self.child_nodes = {}
24
+
25
+ # Define what associations are child nodes
26
+ # It will be used to create Index nodes when the root element will be saved
27
+ # @param [*attributes] list of associations
28
+ def self.child_nodes(*attributes)
29
+ self.child_nodes = valid_associations(attributes)
30
+ end
31
+
32
+ # @return [valid_associations] map with existing associations
33
+ def self.valid_associations(attributes)
34
+ valid_associations = {}
35
+
36
+ # Check if the declared input is an existing association
37
+ Array(attributes).each do |association_name|
38
+ valid_associations[association_name] = reflections[association_name] if reflections[association_name]
39
+ end
40
+
41
+ return valid_associations
42
+ end
43
+
44
+ # Invoke index node creation for children node
45
+ # @param [root_element] id of the root element
46
+ def create_index_nodes_for_children(root_element)
47
+
48
+ self.child_nodes.keys.each do |association_name|
49
+ association_value = send(association_name)
50
+
51
+ Array(association_value).each do |child_node|
52
+ child_node.create_index_node(root_element)
53
+ end
54
+ end
55
+ end
56
+
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,8 @@
1
+ # Defines the ActsAssTreeNode Method
2
+ module IndexTree
3
+ class Railtie < Rails::Railtie
4
+ ActiveSupport.on_load(:active_record) do
5
+ extend IndexTree::ActsAsIndexedNode
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,82 @@
1
+ require 'active_support/concern'
2
+
3
+ # Defines the root element of the tree, it invokes the recursive index nodes creation.
4
+ # And Hold the structure of the tree
5
+ module IndexTree
6
+ module RootElement
7
+ extend ActiveSupport::Concern
8
+ include IndexTree::FinderMethods
9
+ include IndexTree::NodeElement
10
+
11
+ included do
12
+ has_many :index_tree_index_nodes, :class_name => 'IndexTree::IndexNode', as: :root_element, dependent: :destroy
13
+
14
+ after_save :rebuild_index_nodes
15
+
16
+ private
17
+ # Will rebuild the index nodes for the decision tree of the Root element
18
+ # It will be used in the eager load for fetching only the data that is related to the current Root Element
19
+ def rebuild_index_nodes
20
+ # Delete all old indices
21
+ IndexTree::IndexNode.delete_all({:root_element => self})
22
+
23
+ # Creates index node for the current tree node, and invokes recursive call for the children of the node
24
+ create_index_nodes_for_children(self)
25
+ end
26
+
27
+ # @return [tree_structure] Each Root Element hold the structure of the tree
28
+ def self.tree_structure
29
+ @@tree_structure ||= create_tree_structure({}, self)
30
+ end
31
+
32
+ # Recursive function that create the structure of the tree
33
+ # Iterates each tree node relation and creates a loading instruction
34
+ # @param [loaded_associations] Previous associations
35
+ # @param [class_to_load] Current class association
36
+ def self.create_tree_structure(loaded_associations, class_to_load)
37
+ class_to_load.child_nodes.each do |association_name, association|
38
+ get_all_child_classes(association).each do |child_class|
39
+
40
+ if (loaded_associations[class_to_load].nil? || loaded_associations[class_to_load][child_class].nil?)
41
+ loaded_associations[class_to_load] ||= {}
42
+
43
+ if association.macro == :belongs_to
44
+ loaded_associations[class_to_load][child_class] = {many: false, opposite: false, association_name: association_name, polymorphic: association.polymorphic?}
45
+ elsif association.macro == :has_one
46
+ loaded_associations[class_to_load][child_class] = {many: false, opposite: true, association_name: association_name, polymorphic: association.polymorphic? }
47
+ elsif association.macro == :has_many
48
+ loaded_associations[class_to_load][child_class] = {many: true, opposite: true, association_name: association_name, polymorphic: association.polymorphic?}
49
+ end
50
+
51
+ create_tree_structure(loaded_associations, child_class)
52
+ end
53
+ end
54
+ end
55
+
56
+ return loaded_associations
57
+ end
58
+
59
+ # Return all the classes for current association
60
+ # @param [association]
61
+ def self.get_all_child_classes(association)
62
+ if (association.polymorphic?)
63
+ return find_all_polymorphic_classes(association.name)
64
+ else
65
+ return Array(association.klass)
66
+ end
67
+ end
68
+
69
+ # Find all the classes from the polymorphic type
70
+ # @param [polymorphic_class] type to find
71
+ def self.find_all_polymorphic_classes(polymorphic_class)
72
+ ret = []
73
+ ObjectSpace.each_object(Class).select { |klass| klass < ActiveRecord::Base }.each do |i|
74
+ unless i.reflect_on_all_associations.select { |j| j.options[:as] == polymorphic_class }.empty?
75
+ ret << i
76
+ end
77
+ end
78
+ ret.flatten
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,68 @@
1
+ module IndexTree
2
+ module TreePreloader
3
+ def self.preload_entities(root_entities)
4
+
5
+ root_entities_array = Array(root_entities)
6
+ root_entity_class = root_entities_array.first.class
7
+
8
+ cache = {root_entity_class =>
9
+ Hash[root_entities_array.map { |t| [t.id, t] }]}
10
+
11
+ tree_structure = root_entity_class.tree_structure
12
+
13
+ tree_structure.each do |source_class, target_classes|
14
+ target_classes.each do |target_class, load_instruction|
15
+ preload_class(cache, root_entity_class, source_class)
16
+ preload_class(cache, root_entity_class, target_class)
17
+
18
+ if load_instruction[:opposite]
19
+ associate_entities_opposite(cache, source_class, load_instruction[:association_name], target_class, load_instruction[:many])
20
+ else
21
+ associate_entities(cache, source_class, load_instruction[:association_name], target_class, load_instruction[:polymorphic])
22
+ end
23
+ end
24
+ end
25
+
26
+ return root_entities
27
+ end
28
+
29
+ def self.preload_class(cache, root_entity_class, class_to_load)
30
+ unless cache.has_key?(class_to_load)
31
+ cache[class_to_load] = Hash[class_to_load.joins(:index_tree_index_node).where(:index_tree_index_nodes => {:root_element_type => cache[root_entity_class].values.first.class,
32
+ :root_element_id => cache[root_entity_class].keys}).all.map { |t| [t.id, t] }]
33
+ end
34
+ end
35
+
36
+ def self.associate_entities(cache, source_class, association_name, target_class, is_polymorphic)
37
+ cache[source_class].values.each do |source_entity|
38
+ if (is_polymorphic)
39
+ parent_type = source_entity.send(association_name.to_s + '_type')
40
+ next unless parent_type == target_class.to_s
41
+ end
42
+
43
+ parent_id = source_entity.send(association_name.to_s + '_id')
44
+ source_entity.association(association_name).target = cache[target_class][parent_id]
45
+ source_entity.association(association_name).loaded!
46
+ end
47
+ end
48
+
49
+ def self.associate_entities_opposite(cache, source_class, association_name, target_class, many)
50
+ cache[target_class].values.each do |target_entity|
51
+ attr_name = source_class.model_name.element + '_id'
52
+ parent_id = target_entity.send(attr_name)
53
+ if (parent_id)
54
+ if (many)
55
+ cache[source_class][parent_id].association(association_name).target << target_entity
56
+ else
57
+ cache[source_class][parent_id].association(association_name).target = target_entity
58
+ end
59
+ end
60
+ end
61
+
62
+ cache[source_class].values.each do |source_entity|
63
+ source_entity.association(association_name).loaded!
64
+ end
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,3 @@
1
+ module IndexTree
2
+ VERSION = "0.0.1"
3
+ end
data/lib/index_tree.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "index_tree/version"
2
+ require "index_tree/engine"
3
+
4
+
5
+ require 'index_tree/finder_methods'
6
+ require 'index_tree/node_element'
7
+ require 'index_tree/root_element'
8
+ require 'models/index_node'
9
+ require 'index_tree/tree_preloader'
10
+
11
+ require 'index_tree/acts_as_indexed_node'
12
+ require 'index_tree/railtie'
13
+
14
+ module IndexTree
15
+ def self.table_name_prefix
16
+ 'index_tree_'
17
+ end
18
+ end
metadata ADDED
@@ -0,0 +1,84 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: index_tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Alex Stanovsky
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-09-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: |-
28
+ Eager loads trees by indexing the nodes of the tree. The number of queries needed to load a tree is N,
29
+ when N is number of different models(ActiveRecords) in the tree
30
+ email:
31
+ - info@naturalint.com
32
+ executables: []
33
+ extensions: []
34
+ extra_rdoc_files: []
35
+ files:
36
+ - ".gitignore"
37
+ - Gemfile
38
+ - LICENSE
39
+ - LICENSE.txt
40
+ - README.md
41
+ - Rakefile
42
+ - app/models/index_node.rb
43
+ - db/migrate/20141004075233_create_index_tree_index_nodes.rb
44
+ - db/migrate/20141004075333_add_index_to_tree_index_nodes.rb
45
+ - index_tree.gemspec
46
+ - lib/index_tree.rb
47
+ - lib/index_tree/acts_as_indexed_node.rb
48
+ - lib/index_tree/engine.rb
49
+ - lib/index_tree/finder_methods.rb
50
+ - lib/index_tree/node_element.rb
51
+ - lib/index_tree/railtie.rb
52
+ - lib/index_tree/root_element.rb
53
+ - lib/index_tree/tree_preloader.rb
54
+ - lib/index_tree/version.rb
55
+ homepage: http://www.naturalint.com
56
+ licenses: []
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ - app
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ requirements: []
74
+ rubyforge_project:
75
+ rubygems_version: 2.2.2
76
+ signing_key:
77
+ specification_version: 4
78
+ summary: This Gem eager loads trees by indexing the nodes of the tree. The number
79
+ of queries needed to load a tree is N, when N is number of different models(ActiveRecords)
80
+ in the tree. Each inner object in the tree have an index node instance that is connecting
81
+ it to the root. When the root of the tree is loaded, only the objects that are in
82
+ the tree are fetched(Pruning). The index nodes are created when the root element
83
+ is saved.
84
+ test_files: []