eager_record 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+