public
Description: Caches ActiveRecord associations using memcache
Clone URL: git://github.com/cmorss/association_cache.git
Added has_and_belongs_to_many, finder, etc
cmorss (author)
Mon Jun 16 18:22:49 -0700 2008
commit  9472169545008aaa8e9df11728aab0293b2d83b7
tree    f08590d4dd5611e0d8474ffe68195abe9592e510
parent  d94e342e0a7d240640bf35ae2d3cf5230cff14fa
0
...
1
2
3
4
 
5
 
 
 
 
 
6
7
8
9
10
 
 
 
 
 
11
12
13
 
...
1
2
3
 
4
5
6
7
8
9
10
11
12
13
14
 
15
16
17
18
19
20
21
 
22
0
@@ -1,13 +1,22 @@
0
 AssociationCache
0
 ================
0
 
0
-Introduction goes here.
0
+Caches "some" active record associations.
0
 
0
+For a belongs_to the object is looked up directly using the cache_key. If not found
0
+then a find() is done and the object is put in the cache and returned.
0
+
0
+Collections are turned into queries just for the ids and then if the objects are in the
0
+cache they are returned and if not they are fetched and added to the cache.
0
 
0
 Example
0
 =======
0
 
0
-Example goes here.
0
+belongs_to :cow, :cached => true
0
+
0
+has_many :chickens, :cached => true
0
+
0
+has_and_belongs_to_many :farmers, :cached => true
0
 
0
 
0
-Copyright (c) 2008 [name of plugin creator], released under the MIT license
0
+Copyright (c) 2008 [Charlie Morss], released under the MIT license
...
1
2
 
 
3
...
 
 
1
2
3
0
@@ -1,3 +1,3 @@
0
-require 'memcache'
0
-require 'memcache_util'
0
+# require 'memcache'
0
+# require 'memcache_util'
0
 require 'association_cache'
...
1
2
3
 
 
 
 
 
 
 
 
4
5
6
 
7
8
9
...
15
16
17
18
 
19
20
21
22
23
24
 
 
 
 
 
 
25
26
27
...
38
39
40
41
42
 
 
43
44
45
46
47
48
 
 
49
50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
52
53
...
56
57
58
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
62
63
64
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
 
67
68
69
...
99
100
101
102
103
 
 
104
105
106
...
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
 
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
 
152
153
154
...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
24
25
26
 
27
28
29
30
31
 
 
32
33
34
35
36
37
38
39
40
...
51
52
53
 
 
54
55
56
57
58
59
 
 
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
...
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
 
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
...
269
270
271
 
 
272
273
274
275
276
...
280
281
282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
286
287
288
0
@@ -1,9 +1,18 @@
0
 module ActiveRecord
0
   module AssociationCache
0
 
0
+ def self.active?
0
+ @caching_active
0
+ end
0
+
0
+ def self.active=(on)
0
+ @caching_active = on
0
+ end
0
+
0
     def self.extended(base)
0
       class << base
0
         alias_method_chain :belongs_to, :cache
0
+ alias_method_chain :has_and_belongs_to_many, :cache
0
       end
0
     end
0
 
0
@@ -15,13 +24,17 @@ module ActiveRecord
0
       belongs_to_without_cache(association_name, options)
0
 
0
       if cached
0
- association_id_name = options[:foreign_key_id] || "#{association_name}_id"
0
+ association_id_name = options[:foreign_key] || "#{association_name}_id"
0
         association_class = options[:class_name] || association_name.to_s.classify
0
 
0
         class_eval <<-END
0
           def #{association_name}_with_cache
0
- id = #{association_id_name}
0
- Cache.get("#{association_class}::\#{id}") do
0
+ if ActiveRecord::AssociationCache.active?
0
+ id = #{association_id_name}
0
+ Cache.get("#{association_class}::\#{id}") do
0
+ #{association_name}_without_cache
0
+ end
0
+ else
0
               #{association_name}_without_cache
0
             end
