volt-repo_cache 0.1.4
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.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +314 -0
- data/Rakefile +1 -0
- data/lib/volt/repo_cache.rb +6 -0
- data/lib/volt/repo_cache/association.rb +100 -0
- data/lib/volt/repo_cache/cache.rb +116 -0
- data/lib/volt/repo_cache/collection.rb +259 -0
- data/lib/volt/repo_cache/model.rb +671 -0
- data/lib/volt/repo_cache/model_array.rb +169 -0
- data/lib/volt/repo_cache/util.rb +78 -0
- data/lib/volt/repo_cache/version.rb +5 -0
- data/spec/dummy/.gitignore +9 -0
- data/spec/dummy/README.md +4 -0
- data/spec/dummy/app/main/assets/css/app.css.scss +1 -0
- data/spec/dummy/app/main/config/dependencies.rb +11 -0
- data/spec/dummy/app/main/config/initializers/boot.rb +10 -0
- data/spec/dummy/app/main/config/routes.rb +14 -0
- data/spec/dummy/app/main/controllers/main_controller.rb +27 -0
- data/spec/dummy/app/main/models/customer.rb +4 -0
- data/spec/dummy/app/main/models/order.rb +6 -0
- data/spec/dummy/app/main/models/product.rb +5 -0
- data/spec/dummy/app/main/models/user.rb +12 -0
- data/spec/dummy/app/main/views/main/about.html +7 -0
- data/spec/dummy/app/main/views/main/index.html +6 -0
- data/spec/dummy/app/main/views/main/main.html +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/app.rb +147 -0
- data/spec/dummy/config/base/index.html +15 -0
- data/spec/dummy/config/initializers/boot.rb +4 -0
- data/spec/integration/sample_integration_spec.rb +11 -0
- data/spec/sample_spec.rb +7 -0
- data/spec/spec_helper.rb +18 -0
- data/volt-repo_cache.gemspec +38 -0
- metadata +287 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9cb06385cea5ffc9a2c6518ec011d99c65957550
|
4
|
+
data.tar.gz: b88fab46ec514ac59bf7da97a84e2c3461fee23b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: ff6af4ea44e4067961a8a3fd289004736538ab165cec33c8a258ae7c5cb770cc120f9d4267adb179a1843ccd6b26df349d0b4683a51bfb324bb22f3a3199b641
|
7
|
+
data.tar.gz: e0616aded5eaff2f4079a7a28e645baf48004e12d5eaeb8119c7086b5266f7cd3ac32d76b4c0d1ea053e0889215be45ed8e73359ad35fb56922aad12aa9ae6b3
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in volt-repo_cache.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
# Optional Gems for testing/dev
|
7
|
+
|
8
|
+
# The implementation of ReadWriteLock in Volt uses concurrent ruby and ext helps performance.
|
9
|
+
gem 'concurrent-ruby-ext', '~> 0.8.0'
|
10
|
+
|
11
|
+
# Gems you use for development should be added to the gemspec file as
|
12
|
+
# development dependencies.
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Colin Gunn
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,314 @@
|
|
1
|
+
# Volt::RepoCache
|
2
|
+
|
3
|
+
- Provides client-side caching of repository (db) collections, models and their associations.
|
4
|
+
- Loads multiple associated collections (or query based subsets) into a cache.
|
5
|
+
- Buffers changes to models, collections and associations until flushed.
|
6
|
+
- Allows for flushes to be performed at model, collection or cache level.
|
7
|
+
- Provides increased associational integrity.
|
8
|
+
- Reduces the burden of promise handling in repository (db) operations.
|
9
|
+
- Is ideal for use where multiple associated models are being displayed and edited.
|
10
|
+
- Preserves standard Volt model and collection interfaces and reactivity.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'volt-repo_cache'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install volt-repo_cache
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
Assume we have a sales application with three model classes:
|
29
|
+
|
30
|
+
class Customer < Volt::Model
|
31
|
+
field :name
|
32
|
+
has_many :orders
|
33
|
+
end
|
34
|
+
|
35
|
+
class Product < Volt::Model
|
36
|
+
field :name
|
37
|
+
field :price
|
38
|
+
has_many :orders
|
39
|
+
end
|
40
|
+
|
41
|
+
class Order < Volt:Model
|
42
|
+
belongs_to :customer
|
43
|
+
belongs_to :product
|
44
|
+
field :date
|
45
|
+
field :quantity
|
46
|
+
end
|
47
|
+
|
48
|
+
Let's say we want to cache all customers, products,
|
49
|
+
and orders, the latter between some given dates.
|
50
|
+
|
51
|
+
The following code will create the cache and load it
|
52
|
+
in a controller's `index` method. We'll also add a
|
53
|
+
`before_index_remove` method to clear the cache
|
54
|
+
when leaving the page.
|
55
|
+
|
56
|
+
#### Example 1 - defining and loading a cache
|
57
|
+
|
58
|
+
class OrderController < Volt::ModelController
|
59
|
+
|
60
|
+
def index
|
61
|
+
new_cache.loaded.then do |cache|
|
62
|
+
page._cache = cache
|
63
|
+
end.fail do |errors|
|
64
|
+
flashes << errors.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def new_cache
|
69
|
+
Volt::RepoCache.new(
|
70
|
+
Volt.current_app.store,
|
71
|
+
customer: {
|
72
|
+
has_many: :orders,
|
73
|
+
}
|
74
|
+
product: {
|
75
|
+
has_many: :orders,
|
76
|
+
}
|
77
|
+
order: {
|
78
|
+
belongs_to: [:customer, :product]
|
79
|
+
where: {'$and' => [:date => {'$gte' => start_date}, :date => {'$lte' => end_date}]}
|
80
|
+
}
|
81
|
+
)
|
82
|
+
end
|
83
|
+
|
84
|
+
def before_index_remove
|
85
|
+
page._cache.clear if _cache
|
86
|
+
end
|
87
|
+
|
88
|
+
...
|
89
|
+
end
|
90
|
+
|
91
|
+
**Under Volt 0.9.7 the specification of associations
|
92
|
+
will be provided by the underlying models' class
|
93
|
+
definitions and will no longer be required in
|
94
|
+
the cache options.**
|
95
|
+
|
96
|
+
In the `index` method we only need to resolve
|
97
|
+
one promise when the cache is `loaded`.
|
98
|
+
|
99
|
+
Collections may be identified in the singular or plural
|
100
|
+
according to preference, e.g. `order:` or `orders:`,
|
101
|
+
with or without an underscore prefix.
|
102
|
+
|
103
|
+
A `where:` or `query:` option may be provided for each collection
|
104
|
+
to specify which models are loaded from the repository.
|
105
|
+
The default behaviour is to load all models in a collection.
|
106
|
+
|
107
|
+
Resolution of associations between cached models will
|
108
|
+
depend on what has been loaded into the cache for
|
109
|
+
each collection.
|
110
|
+
|
111
|
+
After the cache is loaded you can then access
|
112
|
+
collections, models and associations without
|
113
|
+
handling promise resolution or failure.
|
114
|
+
|
115
|
+
Otherwise, the interfaces to cached models and collections
|
116
|
+
largely behave as normal.
|
117
|
+
|
118
|
+
#### Example 2 - query and association resolution
|
119
|
+
|
120
|
+
# find all orders for customer 'ABC and product 'XYZ'
|
121
|
+
cache._customers.where(name: 'ABC').orders.select { |order|
|
122
|
+
order.product.name == 'XYZ'
|
123
|
+
}
|
124
|
+
|
125
|
+
Unlike a standard Volt query and association call
|
126
|
+
(`order.product`) we have no intervening promise(s) to
|
127
|
+
resolve, and also avoid relatively slow database request(s).
|
128
|
+
|
129
|
+
#### Example 3 - query and association resolution
|
130
|
+
|
131
|
+
# total cost of products ordered by customer
|
132
|
+
customer = cache._customers.where(name: 'ABC')
|
133
|
+
total_cost = customer.orders.reduce(0) do |sum, order|
|
134
|
+
sum + (order.quantity * order.product.price)
|
135
|
+
end
|
136
|
+
|
137
|
+
Again, no promises to resolve and faster calculation of
|
138
|
+
total cost than would be the case with uncached database
|
139
|
+
access.
|
140
|
+
|
141
|
+
### Changes and flushing
|
142
|
+
|
143
|
+
Changes to field values in models are buffered until
|
144
|
+
flushed (saved) to the database. Flushes may be requested
|
145
|
+
at the model, collection or cache level. Each flush
|
146
|
+
returns a single promise. Some examples:
|
147
|
+
|
148
|
+
#### Example 4 - change and save a single model
|
149
|
+
|
150
|
+
# change the price of a product and save it
|
151
|
+
product = cache._products.where(name: 'XYZ')
|
152
|
+
product.price = 9.99
|
153
|
+
# flush the product model
|
154
|
+
product.flush!.then do |result|
|
155
|
+
puts "#{result} saved"
|
156
|
+
end.fail do |errors|
|
157
|
+
puts errors
|
158
|
+
end
|
159
|
+
|
160
|
+
#### Example 5 - change and save several models in a collection
|
161
|
+
|
162
|
+
# change the price of multiple products
|
163
|
+
# and save them all together
|
164
|
+
products = cache._products
|
165
|
+
products.where(name: 'X').price = 7.77
|
166
|
+
products.where(name: 'Y').price = 8.88
|
167
|
+
products.where(name: 'Z').price = 9.99
|
168
|
+
# flush the 'products' collection
|
169
|
+
products.flush!.then do |result|
|
170
|
+
puts "all products saved"
|
171
|
+
end.fail do |errors|
|
172
|
+
puts "error saving products: #{errors}"
|
173
|
+
end
|
174
|
+
|
175
|
+
#### Example 6 - change and save models in more than one collection
|
176
|
+
|
177
|
+
# change the price of a product
|
178
|
+
# and the name of a customer
|
179
|
+
# and save them together
|
180
|
+
cache._products.where(name: 'XYZ').price = 7.77
|
181
|
+
cache._customer.where(name: 'ABC').name = 'EFG'
|
182
|
+
# flush the whole cache
|
183
|
+
cache.flush!.then do |result|
|
184
|
+
puts "cached flushed successfully"
|
185
|
+
end.fail do |errors|
|
186
|
+
puts "error flushing cache: #{errors}"
|
187
|
+
end
|
188
|
+
|
189
|
+
### Creating new models with no owners
|
190
|
+
|
191
|
+
There are two ways to create a new instance of a model
|
192
|
+
not belonging to another model:
|
193
|
+
|
194
|
+
#### Example 7 - create a new model (with no owner) via a collection
|
195
|
+
|
196
|
+
# create a new product
|
197
|
+
p = Product.new(name: 'IJK')
|
198
|
+
cache._products << p
|
199
|
+
|
200
|
+
A new model must be added to the appropriate cached collection
|
201
|
+
(using `#<<` or `#append`) before it also is cached. It will
|
202
|
+
not be saved to the database until the model or its containing
|
203
|
+
collection or cache is flushed.
|
204
|
+
|
205
|
+
NB Both `#<<` and `#append` return the collection, not the
|
206
|
+
appended model.
|
207
|
+
|
208
|
+
Another way of creating a new model via a collection using a hash:
|
209
|
+
|
210
|
+
#### Example 8 - create a new model (with no owner) via a collection
|
211
|
+
|
212
|
+
# create a new product
|
213
|
+
cache._products << {name: 'IJK'}
|
214
|
+
p = cache._products.where(name: 'IJK')
|
215
|
+
|
216
|
+
### Creating new models with owners
|
217
|
+
|
218
|
+
When creating a new model which belongs to one or more models
|
219
|
+
you must set the foreign key id(s) to establish the association(s).
|
220
|
+
|
221
|
+
#### Example 9 - create a new model (with two owners) via a collection
|
222
|
+
|
223
|
+
# create a new order which belongs to a customer and a product
|
224
|
+
product = cache._products.where(code: 'XYZ')
|
225
|
+
customer = cache._customers.where(code: 'ABC')
|
226
|
+
order = Order.new(product_id: product.id, customer_id: customer.id, quantity: 1, date: Date.today)
|
227
|
+
cache._orders << order
|
228
|
+
|
229
|
+
An easier way is ask an owner model to create a new owned model:
|
230
|
+
|
231
|
+
#### Example 10 - create a new model (with two owners) via an owner model
|
232
|
+
|
233
|
+
product = cache._products.where(code: 'XYZ')
|
234
|
+
customer = cache._customers.where(code: 'ABC')
|
235
|
+
# ask the customer to create a new order, give it the product id
|
236
|
+
order = customer.new_order(product_id: product.id, quantity: 1, date: Date.today)
|
237
|
+
|
238
|
+
### Destroying models
|
239
|
+
|
240
|
+
Models in the cache can be marked for destruction when the cache is flushed using `#mark_for_destruction!`.
|
241
|
+
Still to do - associational integrity checks when marking for destruction.
|
242
|
+
|
243
|
+
## Warnings
|
244
|
+
|
245
|
+
**Flushes to the underlying repository are not atomic and cannot be rolled back**.
|
246
|
+
If part of the cache/collection/model/association
|
247
|
+
flush fails the transaction(s) may lose integrity.
|
248
|
+
|
249
|
+
The cached models and collections contain circular references
|
250
|
+
(the models refer to the collection which contains them and
|
251
|
+
collections refer to the cache). Not being sure what the
|
252
|
+
implications are for efficient garbage collection (in Ruby
|
253
|
+
on the server and Javascript on the client), a method
|
254
|
+
is provided to clear the cache when it is no longer required,
|
255
|
+
breaking all internal (circular) references.
|
256
|
+
|
257
|
+
## TODO
|
258
|
+
|
259
|
+
1. Use associations_data in Volt::Models when 0.9.7 (sql) version available.
|
260
|
+
2. Handle non-standard collection, foreign_key and local_key Volt model options.
|
261
|
+
3. Association integrity checks on mark_for_destruction!
|
262
|
+
4. Test spec.
|
263
|
+
5. Locking?
|
264
|
+
6. Atomic transactions?
|
265
|
+
7. Removal of circular references?
|
266
|
+
|
267
|
+
## Contributing and use
|
268
|
+
|
269
|
+
This gem was written as part of the development of a production
|
270
|
+
application, primarily to speed up processing requiring many
|
271
|
+
implicit database queries (across associated collections), as well
|
272
|
+
as simplifying association management and reducing the burden of
|
273
|
+
asynchronous promise resolution.
|
274
|
+
|
275
|
+
It works well enough for our current application's needs,
|
276
|
+
but it may not be suitable for all requirements.
|
277
|
+
|
278
|
+
We will look at extending the cache framework to support locking and
|
279
|
+
atomic transactions (with rollback), but in the meantime if you have
|
280
|
+
a need or interest in this area your suggestions and contributions
|
281
|
+
are very welcome.
|
282
|
+
|
283
|
+
To contribute:
|
284
|
+
|
285
|
+
1. Fork it ( http://github.com/[my-github-username]/volt-repo_cache/fork )
|
286
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
287
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
288
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
289
|
+
5. Create new Pull Request
|
290
|
+
|
291
|
+
## License
|
292
|
+
|
293
|
+
Copyright (c) 2015 Colin Gunn
|
294
|
+
|
295
|
+
MIT License
|
296
|
+
|
297
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
298
|
+
a copy of this software and associated documentation files (the
|
299
|
+
"Software"), to deal in the Software without restriction, including
|
300
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
301
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
302
|
+
permit persons to whom the Software is furnished to do so, subject to
|
303
|
+
the following conditions:
|
304
|
+
|
305
|
+
The above copyright notice and this permission notice shall be
|
306
|
+
included in all copies or substantial portions of the Software.
|
307
|
+
|
308
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
309
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
310
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
311
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
312
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
313
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
314
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module Volt
|
2
|
+
module RepoCache
|
3
|
+
class Association
|
4
|
+
include Volt::RepoCache::Util
|
5
|
+
|
6
|
+
attr_reader :local_name_singular, :local_name_plural
|
7
|
+
attr_reader :local_collection
|
8
|
+
attr_reader :foreign_name, :foreign_collection_name
|
9
|
+
attr_reader :foreign_model_class_name, :foreign_model_class
|
10
|
+
attr_reader :type, :foreign_id_field, :local_id_field
|
11
|
+
|
12
|
+
def initialize(local_collection, foreign_name, type)
|
13
|
+
_local_name = local_collection.name.to_s.sub(/^_/, '')
|
14
|
+
@local_name_singular = _local_name.singularize.to_sym
|
15
|
+
@local_name_plural = _local_name.pluralize.to_sym
|
16
|
+
@local_collection = local_collection
|
17
|
+
@foreign_name = foreign_name
|
18
|
+
@type = type
|
19
|
+
@foreign_model_class_name = @foreign_name.to_s.singularize.camelize
|
20
|
+
@foreign_model_class = Object.const_get(@foreign_model_class_name)
|
21
|
+
@foreign_collection_name = :"_#{@foreign_name.to_s.pluralize}"
|
22
|
+
@foreign_id_field = has_any? ? :"#{@local_collection.model_class_name.underscore}_id" : :id
|
23
|
+
@local_id_field = belongs_to? ? :"#{@foreign_name.to_s}_id" : :id
|
24
|
+
end
|
25
|
+
|
26
|
+
# Hide circular references to local
|
27
|
+
# and foreign collections for inspection.
|
28
|
+
def inspect
|
29
|
+
__local = @local_collection
|
30
|
+
__foreign = @foreign_collection
|
31
|
+
@local_collection = "{{#{@local_collection ? @local_collection.name : :nil}}"
|
32
|
+
@foreign_collection = "{{#{@foreign_collection ? @foreign_collection.name : :nil}}"
|
33
|
+
result = super
|
34
|
+
@local_collection = __local
|
35
|
+
@foreign_collection = __foreign
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def cache
|
40
|
+
@local_collection.cache
|
41
|
+
end
|
42
|
+
|
43
|
+
# Must be lazy initialization since we
|
44
|
+
# don't know order in which collections
|
45
|
+
# will be loaded to cache.
|
46
|
+
def foreign_collection
|
47
|
+
@foreign_collection ||= cache.collections[@foreign_collection_name]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns the reciprocal association
|
51
|
+
# which may be nil if the foreign_collection
|
52
|
+
# is not interested (has not specified)
|
53
|
+
# the reciprocal association.
|
54
|
+
# It may be, for example, that this association
|
55
|
+
# is a belongs_to, but there is no reciprocal
|
56
|
+
# has_one or has_many association in the 'owner'.
|
57
|
+
# Must be lazy initialization since it depends on
|
58
|
+
# foreign_collection being lazily initialized.
|
59
|
+
def reciprocal
|
60
|
+
unless @reciprocal
|
61
|
+
# debug __method__, __LINE__, ""
|
62
|
+
@reciprocal = foreign_collection.associations.values.detect do |a|
|
63
|
+
# debug __method__, __LINE__, "#{a.foreign_collection.name} ?==? #{local_collection.name}"
|
64
|
+
a.foreign_collection.name == local_collection.name
|
65
|
+
end
|
66
|
+
@reciprocal = :nil unless @reciprocal
|
67
|
+
# debug __method__, __LINE__, "reciprocal of #{self.inspect} is #{@reciprocal.inspect}"
|
68
|
+
end
|
69
|
+
@reciprocal == :nil ? nil : @reciprocal
|
70
|
+
end
|
71
|
+
|
72
|
+
def reciprocated?
|
73
|
+
!!reciprocal
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_one?
|
77
|
+
type == :has_one
|
78
|
+
end
|
79
|
+
|
80
|
+
def has_many?
|
81
|
+
type == :has_many
|
82
|
+
end
|
83
|
+
|
84
|
+
def has_any?
|
85
|
+
has_one? || has_many?
|
86
|
+
end
|
87
|
+
|
88
|
+
def belongs_to?
|
89
|
+
type == :belongs_to
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def uncache
|
95
|
+
@local_collection = @foreign_collection = @reciprocal = nil
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|