eager_record 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt ADDED
@@ -0,0 +1,2 @@
1
+ == 0.0.1 2010-05-03
2
+ * Initial release
data/README.rdoc ADDED
@@ -0,0 +1,71 @@
1
+ = EagerRecord
2
+
3
+ Never pass the `:include` option to `find` again.
4
+
5
+ EagerRecord is a simple extension to ActiveRecord that automates the process
6
+ of preloading associations in a result set. Each time ActiveRecord loads a
7
+ collection of more than one record out of the database, a reference to the
8
+ collection is stored in each record. Then, when you first access an association
9
+ on one of those records; EagerRecord automatically preloads that association for
10
+ all of the records in the collection.
11
+
12
+ == Basic Usage
13
+
14
+ There isn't really any usage; just install the gem and add it as a dependency.
15
+ Whereas before you might write:
16
+
17
+ Comment.all(:include => :user).each { |comment| puts comment.user }
18
+
19
+ Now you get the same effect with:
20
+
21
+ Comment.all.each { |comment| puts comment.user }
22
+
23
+ In the first iteration of that loop, all the comments' users are loaded in a
24
+ single SQL query.
25
+
26
+ == Scoped Preloading
27
+
28
+ It isn't enabled by default, but EagerRecord has experimental support for
29
+ "scoped preloading". If you turn it on, EagerRecord will perform preloading
30
+ on scoped associations in the same way that it does so for normal ones. For
31
+ instance, let's say you've got a `:ham` named scope for your `Comment` class:
32
+
33
+ Post.all.each { |post| puts post.comments.ham.inspect }
34
+
35
+ In the first iteration of this loop, EagerRecord will preload all of the ham
36
+ comments for all of the posts in the collection; the above code will only
37
+ generate two SQL queries. A couple of caveats:
38
+
39
+ * This only works for normal `has_many` associations right now. I could add
40
+ support for other collection types in the future.
41
+ * The code that does this is considerably more invasive in ActiveRecord's
42
+ internals than the regular eager preloading code. It basically has to trick
43
+ ActiveRecord into thinking that there is an association collection defined
44
+ on your model that doesn't actually exist, and then tell it to preload that
45
+ association. There are a couple of tests; so I know it basically works, but
46
+ beware that your mileage may vary. Because of this, you have to explicitly
47
+ tell eager record you want to use this feature (put this line in a file in
48
+ `config/initializers`:
49
+
50
+ EagerRecord.use_scoped_preload = true
51
+
52
+ === Does it work with all association types?
53
+
54
+ The test suite covers just about all of them; if you find one that doesn't work,
55
+ please file an issue.
56
+
57
+ === Do I need to make any changes to my code?
58
+
59
+ EagerRecord doesn't add any new methods to ActiveRecord, so you don't need to
60
+ interact with it directly. However, you might want to make adjustments to your
61
+ code to encourage preloading to happen at the earliest possible point. In
62
+ particular, if you're using `size` or `first` or `last`, on a collection,
63
+ but then later on also working with all the members of that collection, you're
64
+ better off calling `to_a` at the beginning so that preloading happens and
65
+ ActiveRecord doesn't make extra SQL queries.
66
+
67
+ == This is free software.
68
+
69
+ The author(s) of this software relinqiush all copyright on it to the maximum
70
+ extent permitted by law. Anyone is free to copy, distribute, mangle, sell,
71
+ improve, destroy, heckle, hate, or love this software with no restrictions.
@@ -0,0 +1,3 @@
1
+ module EagerRecord
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,180 @@
1
+ require 'rubygems'
2
+ require 'active_record'
3
+ require 'digest'
4
+
5
+ module EagerRecord
6
+ autoload :VERSION, File.join(File.dirname(__FILE__), 'eager_record', 'version')
7
+
8
+ TEMPORARY_SCOPED_PRELOAD_ASSOCIATION = :"_temporary_association_for_scoped_preloading"
9
+
10
+ class <<self
11
+ def install
12
+ ActiveRecord::Base.module_eval do
13
+ extend(EagerRecord::BaseExtensions::ClassMethods)
14
+ include(EagerRecord::BaseExtensions::InstanceMethods)
15
+ end
16
+ ActiveRecord::Associations::AssociationProxy.module_eval { include(EagerRecord::AssociationProxyExtensions) }
17
+ ActiveRecord::Associations::AssociationCollection.module_eval { include(EagerRecord::AssociationCollectionExtensions) }
18
+ ActiveRecord::Associations::HasManyAssociation.module_eval { include(EagerRecord::HasManyAssociationExtensions) }
19
+ end
20
+
21
+ def use_scoped_preload=(flag)
22
+ @use_scoped_preload = flag
23
+ end
24
+
25
+ def use_scoped_preload?
26
+ !!@use_scoped_preload
27
+ end
28
+ end
29
+
30
+ module BaseExtensions
31
+ module ClassMethods
32
+ def self.extended(base)
33
+ (class <<base; self; end).module_eval do
34
+ alias_method_chain :find_by_sql, :eager_preloading
35
+ end
36
+ end
37
+
38
+ def find_by_sql_with_eager_preloading(*args)
39
+ collection = find_by_sql_without_eager_preloading(*args)
40
+ grouped_collections = collection.group_by { |record| record.class }
41
+ grouped_collections.values.each do |grouped_collection|
42
+ if grouped_collection.length > 1
43
+ grouped_collection.each do |record|
44
+ record.instance_variable_set(:@originating_collection, grouped_collection)
45
+ end
46
+ end
47
+ end
48
+ collection
49
+ end
50
+ end
51
+
52
+ module InstanceMethods
53
+ def self.included(base)
54
+ base.has_many TEMPORARY_SCOPED_PRELOAD_ASSOCIATION, :readonly => true
55
+ end
56
+
57
+ private
58
+
59
+ def scoped_preloaded_associations
60
+ @scoped_preloaded_associations ||= Hash.new { |h, k| h[k] = {}}
61
+ end
62
+
63
+ def scoped_preloaded_associations_for(association_name)
64
+ scoped_preloaded_associations[association_name.to_sym]
65
+ end
66
+ end
67
+ end
68
+
69
+ module AssociationProxyExtensions
70
+
71
+ def self.included(base)
72
+ base.module_eval do
73
+ alias_method_chain :load_target, :eager_preloading
74
+ end
75
+ end
76
+
77
+ def load_target_with_eager_preloading
78
+ return nil unless defined?(@loaded)
79
+
80
+ if !loaded? and (!@owner.new_record? || foreign_key_present)
81
+ if originating_collection = @owner.instance_variable_get(:@originating_collection)
82
+ association_name = @reflection.name
83
+ @owner.class.__send__(:preload_associations, originating_collection, association_name)
84
+ return @target if loaded?
85
+ end
86
+ end
87
+ load_target_without_eager_preloading
88
+ end
89
+ end
90
+
91
+ module AssociationCollectionExtensions
92
+ def self.included(base)
93
+ base.module_eval do
94
+ alias_method_chain :load_target, :eager_preloading
95
+ alias_method_chain :find, :eager_preloading
96
+ end
97
+ end
98
+
99
+ def load_target_with_eager_preloading
100
+ if !@owner.new_record? || foreign_key_present
101
+ if !loaded?
102
+ if originating_collection = @owner.instance_variable_get(:@originating_collection)
103
+ @owner.class.__send__(:preload_associations, originating_collection, @reflection.name)
104
+ return target if loaded?
105
+ end
106
+ end
107
+ end
108
+ load_target_without_eager_preloading
109
+ end
110
+
111
+ #
112
+ # Because of some likely unintentional plumbing in the scoping/association
113
+ # delegation chain, current_scoped_methods returns an association proxy's
114
+ # scope when called on the association collection. This means that, among
115
+ # other things, a named scope called on an association collection will
116
+ # duplicate the association collection's SQL restriction.
117
+ #
118
+ def current_scoped_methods
119
+ @reflection.klass.__send__(:current_scoped_methods)
120
+ end
121
+
122
+ def find_with_eager_preloading(*args)
123
+ if EagerRecord.use_scoped_preload? && originating_collection = @owner.instance_variable_get(:@originating_collection)
124
+ find_using_scoped_preload(originating_collection, *args)
125
+ else
126
+ find_without_eager_preloading(*args)
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ #
133
+ # Subclasses can override this
134
+ #
135
+ def find_using_scoped_preload(originating_collection, *args)
136
+ find_without_eager_preloading(*args)
137
+ end
138
+ end
139
+
140
+ module HasManyAssociationExtensions
141
+ def find_using_scoped_preload(originating_collection, *args)
142
+ options = args.extract_options!
143
+ reflection_name = @reflection.name
144
+ current_scope =
145
+ if current_scoped_methods && current_scoped_methods[:find] #XXX regression test
146
+ @reflection.options.merge(current_scoped_methods[:find])
147
+ else
148
+ @reflection.options
149
+ end
150
+ owner_class = @owner.class
151
+ reflection_class = @reflection.klass
152
+ scope_key = current_scope.inspect
153
+ if preloaded_association = @owner.__send__(:scoped_preloaded_associations_for, reflection_name)[scope_key]
154
+ return preloaded_association
155
+ end
156
+ reflection = owner_class.__send__(
157
+ :create_has_many_reflection,
158
+ TEMPORARY_SCOPED_PRELOAD_ASSOCIATION,
159
+ current_scope.merge(
160
+ :class_name => reflection_class.name,
161
+ :readonly => true
162
+ )
163
+ )
164
+ originating_collection.each do |record|
165
+ association = ActiveRecord::Associations::HasManyAssociation.new(record, reflection)
166
+ record.__send__(:association_instance_set, TEMPORARY_SCOPED_PRELOAD_ASSOCIATION, association)
167
+ end
168
+ owner_class.__send__(:preload_has_many_association, originating_collection, reflection)
169
+ originating_collection.each do |record|
170
+ record.instance_eval do
171
+ @scoped_preloaded_associations ||= Hash.new { |h, k| h[k] = {} }
172
+ @scoped_preloaded_associations[reflection_name][scope_key] =
173
+ association_instance_get(TEMPORARY_SCOPED_PRELOAD_ASSOCIATION)
174
+ association_instance_set(TEMPORARY_SCOPED_PRELOAD_ASSOCIATION, nil)
175
+ end
176
+ end
177
+ @owner.__send__(:scoped_preloaded_associations_for, reflection_name)[scope_key]
178
+ end
179
+ end
180
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eager_record
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Mat Brown
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-05-03 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: rspec
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description: EagerRecord extends ActiveRecord to automate association preloading. Each time a collection of more than one record is loaded from the database, each record remembers the collection that it is part of; then when one of those records has an association accessed, EagerRecord triggers a preload_associations for all the records in the originating collection. Never worry about that :include option again!
33
+ email: mat@patch.com
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files: []
39
+
40
+ files:
41
+ - lib/eager_record.rb
42
+ - lib/eager_record/version.rb
43
+ - README.rdoc
44
+ - History.txt
45
+ has_rdoc: true
46
+ homepage: http://github.com/outoftime/eager_record
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options: []
51
+
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ requirements: []
69
+
70
+ rubyforge_project: eager_record
71
+ rubygems_version: 1.3.6
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Automatic association preloading for ActiveRecord collections.
75
+ test_files: []
76
+