0
           end
0
@@ -38,16 +51,41 @@ module ActiveRecord
0
       configure_dependency_for_has_many(reflection)
0
 
0
       if options[:through]
0
- collection_reader_method(reflection, HasManyThroughAssociation)
0
- collection_accessor_methods(reflection, HasManyThroughAssociation, false)
0
+ collection_reader_method(reflection, ::ActiveRecord::Associations::HasManyThroughAssociation)
0
+ collection_accessor_methods(reflection, ::ActiveRecord::Associations::HasManyThroughAssociation, false)
0
       else
0
         add_multiple_associated_save_callbacks(reflection.name)
0
         add_association_callbacks(reflection.name, reflection.options)
0
         collection_accessor_methods(reflection,
0
- cached ? ActiveRecord::Associations::CachedHasManyAssociation :
0
- ActiveRecord::Associations::HasManyAssociation)
0
+ cached ? ::ActiveRecord::Associations::CachedHasManyAssociation :
0
+ ::ActiveRecord::Associations::HasManyAssociation)
0
       end
0
     end
0
+
0
+ def has_and_belongs_to_many_with_cache(association_id, options = {}, &extension)
0
+ cached = options.delete(:cached)
0
+
0
+ return has_and_belongs_to_many_without_cache(association_id, options, &extension) unless cached
0
+
0
+ reflection = create_has_and_belongs_to_many_reflection(association_id, options, &extension)
0
+
0
+ add_multiple_associated_save_callbacks(reflection.name)
0
+ collection_accessor_methods(reflection,
0
+ ::ActiveRecord::Associations::CachedHasAndBelongsToManyAssociation)
0
+
0
+ # Don't use a before_destroy callback since users' before_destroy
0
+ # callbacks will be executed after the association is wiped out.
0
+ old_method = "destroy_without_habtm_shim_for_#{reflection.name}"
0
+ class_eval <<-end_eval unless method_defined?(old_method)
0
+ alias_method :#{old_method}, :destroy_without_callbacks
0
+ def destroy_without_callbacks
0
+ #{reflection.name}.clear
0
+ #{old_method}
0
+ end
0
+ end_eval
0
+
0
+ add_association_callbacks(reflection.name, options)
0
+ end
0
   end
0
 end
0
 
0
@@ -56,14 +94,146 @@ ActiveRecord::Base.class_eval do
0
 end
0
 
0
 class ActiveRecord::Base
0
+ class << self
0
+ def find_with_cache(*args)
0
+ options = args.extract_options!
0
+
0
+ if [:first, :all].include?(args.first)
0
+ options[:select] = "#{quoted_table_name}.id"
0
+ results = find(*(args << options)) # make faster like for collections
0
+ if args.first == :all
0
+ CacheHelper.retrieve_records(results.map(&:id), self)
0
+ else
0
+ CacheHelper.retrieve_records([results.id], self).first
0
+ end
0
+ else
0
+ id = args.first
0
+ Cache.get("#{name}::#{id}") do
0
+ find(*(args << options))
0
+ end
0
+ end
0
+ end
0
+ end
0
+
0
+ def cache!
0
+ Cache.put(cache_key, self)
0
+ end
0
+
0
+ def remove_from_cache
0
+ Cache.delete(cache_key, self)
0
+ end
0
+
0
   def cache_key
0
- "#{self.class.name}::#{self.id}"
0
+ key_class = self.class
0
+ while key_class.superclass != ActiveRecord::Base && key_class.superclass != Object
0
+ key_class = key_class.superclass
0
+ end
0
+ "#{key_class.name}::#{self.id}"
0
+ end
0
+end
0
+
0
+class CacheHelper
0
+ class << self
0
+ def retrieve_records(ids, klass)
0
+ cache_keys = ids.map { |id| "#{klass.name}::#{id}" }
0
+ record_hash = Cache.get_multiple(cache_keys)
0
+
0
+ if record_hash.size < ids.size
0
+ record_ids = record_hash.keys.map { |k| k.gsub(/.*::/, '').to_i}
0
+ missing_record_ids = ids.select { |id| !record_ids.include?(id) }
0
+ missing_records = klass.find(:all,
0
+ :conditions => ["#{klass.quoted_table_name}.id in (?)", missing_record_ids])
0
+
0
+ missing_records.each do |record|
0
+ Cache.put(record.cache_key, record)
0
+ end
0
+ missing_records.each { |record| record_hash[record.cache_key] = record }
0
+ end
0
+
0
+ ids.collect { |id| record_hash["#{klass.name}::#{id}"] }
0
+ end
0
   end
