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