cached-models 0.0.2
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/CHANGELOG +84 -0
- data/MIT-LICENSE +20 -0
- data/README +95 -0
- data/Rakefile +81 -0
- data/about.yml +8 -0
- data/cached-models.gemspec +19 -0
- data/init.rb +2 -0
- data/install.rb +1 -0
- data/lib/activerecord/lib/active_record/associations/association_collection.rb +95 -0
- data/lib/activerecord/lib/active_record/associations/association_proxy.rb +39 -0
- data/lib/activerecord/lib/active_record/associations/has_many_association.rb +7 -0
- data/lib/activerecord/lib/active_record/associations.rb +379 -0
- data/lib/activerecord/lib/active_record/base.rb +69 -0
- data/lib/activerecord/lib/active_record.rb +4 -0
- data/lib/cached-models.rb +1 -0
- data/lib/cached_models.rb +4 -0
- data/setup.rb +1585 -0
- data/tasks/cached_models_tasks.rake +90 -0
- data/test/active_record/associations/has_many_association_test.rb +401 -0
- data/test/active_record/base_test.rb +32 -0
- data/test/fixtures/authors.yml +13 -0
- data/test/fixtures/blogs.yml +7 -0
- data/test/fixtures/comments.yml +19 -0
- data/test/fixtures/posts.yml +23 -0
- data/test/fixtures/tags.yml +14 -0
- data/test/models/author.rb +10 -0
- data/test/models/blog.rb +4 -0
- data/test/models/comment.rb +3 -0
- data/test/models/post.rb +7 -0
- data/test/models/tag.rb +3 -0
- data/test/test_helper.rb +42 -0
- data/uninstall.rb +1 -0
- metadata +105 -0
@@ -0,0 +1,379 @@
|
|
1
|
+
# FIXME load paths
|
2
|
+
require File.dirname(__FILE__) + '/associations/association_proxy'
|
3
|
+
require File.dirname(__FILE__) + '/associations/association_collection'
|
4
|
+
require File.dirname(__FILE__) + '/associations/has_many_association'
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module Associations
|
8
|
+
module ClassMethods
|
9
|
+
# Adds the following methods for retrieval and query of collections of associated objects:
|
10
|
+
# +collection+ is replaced with the symbol passed as the first argument, so
|
11
|
+
# <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
|
12
|
+
# * <tt>collection(force_reload = false)</tt> - Returns an array of all the associated objects.
|
13
|
+
# An empty array is returned if none are found.
|
14
|
+
# * <tt>collection<<(object, ...)</tt> - Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
|
15
|
+
# * <tt>collection.delete(object, ...)</tt> - Removes one or more objects from the collection by setting their foreign keys to +NULL+.
|
16
|
+
# This will also destroy the objects if they're declared as +belongs_to+ and dependent on this model.
|
17
|
+
# * <tt>collection=objects</tt> - Replaces the collections content by deleting and adding objects as appropriate.
|
18
|
+
# * <tt>collection_singular_ids</tt> - Returns an array of the associated objects' ids
|
19
|
+
# * <tt>collection_singular_ids=ids</tt> - Replace the collection with the objects identified by the primary keys in +ids+
|
20
|
+
# * <tt>collection.clear</tt> - Removes every object from the collection. This destroys the associated objects if they
|
21
|
+
# are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the database if <tt>:dependent => :delete_all</tt>,
|
22
|
+
# otherwise sets their foreign keys to +NULL+.
|
23
|
+
# * <tt>collection.empty?</tt> - Returns +true+ if there are no associated objects.
|
24
|
+
# * <tt>collection.size</tt> - Returns the number of associated objects.
|
25
|
+
# * <tt>collection.find</tt> - Finds an associated object according to the same rules as Base.find.
|
26
|
+
# * <tt>collection.build(attributes = {}, ...)</tt> - Returns one or more new objects of the collection type that have been instantiated
|
27
|
+
# with +attributes+ and linked to this object through a foreign key, but have not yet been saved. *Note:* This only works if an
|
28
|
+
# associated object already exists, not if it's +nil+!
|
29
|
+
# * <tt>collection.create(attributes = {})</tt> - Returns a new object of the collection type that has been instantiated
|
30
|
+
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
31
|
+
# *Note:* This only works if an associated object already exists, not if it's +nil+!
|
32
|
+
#
|
33
|
+
# Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
|
34
|
+
# * <tt>Firm#clients</tt> (similar to <tt>Clients.find :all, :conditions => "firm_id = #{id}"</tt>)
|
35
|
+
# * <tt>Firm#clients<<</tt>
|
36
|
+
# * <tt>Firm#clients.delete</tt>
|
37
|
+
# * <tt>Firm#clients=</tt>
|
38
|
+
# * <tt>Firm#client_ids</tt>
|
39
|
+
# * <tt>Firm#client_ids=</tt>
|
40
|
+
# * <tt>Firm#clients.clear</tt>
|
41
|
+
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
|
42
|
+
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
|
43
|
+
# * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
|
44
|
+
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
|
45
|
+
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
|
46
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
47
|
+
#
|
48
|
+
# Options are:
|
49
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
50
|
+
# from the association name. So <tt>has_many :products</tt> will by default be linked to the Product class, but
|
51
|
+
# if the real class name is SpecialProduct, you'll have to specify it with this option.
|
52
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated objects must meet in order to be included as a +WHERE+
|
53
|
+
# SQL fragment, such as <tt>price > 5 AND name LIKE 'B%'</tt>. Record creations from the association are scoped if a hash
|
54
|
+
# is used. <tt>has_many :posts, :conditions => {:published => true}</tt> will create published posts with <tt>@blog.posts.create</tt>
|
55
|
+
# or <tt>@blog.posts.build</tt>.
|
56
|
+
# * <tt>:order</tt> - Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
|
57
|
+
# such as <tt>last_name, first_name DESC</tt>.
|
58
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
59
|
+
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ association will use "person_id"
|
60
|
+
# as the default <tt>:foreign_key</tt>.
|
61
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt> all the associated objects are destroyed
|
62
|
+
# alongside this object by calling their +destroy+ method. If set to <tt>:delete_all</tt> all associated
|
63
|
+
# objects are deleted *without* calling their +destroy+ method. If set to <tt>:nullify</tt> all associated
|
64
|
+
# objects' foreign keys are set to +NULL+ *without* calling their +save+ callbacks. *Warning:* This option is ignored when also using
|
65
|
+
# the <tt>:through</tt> option.
|
66
|
+
# * <tt>:finder_sql</tt> - Specify a complete SQL statement to fetch the association. This is a good way to go for complex
|
67
|
+
# associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ is _not_ added.
|
68
|
+
# * <tt>:counter_sql</tt> - Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is
|
69
|
+
# specified but not <tt>:counter_sql</tt>, <tt>:counter_sql</tt> will be generated by replacing <tt>SELECT ... FROM</tt> with <tt>SELECT COUNT(*) FROM</tt>.
|
70
|
+
# * <tt>:extend</tt> - Specify a named module for extending the proxy. See "Association extensions".
|
71
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when the collection is loaded.
|
72
|
+
# * <tt>:group</tt> - An attribute name by which the result should be grouped. Uses the <tt>GROUP BY</tt> SQL-clause.
|
73
|
+
# * <tt>:limit</tt> - An integer determining the limit on the number of rows that should be returned.
|
74
|
+
# * <tt>:offset</tt> - An integer determining the offset from where the rows should be fetched. So at 5, it would skip the first 4 rows.
|
75
|
+
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if you, for example, want to do a join
|
76
|
+
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will rise an error.
|
77
|
+
# * <tt>:as</tt> - Specifies a polymorphic interface (See <tt>belongs_to</tt>).
|
78
|
+
# * <tt>:through</tt> - Specifies a Join Model through which to perform the query. Options for <tt>:class_name</tt> and <tt>:foreign_key</tt>
|
79
|
+
# are ignored, as the association uses the source reflection. You can only use a <tt>:through</tt> query through a <tt>belongs_to</tt>
|
80
|
+
# or <tt>has_many</tt> association on the join model.
|
81
|
+
# * <tt>:source</tt> - Specifies the source association name used by <tt>has_many :through</tt> queries. Only use it if the name cannot be
|
82
|
+
# inferred from the association. <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
|
83
|
+
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
|
84
|
+
# * <tt>:source_type</tt> - Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
|
85
|
+
# association is a polymorphic +belongs_to+.
|
86
|
+
# * <tt>:uniq</tt> - If true, duplicates will be omitted from the collection. Useful in conjunction with <tt>:through</tt>.
|
87
|
+
# * <tt>:readonly</tt> - If true, all the associated objects are readonly through the association.
|
88
|
+
# * <tt>:cached</tt> - If true, all the associated objects will be cached.
|
89
|
+
#
|
90
|
+
# Option examples:
|
91
|
+
# has_many :comments, :order => "posted_on"
|
92
|
+
# has_many :comments, :include => :author
|
93
|
+
# has_many :people, :class_name => "Person", :conditions => "deleted = 0", :order => "name"
|
94
|
+
# has_many :tracks, :order => "position", :dependent => :destroy
|
95
|
+
# has_many :comments, :dependent => :nullify
|
96
|
+
# has_many :tags, :as => :taggable
|
97
|
+
# has_many :reports, :readonly => true
|
98
|
+
# has_many :posts, :cached => true
|
99
|
+
# has_many :subscribers, :through => :subscriptions, :source => :user
|
100
|
+
# has_many :subscribers, :class_name => "Person", :finder_sql =>
|
101
|
+
# 'SELECT DISTINCT people.* ' +
|
102
|
+
# 'FROM people p, post_subscriptions ps ' +
|
103
|
+
# 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' +
|
104
|
+
# 'ORDER BY p.first_name'
|
105
|
+
def has_many(association_id, options = {}, &extension)
|
106
|
+
reflection = create_has_many_reflection(association_id, options, &extension)
|
107
|
+
|
108
|
+
configure_dependency_for_has_many(reflection)
|
109
|
+
|
110
|
+
add_multiple_associated_save_callbacks(reflection.name)
|
111
|
+
add_association_callbacks(reflection.name, reflection.options)
|
112
|
+
|
113
|
+
if options[:through]
|
114
|
+
collection_accessor_methods(reflection, HasManyThroughAssociation, options)
|
115
|
+
else
|
116
|
+
collection_accessor_methods(reflection, HasManyAssociation, options)
|
117
|
+
end
|
118
|
+
|
119
|
+
add_cache_callbacks if options[:cached]
|
120
|
+
end
|
121
|
+
|
122
|
+
# Adds the following methods for retrieval and query for a single associated object for which this object holds an id:
|
123
|
+
# +association+ is replaced with the symbol passed as the first argument, so
|
124
|
+
# <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
|
125
|
+
# * <tt>association(force_reload = false)</tt> - Returns the associated object. +nil+ is returned if none is found.
|
126
|
+
# * <tt>association=(associate)</tt> - Assigns the associate object, extracts the primary key, and sets it as the foreign key.
|
127
|
+
# * <tt>association.nil?</tt> - Returns +true+ if there is no associated object.
|
128
|
+
# * <tt>build_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
129
|
+
# with +attributes+ and linked to this object through a foreign key, but has not yet been saved.
|
130
|
+
# * <tt>create_association(attributes = {})</tt> - Returns a new object of the associated type that has been instantiated
|
131
|
+
# with +attributes+, linked to this object through a foreign key, and that has already been saved (if it passed the validation).
|
132
|
+
#
|
133
|
+
# Example: A Post class declares <tt>belongs_to :author</tt>, which will add:
|
134
|
+
# * <tt>Post#author</tt> (similar to <tt>Author.find(author_id)</tt>)
|
135
|
+
# * <tt>Post#author=(author)</tt> (similar to <tt>post.author_id = author.id</tt>)
|
136
|
+
# * <tt>Post#author?</tt> (similar to <tt>post.author == some_author</tt>)
|
137
|
+
# * <tt>Post#author.nil?</tt>
|
138
|
+
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
|
139
|
+
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
|
140
|
+
# The declaration can also include an options hash to specialize the behavior of the association.
|
141
|
+
#
|
142
|
+
# Options are:
|
143
|
+
# * <tt>:class_name</tt> - Specify the class name of the association. Use it only if that name can't be inferred
|
144
|
+
# from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but
|
145
|
+
# if the real class name is Person, you'll have to specify it with this option.
|
146
|
+
# * <tt>:conditions</tt> - Specify the conditions that the associated object must meet in order to be included as a +WHERE+
|
147
|
+
# SQL fragment, such as <tt>authorized = 1</tt>.
|
148
|
+
# * <tt>:select</tt> - By default, this is <tt>*</tt> as in <tt>SELECT * FROM</tt>, but can be changed if, for example, you want to do a join
|
149
|
+
# but not include the joined columns. Do not forget to include the primary and foreign keys, otherwise it will raise an error.
|
150
|
+
# * <tt>:foreign_key</tt> - Specify the foreign key used for the association. By default this is guessed to be the name
|
151
|
+
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt> association will use
|
152
|
+
# "person_id" as the default <tt>:foreign_key</tt>. Similarly, <tt>belongs_to :favorite_person, :class_name => "Person"</tt>
|
153
|
+
# will use a foreign key of "favorite_person_id".
|
154
|
+
# * <tt>:dependent</tt> - If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to
|
155
|
+
# <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. This option should not be specified when
|
156
|
+
# <tt>belongs_to</tt> is used in conjunction with a <tt>has_many</tt> relationship on another class because of the potential to leave
|
157
|
+
# orphaned records behind.
|
158
|
+
# * <tt>:counter_cache</tt> - Caches the number of belonging objects on the associate class through the use of +increment_counter+
|
159
|
+
# and +decrement_counter+. The counter cache is incremented when an object of this class is created and decremented when it's
|
160
|
+
# destroyed. This requires that a column named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
|
161
|
+
# is used on the associate class (such as a Post class). You can also specify a custom counter cache column by providing
|
162
|
+
# a column name instead of a +true+/+false+ value to this option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
|
163
|
+
# When creating a counter cache column, the database statement or migration must specify a default value of <tt>0</tt>, failing to do
|
164
|
+
# this results in a counter with +NULL+ value, which will never increment.
|
165
|
+
# Note: Specifying a counter cache will add it to that model's list of readonly attributes using +attr_readonly+.
|
166
|
+
# * <tt>:include</tt> - Specify second-order associations that should be eager loaded when this object is loaded.
|
167
|
+
# * <tt>:polymorphic</tt> - Specify this association is a polymorphic association by passing +true+.
|
168
|
+
# Note: If you've enabled the counter cache, then you may want to add the counter cache attribute
|
169
|
+
# to the +attr_readonly+ list in the associated classes (e.g. <tt>class Post; attr_readonly :comments_count; end</tt>).
|
170
|
+
# * <tt>:readonly</tt> - If true, the associated object is readonly through the association.
|
171
|
+
# * <tt>:validate</tt> - If false, don't validate the associated objects when saving the parent object. +false+ by default.
|
172
|
+
#
|
173
|
+
# Option examples:
|
174
|
+
# belongs_to :firm, :foreign_key => "client_of"
|
175
|
+
# belongs_to :author, :class_name => "Person", :foreign_key => "author_id"
|
176
|
+
# belongs_to :valid_coupon, :class_name => "Coupon", :foreign_key => "coupon_id",
|
177
|
+
# :conditions => 'discounts > #{payments_count}'
|
178
|
+
# belongs_to :attachable, :polymorphic => true
|
179
|
+
# belongs_to :project, :readonly => true
|
180
|
+
# belongs_to :post, :counter_cache => true
|
181
|
+
# belongs_to :blog, :cached => true
|
182
|
+
def belongs_to(association_id, options = {})
|
183
|
+
reflection = create_belongs_to_reflection(association_id, options)
|
184
|
+
|
185
|
+
ivar = "@#{reflection.name}"
|
186
|
+
|
187
|
+
if reflection.options[:polymorphic]
|
188
|
+
association_accessor_methods(reflection, BelongsToPolymorphicAssociation)
|
189
|
+
|
190
|
+
method_name = "polymorphic_belongs_to_before_save_for_#{reflection.name}".to_sym
|
191
|
+
define_method(method_name) do
|
192
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
193
|
+
|
194
|
+
if association && association.target
|
195
|
+
if association.new_record?
|
196
|
+
association.save(true)
|
197
|
+
end
|
198
|
+
|
199
|
+
if association.updated?
|
200
|
+
self["#{reflection.primary_key_name}"] = association.id
|
201
|
+
self["#{reflection.options[:foreign_type]}"] = association.class.base_class.name.to_s
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
before_save method_name
|
206
|
+
else
|
207
|
+
association_accessor_methods(reflection, BelongsToAssociation)
|
208
|
+
association_constructor_method(:build, reflection, BelongsToAssociation)
|
209
|
+
association_constructor_method(:create, reflection, BelongsToAssociation)
|
210
|
+
|
211
|
+
method_name = "belongs_to_before_save_for_#{reflection.name}".to_sym
|
212
|
+
define_method(method_name) do
|
213
|
+
association = instance_variable_get("#{ivar}") if instance_variable_defined?("#{ivar}")
|
214
|
+
|
215
|
+
if !association.nil?
|
216
|
+
if association.new_record?
|
217
|
+
association.save(true)
|
218
|
+
end
|
219
|
+
|
220
|
+
if association.updated?
|
221
|
+
self["#{reflection.primary_key_name}"] = association.id
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
before_save method_name
|
226
|
+
end
|
227
|
+
|
228
|
+
# Create the callbacks to update counter cache
|
229
|
+
if options[:counter_cache]
|
230
|
+
cache_column = options[:counter_cache] == true ?
|
231
|
+
"#{self.to_s.underscore.pluralize}_count" :
|
232
|
+
options[:counter_cache]
|
233
|
+
|
234
|
+
method_name = "belongs_to_counter_cache_after_create_for_#{reflection.name}".to_sym
|
235
|
+
define_method(method_name) do
|
236
|
+
association = send("#{reflection.name}")
|
237
|
+
association.class.increment_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil?
|
238
|
+
end
|
239
|
+
after_create method_name
|
240
|
+
|
241
|
+
method_name = "belongs_to_counter_cache_before_destroy_for_#{reflection.name}".to_sym
|
242
|
+
define_method(method_name) do
|
243
|
+
association = send("#{reflection.name}")
|
244
|
+
association.class.decrement_counter("#{cache_column}", send("#{reflection.primary_key_name}")) unless association.nil?
|
245
|
+
end
|
246
|
+
before_destroy method_name
|
247
|
+
|
248
|
+
module_eval(
|
249
|
+
"#{reflection.class_name}.send(:attr_readonly,\"#{cache_column}\".intern) if defined?(#{reflection.class_name}) && #{reflection.class_name}.respond_to?(:attr_readonly)"
|
250
|
+
)
|
251
|
+
end
|
252
|
+
|
253
|
+
if options[:cached]
|
254
|
+
method_name = "belongs_to_after_save_for_#{reflection.name}".to_sym
|
255
|
+
define_method(method_name) do
|
256
|
+
send(reflection.name).expire_cache_for(self.class.name)
|
257
|
+
end
|
258
|
+
after_save method_name
|
259
|
+
end
|
260
|
+
|
261
|
+
add_single_associated_validation_callbacks(reflection.name) if options[:validate] == true
|
262
|
+
|
263
|
+
configure_dependency_for_belongs_to(reflection)
|
264
|
+
end
|
265
|
+
|
266
|
+
def collection_reader_method(reflection, association_proxy_class, options)
|
267
|
+
define_method(reflection.name) do |*params|
|
268
|
+
ivar = "@#{reflection.name}"
|
269
|
+
|
270
|
+
force_reload = params.first unless params.empty?
|
271
|
+
|
272
|
+
association = if options[:cached]
|
273
|
+
cache_read(reflection)
|
274
|
+
else
|
275
|
+
instance_variable_get(ivar) if instance_variable_defined?(ivar)
|
276
|
+
end
|
277
|
+
|
278
|
+
unless association.respond_to?(:loaded?)
|
279
|
+
association = association_proxy_class.new(self, reflection)
|
280
|
+
instance_variable_set(ivar, association)
|
281
|
+
end
|
282
|
+
|
283
|
+
if force_reload
|
284
|
+
association.reload
|
285
|
+
cache_delete(reflection) if options[:cached]
|
286
|
+
end
|
287
|
+
|
288
|
+
if options[:cached]
|
289
|
+
cache_fetch(reflection, association)
|
290
|
+
else
|
291
|
+
association
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
method_name = "#{reflection.name.to_s.singularize}_ids"
|
296
|
+
define_method(method_name) do
|
297
|
+
if options[:cached]
|
298
|
+
cache_fetch("#{cache_key}/#{method_name}", send("calculate_#{method_name}"))
|
299
|
+
else
|
300
|
+
send("calculate_#{method_name}")
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
define_method("calculate_#{method_name}") do
|
305
|
+
send(reflection.name).map { |record| record.id }
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
def collection_accessor_methods(reflection, association_proxy_class, options, writer = true)
|
310
|
+
collection_reader_method(reflection, association_proxy_class, options)
|
311
|
+
|
312
|
+
if writer
|
313
|
+
define_method("#{reflection.name}=") do |new_value|
|
314
|
+
# Loads proxy class instance (defined in collection_reader_method) if not already loaded
|
315
|
+
association = send(reflection.name)
|
316
|
+
association.replace(new_value)
|
317
|
+
|
318
|
+
cache_write(reflection, association) if options[:cached]
|
319
|
+
|
320
|
+
association
|
321
|
+
end
|
322
|
+
|
323
|
+
define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
|
324
|
+
ids = (new_value || []).reject { |nid| nid.blank? }
|
325
|
+
send("#{reflection.name}=", reflection.class_name.constantize.find(ids))
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
def create_has_many_reflection(association_id, options, &extension)
|
331
|
+
options.assert_valid_keys(
|
332
|
+
:class_name, :table_name, :foreign_key, :primary_key,
|
333
|
+
:dependent,
|
334
|
+
:select, :conditions, :include, :order, :group, :limit, :offset,
|
335
|
+
:as, :through, :source, :source_type,
|
336
|
+
:uniq,
|
337
|
+
:finder_sql, :counter_sql,
|
338
|
+
:before_add, :after_add, :before_remove, :after_remove,
|
339
|
+
:extend, :readonly,
|
340
|
+
:validate, :accessible,
|
341
|
+
:cached
|
342
|
+
)
|
343
|
+
|
344
|
+
options[:extend] = create_extension_modules(association_id, extension, options[:extend])
|
345
|
+
|
346
|
+
create_reflection(:has_many, association_id, options, self)
|
347
|
+
end
|
348
|
+
|
349
|
+
def create_belongs_to_reflection(association_id, options)
|
350
|
+
options.assert_valid_keys(
|
351
|
+
:class_name, :foreign_key, :foreign_type, :remote, :select, :conditions, :include, :dependent,
|
352
|
+
:counter_cache, :extend, :polymorphic, :readonly, :validate, :cached
|
353
|
+
)
|
354
|
+
|
355
|
+
reflection = create_reflection(:belongs_to, association_id, options, self)
|
356
|
+
|
357
|
+
if options[:polymorphic]
|
358
|
+
reflection.options[:foreign_type] ||= reflection.class_name.underscore + "_type"
|
359
|
+
end
|
360
|
+
|
361
|
+
reflection
|
362
|
+
end
|
363
|
+
|
364
|
+
def add_cache_callbacks
|
365
|
+
method_name = :after_save_cache_expire
|
366
|
+
return if respond_to? method_name
|
367
|
+
|
368
|
+
define_method(method_name) do
|
369
|
+
return unless self[:updated_at]
|
370
|
+
|
371
|
+
self.class.reflections.each do |name, reflection|
|
372
|
+
cache_delete(reflection) if reflection.options[:cached]
|
373
|
+
end
|
374
|
+
end
|
375
|
+
after_save method_name
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
class Base
|
3
|
+
@@rails_cache = nil
|
4
|
+
cattr_accessor :rails_cache
|
5
|
+
|
6
|
+
protected
|
7
|
+
def rails_cache
|
8
|
+
self.class.rails_cache
|
9
|
+
end
|
10
|
+
|
11
|
+
# Expire the cache for the associations which contains the given class.
|
12
|
+
#
|
13
|
+
# Example:
|
14
|
+
# class Blog < ActiveRecord::Base
|
15
|
+
# has_many :posts, :cached => true
|
16
|
+
# has_many :recent_posts, :class_name => 'Post',
|
17
|
+
# :limit => 10, :order => 'id DESC', :cached => true
|
18
|
+
#
|
19
|
+
# has_many :readers, :class_name => 'Person'
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# If one of the most recent posts will be updated, #expire_cache_for
|
23
|
+
# will be invoked with the "Post" parameter, in order to expire the
|
24
|
+
# cache for the first to associations.
|
25
|
+
def expire_cache_for(class_name)
|
26
|
+
self.class.reflections.each do |name, reflection|
|
27
|
+
if reflection.options[:cached] and reflection.class_name == class_name
|
28
|
+
cache_delete(reflection)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def cache_read(reflection)
|
34
|
+
return unless cached_associations[reflection.name]
|
35
|
+
rails_cache.read(reflection_cache_key(reflection))
|
36
|
+
end
|
37
|
+
|
38
|
+
def cache_write(reflection, value)
|
39
|
+
cached_associations[reflection.name] = rails_cache.write(reflection_cache_key(reflection), value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def cache_delete(reflection)
|
43
|
+
return unless cached_associations[reflection.name]
|
44
|
+
cached_associations[reflection.name] = !rails_cache.delete(reflection_cache_key(reflection))
|
45
|
+
end
|
46
|
+
|
47
|
+
def cache_fetch(reflection, value)
|
48
|
+
reflection_name, key = extract_options_for_cache(reflection)
|
49
|
+
cached_associations[reflection_name] = true
|
50
|
+
rails_cache.fetch(key) { value }
|
51
|
+
end
|
52
|
+
|
53
|
+
def extract_options_for_cache(reflection)
|
54
|
+
if reflection.is_a?(AssociationReflection)
|
55
|
+
[ reflection.name, reflection_cache_key(reflection) ]
|
56
|
+
else
|
57
|
+
[ reflection.split('/').last, reflection ]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def reflection_cache_key(reflection)
|
62
|
+
"#{cache_key}/#{reflection.name}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def cached_associations
|
66
|
+
@cached_associations ||= {}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'cached_models'
|