0
 end
0
 
0
 module ActiveRecord
0
   module Associations
0
+
0
+ module AssociationHelper
0
+ def select_ids(options)
0
+ connection.select_all(
0
+ construct_finder_sql_for_ids(options), "#{name} Loading ids")
0
+ end
0
+
0
+ def construct_finder_sql_for_ids(options)
0
+ scope = scope(:find)
0
+ sql = "SELECT #{quoted_table_name}.id FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
0
+
0
+ add_conditions!(sql, options[:conditions], scope)
0
+
0
+ add_group!(sql, options[:group], scope)
0
+ add_order!(sql, options[:order], scope)
0
+ add_lock!(sql, options, scope)
0
+
0
+ return sanitize_sql(sql)
0
+ end
0
+ end
0
+
0
+ class CachedHasAndBelongsToManyAssociation < HasAndBelongsToManyAssociation
0
+
0
+ include AssociationHelper
0
+
0
+ def find(*args)
0
+ options = args.extract_options!
0
+
0
+ # If using a custom finder_sql, scan the entire collection.
0
+ # The finder_sql condition is unchanged from the superclass definition
0
+ if @reflection.options[:finder_sql]
0
+ expects_array = args.first.kind_of?(Array)
0
+ ids = args.flatten.compact.uniq
0
+
0
+ if ids.size == 1
0
+ id = ids.first.to_i
0
+ record = load_target.detect { |r| id == r.id }
0
+ expects_array ? [record] : record
0
+ else
0
+ load_target.select { |r| ids.include?(r.id) }
0
+ end
0
+ else
0
+ conditions = "#{@finder_sql}"
0
+
0
+ if sanitized_conditions = sanitize_sql(options[:conditions])
0
+ conditions << " AND (#{sanitized_conditions})"
0
+ end
0
+
0
+ options[:conditions] = conditions
0
+ options[:joins] = @join_sql
0
+ options[:readonly] = finding_with_ambiguous_select?(options[:select] || @reflection.options[:select])
0
+
0
+ if options[:order] && @reflection.options[:order]
0
+ options[:order] = "#{options[:order]}, #{@reflection.options[:order]}"
0
+ elsif @reflection.options[:order]
0
+ options[:order] = @reflection.options[:order]
0
+ end
0
+
0
+ merge_options_from_reflection!(options)
0
+
0
+ options[:select] = "#{@reflection.table_name}.id"
0
+ #
0
+ # # Pass through args exactly as we received them.
0
+ # args << options
0
+ # @reflection.klass.find(*args)
0
+
0
+ # Pass through args exactly as we received them.
0
+ args << options
0
+ ids = @reflection.klass.find(*args).map(&:id)
0
+ CacheHelper.retrieve_records(ids, @reflection.klass)
0
+ end
0
+ end
0
+ end
0
+
0
     class CachedHasManyAssociation < HasManyAssociation
0
+ include AssociationHelper
0
+
0
       def find(*args)
0
         options = args.extract_options!
0
 
0
@@ -99,8 +269,8 @@ module ActiveRecord
0
 
0
           # Pass through args exactly as we received them.
0
           args << options
0
-
0
- unless options[:join]
0
+
0
+ unless options[:join] || (scope(:find) && scope(:find)[:join])
0
             # Doing it this way will cause complex joins to break,
