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 +2 -0
- data/README.rdoc +71 -0
- data/lib/eager_record/version.rb +3 -0
- data/lib/eager_record.rb +180 -0
- metadata +76 -0
data/History.txt
ADDED
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.
|
data/lib/eager_record.rb
ADDED
@@ -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
|
+
|