0
             # but its way faster then the else condition.
0
             ids = select_ids(options).map { |row| row["id"].to_i }
0
@@ -110,45 +280,9 @@ module ActiveRecord
0
             ids = @reflection.klass.find(*args).map(&:id)
0
           end
0
 
0
- cache_keys = ids.map { |id| "#{@reflection.klass.name}::#{id}" }
0
- records = Cache.get_multiple(cache_keys)
0
- record_ids = records.map(&:id)
0
-
0
- missing_record_ids = ids.select { |id| !record_ids.include?(id) }
0
-
0
- missing_records = @reflection.klass.find(:all,
0
- :conditions => ['id in (?)', missing_record_ids])
0
-
0
- missing_records.each do |record|
0
- Cache.put(record.cache_key, record)
0
- end
0
-
0
- records.concat(missing_records)
0
-
0
- # Slow sort method. Use a hash for more goodness
0
- ids.collect { |id| records.detect { |r| r.id == id } }
0
+ CacheHelper.retrieve_records(ids, @reflection.klass)
0
         end
0
- end
0
-
0
- def select_ids(options)
0
- connection.select_all(
0
- construct_finder_sql_for_ids(options), "#{name} Loading ids")
0
- end
0
-
0
- def construct_finder_sql_for_ids(options)
0
- scope = scope(:find)
0
- sql = "SELECT #{quoted_table_name}.id FROM #{(scope && scope[:from]) || options[:from] || quoted_table_name} "
0
-
0
- add_joins!(sql, options, scope)
0
- add_conditions!(sql, options[:conditions], scope)
0
-
0
- add_group!(sql, options[:group], scope)
0
- add_order!(sql, options[:order], scope)
0
- add_lock!(sql, options, scope)
0
-
0
- return sanitize_sql(sql)
0
- end
0
-
0
+ end
0
     end
0
   end
0
 end
...
7
8
9
 
 
 
 
 
10
11
 
12
13
14
...
20
21
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
24
25
...
31
32
33
34
 
35
36
37
38
39
 
 
 
 
40
41
 
42
43
44
...
47
48
49
50
 
51
52
53
 
54
55
56
57
 
58
59
60
61
 
62
63
64
65
66
67
 
68
69
70
 
71
72
73
 
74
75
 
76
77
78
 
 
79
80
81
...
84
85
86
87
 
88
89
 
90
91
92
93
94
95
96
 
97
98
99
100
 
101
102
103
 
104
105
 
106
107
108
 
109
110
111
...
118
119
120
121
 
122
123
124
...
130
131
132
133
 
 
134
135
136
137
138
 
139
140
141
...
154
155
156
157
 
158
159
160
...
7
8
9
10
11
12
13
14
15
 
16
17
18
19
...
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
...
100
101
102
 
103
104
 
 
 
 
105
106
107
108
109
 
110
111
112
113
...
116
117
118
 
119
120
121
 
122
123
124
125
 
126
127
128
129
 
130
131
132
133
134
135
 
136
137
138
 
139
140
141
 
142
143
 
144
145
 
 
146
147
148
149
150
...
153
154
155
 
156
157
 
158
159
160
161
162
163
164
 
165
166
167
168
 
169
170
171
 
172
173
 
174
175
176
 
177
178
179
180
...
187
188
189
 
190
191
192
193
...
199
200
201
 
202
203
204
205
206
207
 
208
209
210
211
...
224
225
226
 
227
228
229
230
0
@@ -7,8 +7,13 @@ class AssociationCacheTest < ActiveRecordTestCase
0
 
0
   def setup
0
     Cache.clear!
0
+ User.connection.execute("delete from users;")
0
+ Account.connection.execute("delete from accounts;")
0
+ Account.connection.execute("delete from projects;")
0
+ Account.connection.execute("delete from projects_users;")
0
+ ActiveRecord::AssociationCache.active = true
0
   end
0
-
0
+
0
   def test_uncached_belongs_to
0
     User.belongs_to :account
0
     Account.has_many :users
0
@@ -20,6 +25,70 @@ class AssociationCacheTest < ActiveRecordTestCase
0
     assert_equal(veggies, carrot.account)
0
   end
0
 
0
+ def test_find_with_cache_by_id
0
+ user = User.create!(:name => 'cow')
0
+ assert_equal(0, Cache.hits)
0
+
0
+ user = User.find_with_cache(user.id)
0
+ assert_equal(0, Cache.hits)
0
+
0
+ user = User.find_with_cache(user.id)
0
+ assert_equal(1, Cache.hits)
0
+ end
0
+
0
+ def test_non_cached_has_and_belongs_to_many
0
+ User.has_and_belongs_to_many :projects
0
+ Project.has_and_belongs_to_many :users
0
+
0
+ user = User.create!(:name => 'cow')
0
+ user.projects.create!(:name => 'milk')
0
+ user.projects.create!(:name => 'spots')
0
+ assert_equal(0, Cache.hits)
0
+
0
+ user = User.find(user.id)
0
+ assert_equal(2, user.projects.to_a.size)
0
+ assert_equal(0, Cache.hits)
0
+ end
0
+
0
+ def test_cached_has_and_belongs_to_many
0
+ User.has_and_belongs_to_many :projects, :cached => true
0
+ Project.has_and_belongs_to_many :users, :cached => true
0
+
0
+ user = User.create!(:name => 'cow')
0
+ user.projects.create!(:name => 'milk')
0
+ user.projects.create!(:name => 'spots')
0
+
0
+ user = User.find(user.id)
0
+ assert_equal(2, user.projects.to_a.size)
0
+ assert_equal(0, Cache.hits)
0
+
0
+ user = User.find(user.id)
0
+ assert_equal(2, user.projects.to_a.size)
0
+ assert_equal(2, Cache.hits)
0
+ end
0
+
0
+ def test_find_with_cache_with_conditions
0
+ user = User.create!(:name => 'cow')
0
+ assert_equal(0, Cache.hits)
0
+
0
+ users = User.find(:all, :conditions => ["name = 'cow'"])
0
+ assert_equal(1, users.size)
0
+
0
+ users = User.find_with_cache(:all, :conditions => ["name = 'cow'"])
0
+ assert_equal(1, users.size)
0
+ assert_equal('cow', users.first.name)
0
+ assert_equal(0, Cache.hits)
0
+
0
+ users = User.find_with_cache(:all, :conditions => ["name = 'cow'"])
0
+ assert_equal(1, users.size)
0
+ assert_equal('cow', users.first.name)
0
+ assert_equal(1, Cache.hits)
0
+
0
+ user = User.find_with_cache(user.id)
0
+ assert_equal('cow', users.first.name)
0
+ assert_equal(2, Cache.hits)
0
+ end
0
+
0
   def test_cached_belongs_to
0
     User.belongs_to :account, :cached => true
0
     Account.has_many :users
0
@@ -31,14 +100,14 @@ class AssociationCacheTest < ActiveRecordTestCase
0
 
0
     assert_equal(0, Cache.hits)
0
     assert_equal(veggies, carrot.account) # loads cache
0
-
0
+
0
     assert_equal(0, Cache.hits)
0
-
0
- carrot.reload
0
- assert_equal(veggies, carrot.account) # should get from cache
0
- assert_equal(1, Cache.hits)
0
+
0
+ carrot = User.find(carrot.id)
0
+ assert_equal(veggies.name, carrot.account.name) # should get from cache
0
+ assert_equal(1, Cache.hits)
0
   end
0
-
0
+
0
   def test_uncached_has_many
0
     User.belongs_to :account
0
     Account.has_many :users
0
@@ -47,35 +116,35 @@ class AssociationCacheTest < ActiveRecordTestCase
0
     veggies.users.create!(:name => 'carrot')
0
     veggies.users.create!(:name => 'parsnip')
0
     veggies.reload
0
-
0
+
0
     assert_equal(2, veggies.users.to_a.size)
0
   end
0
-
0
+
0
   def test_cached_has_many_with_none_in_cache
0
     User.belongs_to :account
0
     Account.has_many :users, :cached => true
0
-
0
+
0
     veggies = Account.create!(:name => 'veggies')
0
     veggies.users.create!(:name => 'carrot')
0
     veggies.users.create!(:name => 'parsnip')
0
- veggies.reload
0
+ veggies.reload
0
 
0
     assert_equal(0, Cache.hits)
0
     assert_equal(0, Cache.misses)
0
 
0
     assert_equal(2, veggies.users.to_a.size)
0
-
0
+
0
     assert_equal(0, Cache.hits)
0
     assert_equal(2, Cache.misses)
0
-
0
+
0
     veggies.reload
0
     Cache.reset_counters!
0
-
0
+
0
     assert_equal(2, veggies.users.to_a.size)
0
-
0
+
0
     assert_equal(2, Cache.hits)
0
- assert_equal(0, Cache.misses)
0
- end
0
+ assert_equal(0, Cache.misses)
0
+ end
0
 
0
   def test_cached_has_many_with_one_in_cache
0
     User.belongs_to :account
0
@@ -84,28 +153,28 @@ class AssociationCacheTest < ActiveRecordTestCase
0
     veggies = Account.create!(:name => 'veggies')
0
     carrot = veggies.users.create!(:name => 'carrot')
0
     Cache.put(carrot.cache_key, carrot)
0
-
0
+
0
     veggies.users.create!(:name => 'parsnip')
0
- veggies.reload
0
+ veggies.reload
0
 
0
     assert_equal(1, Cache.keys.size)
0
     assert_equal(0, Cache.hits)
0
     assert_equal(0, Cache.misses)
0
 
0
     assert_equal(2, veggies.users.to_a.size)
0
-
0
+
0
     assert_equal(2, Cache.keys.size)
0
     assert_equal(1, Cache.hits)
0
     assert_equal(1, Cache.misses)
0
-
0
+
0
     veggies.reload
0
     Cache.reset_counters!
0
-
0
+
0
     assert_equal(2, veggies.users.to_a.size)
0
-
0
+
0
     assert_equal(2, Cache.keys.size)
0
     assert_equal(2, Cache.hits)
0
- assert_equal(0, Cache.misses)
0
+ assert_equal(0, Cache.misses)
0
   end
0
 end
0
 
0
@@ -118,7 +187,7 @@ module Cache
0
     else
0
       @misses ||= 0; @misses += 1
0
     end
0
-
0
+
0
     value = yield
0
     @cache[key] = value
0
   end
0
@@ -130,12 +199,13 @@ module Cache
0
 
0
   def self.get_multiple(keys)
0
     @cache ||= {}
0
- results = keys.map { |key| @cache[key] }.compact
0
+ results = {}
0
+ keys.each { |key| results[key] = @cache[key] if @cache[key] }
0
     @misses ||= 0; @misses += keys.size - results.size
0
     @hits ||= 0; @hits += results.size
0
     results
0
   end
0
-
0
+
0
   def self.cached?(key)
0
     @cache ||= {}
0
     @cache[key]
0
@@ -154,7 +224,7 @@ module Cache
0
     @hits = 0
0
     @misses = 0
0
   end
0
-
0
+
0
   def self.hits
0
     @hits || 0
0
   end
...
22
23
24
25
 
26
27
28
...
22
23
24
 
25
26
27
28
0
@@ -22,7 +22,7 @@ CREATE TABLE 'projects' (
0
   'name' TEXT DEFAULT NULL
0
 );
0
 
0
-CREATE TABLE 'users_projects' (
0
+CREATE TABLE 'projects_users' (
0
   'user_id' INTEGER NOT NULL,
0
   'project_id' INTEGER NOT NULL
0
 );

Comments

    No one has commented yet.