redisted 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/Gemfile +16 -0
- data/Guardfile +31 -0
- data/README.md +643 -0
- data/Rakefile +1 -0
- data/config/application.rb +59 -0
- data/config/boot.rb +6 -0
- data/config/environment.rb +7 -0
- data/config/redis.rb +17 -0
- data/config/redis.yml +0 -0
- data/index.html +1 -0
- data/lib/redisted/base.rb +129 -0
- data/lib/redisted/delete.rb +21 -0
- data/lib/redisted/errors.rb +12 -0
- data/lib/redisted/fields.rb +59 -0
- data/lib/redisted/getsetattr.rb +220 -0
- data/lib/redisted/index.rb +330 -0
- data/lib/redisted/instantiate.rb +34 -0
- data/lib/redisted/references.rb +105 -0
- data/lib/redisted/relation.rb +190 -0
- data/lib/redisted/version.rb +3 -0
- data/lib/redisted.rb +10 -0
- data/log/development.log +0 -0
- data/log/test.log +0 -0
- data/readme.md +643 -0
- data/redisted.gemspec +23 -0
- data/spec/redisted/callbacks_spec.rb +76 -0
- data/spec/redisted/delete_destroy_spec.rb +49 -0
- data/spec/redisted/fields_spec.rb +118 -0
- data/spec/redisted/filter_index_spec.rb +90 -0
- data/spec/redisted/find_spec.rb +30 -0
- data/spec/redisted/log/test.log +0 -0
- data/spec/redisted/match_index_spec.rb +36 -0
- data/spec/redisted/persisted_fields_spec.rb +238 -0
- data/spec/redisted/recalculate_index_spec.rb +6 -0
- data/spec/redisted/references_spec.rb +128 -0
- data/spec/redisted/scopes_spec.rb +88 -0
- data/spec/redisted/unique_index_spec.rb +113 -0
- data/spec/redisted/validations_spec.rb +71 -0
- data/spec/spec_helper.rb +61 -0
- metadata +110 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in redisted.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
group :development,:test do
|
7
|
+
gem 'rails', '3.1.0'
|
8
|
+
gem 'rspec'
|
9
|
+
gem 'rspec-rails'
|
10
|
+
gem 'capybara'
|
11
|
+
gem 'guard-rspec'
|
12
|
+
gem 'growl'
|
13
|
+
gem 'spork','> 0.9.0.rc'
|
14
|
+
gem 'guard-spork'
|
15
|
+
gem "pry"
|
16
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do
|
5
|
+
watch('config/application.rb')
|
6
|
+
watch('config/environment.rb')
|
7
|
+
watch(%r{^config/environments/.+\.rb$})
|
8
|
+
watch(%r{^config/initializers/.+\.rb$})
|
9
|
+
watch('spec/spec_helper.rb')
|
10
|
+
watch(%r{^spec/support/.+\.rb$})
|
11
|
+
end
|
12
|
+
|
13
|
+
guard 'rspec', :version => 2, :cli=>"--drb", :all_on_start=>false, :all_on_pass=>false do
|
14
|
+
watch(%r{^spec/.+_spec\.rb$})
|
15
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
16
|
+
watch('spec/spec_helper.rb') { "spec" }
|
17
|
+
|
18
|
+
# Rails example
|
19
|
+
watch(%r{^spec/.+_spec\.rb$})
|
20
|
+
watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
21
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
22
|
+
watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
|
23
|
+
watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
|
24
|
+
watch('spec/spec_helper.rb') { "spec" }
|
25
|
+
watch('config/routes.rb') { "spec/routing" }
|
26
|
+
watch('app/controllers/application_controller.rb') { "spec/controllers" }
|
27
|
+
# Capybara request specs
|
28
|
+
watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
|
29
|
+
end
|
30
|
+
|
31
|
+
|
data/README.md
ADDED
@@ -0,0 +1,643 @@
|
|
1
|
+
PLEASE NOTE
|
2
|
+
===========
|
3
|
+
|
4
|
+
This is still ***UNDER DEVELOPMENT*** and not yet ready for prime-time. Feel free to fool around with it, file bug
|
5
|
+
reports (via GitHub), and make suggestions. It is a work in progress. However, I do hope to have a fully
|
6
|
+
flushed out implementation as soon as possible.
|
7
|
+
|
8
|
+
|
9
|
+
Redisted
|
10
|
+
========
|
11
|
+
|
12
|
+
Redisted provides higher level model functionality for redis databases. It is not intended as an ActiveRecord
|
13
|
+
plugin. If you want to use ActiveRecord, use a SQL database or something like Mongoid. Redisted is designed
|
14
|
+
to provide model symatics but customized and optimized for redis data store.
|
15
|
+
|
16
|
+
Do not expect ActiveRecord compatibility, but expect ActiveRecord-like capabilities highly taylored and customized to
|
17
|
+
take best advantage of Redis.
|
18
|
+
|
19
|
+
Installing
|
20
|
+
==========
|
21
|
+
|
22
|
+
TODO
|
23
|
+
|
24
|
+
Configuring
|
25
|
+
===========
|
26
|
+
|
27
|
+
TODO
|
28
|
+
|
29
|
+
Creating Models
|
30
|
+
===============
|
31
|
+
A basic Redisted model looks like this:
|
32
|
+
|
33
|
+
class MyModel < Redisted::Base
|
34
|
+
end
|
35
|
+
|
36
|
+
Within the model, you specify fields, indices, relations, scopes, and additional model functionality, just like
|
37
|
+
you do with ActiveRecord models.
|
38
|
+
|
39
|
+
Instantiating Instances
|
40
|
+
=======================
|
41
|
+
Creating an instance is as simple as new or create:
|
42
|
+
|
43
|
+
# Creating an instance in memory only
|
44
|
+
obj=MyModel.new
|
45
|
+
|
46
|
+
# Creating and persisting an instance to Redis:
|
47
|
+
obj=MyModel.create
|
48
|
+
puts obj.id # <<<---An 'id' is created when it is persisted to Redis.
|
49
|
+
|
50
|
+
# Creating in memory, then later saving to Redis:
|
51
|
+
obj=MyModel.new
|
52
|
+
...
|
53
|
+
obj.save
|
54
|
+
puts obj.id # <<<---The 'id' was created at time of 'save'
|
55
|
+
|
56
|
+
Fields
|
57
|
+
======
|
58
|
+
Redis is a basic key/value store that is inheritantly schema-less. Redisted continues this by allowing the dynamic
|
59
|
+
specification fields without having to create a static schema that must be migrated. Simply adding a field
|
60
|
+
to the model specification makes it available. No database migration is necessary.
|
61
|
+
|
62
|
+
To specify a field in Redisted, you use the field command. This command looks like this:
|
63
|
+
|
64
|
+
field :field_name,type: :field_type
|
65
|
+
|
66
|
+
Field type can be one of :string, :integer, or :datetime. Here is an example class with fields defined:
|
67
|
+
|
68
|
+
class MyModel < Redisted::Base
|
69
|
+
field :name, type: :string
|
70
|
+
field :size, type: :integer
|
71
|
+
field :created, type: :datetime
|
72
|
+
end
|
73
|
+
|
74
|
+
Instances of a Redisted model are stored in a single Redis hash. The name of the key is based on the name of the
|
75
|
+
model and the ID associated with that model. For example, a MyModel instance (such as above), with an ID value
|
76
|
+
of 1234 would be stored in a hash under the key:
|
77
|
+
|
78
|
+
mymodel:1234
|
79
|
+
|
80
|
+
Individual fields within the hash would correspond to each field within the model. Each field is stored in Redis as
|
81
|
+
a string, and Redisted deals with converting that string to/from the desired field types, defined above.
|
82
|
+
|
83
|
+
Destroying an instance of a Redisted model is as simple as deleting the corresponding Redis key.
|
84
|
+
|
85
|
+
Reading/Writing Fields
|
86
|
+
----------------------
|
87
|
+
Reading/writing a value to a field is as simple as using the included accessors:
|
88
|
+
|
89
|
+
class MyModel < Redisted::Base
|
90
|
+
field :name, type: :string
|
91
|
+
field :size, type: :integer
|
92
|
+
field :created, type: :datetime
|
93
|
+
end
|
94
|
+
|
95
|
+
obj=MyModel.create
|
96
|
+
obj.name="My Name"
|
97
|
+
puts "Name: #{obj.name}"
|
98
|
+
|
99
|
+
If the object is persisted to Redis (using 'create' to make the object or a previous call to 'save', and therefore it has an 'id'), then
|
100
|
+
by default reading/writing attributes read and write the value directly from Redis. For instance, using the above model:
|
101
|
+
|
102
|
+
obj=MyModel.create
|
103
|
+
obj.name="MyName" # Issues Redis call: HSET my_model[123] name "MyName"
|
104
|
+
puts "Name: #{obj.name}" # Issues Redis call: HGET my_model[123] name
|
105
|
+
|
106
|
+
To save round trip latency to Redis when you are making several changes to an instance, you can cache the changes:
|
107
|
+
|
108
|
+
obj=MyModel.create
|
109
|
+
obj.cache do
|
110
|
+
obj.name="MyName"
|
111
|
+
obj.size=123
|
112
|
+
obj.size=obj.size+222
|
113
|
+
end # Issues Redis call: HMSET my_model[112334] name "MyName" size "345"
|
114
|
+
|
115
|
+
Or:
|
116
|
+
|
117
|
+
obj=MyModel.create
|
118
|
+
obj.cache
|
119
|
+
obj.name="MyName"
|
120
|
+
obj.size=123
|
121
|
+
obj.size=obj.size+222
|
122
|
+
obj.save # Issues Redis call: HMSET my_model[112334] name "MyName" size "345"
|
123
|
+
|
124
|
+
You can also pass in a hash to the create call:
|
125
|
+
|
126
|
+
obj=MyModel.create({name: "MyName", size: 123}) # Issue Redis call:HMSETmy_model[112334] name "MyName" size "123"
|
127
|
+
|
128
|
+
When an object has not yet been persisted to disk (has no 'id'), this is the normal behavior:
|
129
|
+
|
130
|
+
obj=MyModel.new # <<<---Caching is on until the first save...
|
131
|
+
obj.name="MyName"
|
132
|
+
obj.size=123
|
133
|
+
obj.size=obj.size+222
|
134
|
+
obj.save # Issues Redis call: HMSET my_model[123] name "MyName" size "345"
|
135
|
+
obj.name="MyName2" # <<<---This now reverts to normal, non-caching functionality and issues immediately: HSET my_model[123] name "MyName2"
|
136
|
+
|
137
|
+
You can change the default persisting strategy for an entire class within the class definition:
|
138
|
+
|
139
|
+
class MyModel < Redisted::Base
|
140
|
+
always_cache_until_save # <<<--- Tells Redisted to always cache each change until 'save' is called
|
141
|
+
field :name, type: :string
|
142
|
+
field :size, type: :integer
|
143
|
+
field :created, type: :datetime
|
144
|
+
end
|
145
|
+
obj=MyModel.create
|
146
|
+
obj.cache
|
147
|
+
obj.name="MyName"
|
148
|
+
obj.size=123
|
149
|
+
obj.size=obj.size+222
|
150
|
+
obj.save # Issues Redis call: HMSET my_model[123] name "MyName" size "345"
|
151
|
+
|
152
|
+
For reading, the value is first checked to see if it has already been cached (by a previous read or write). If not, then
|
153
|
+
it will be read directly from Redis:
|
154
|
+
|
155
|
+
obj=MyModel.find(123) #<<<--- Assumes an object with this 'id' was previously created
|
156
|
+
puts obj.name # Issues Redis call: HGET my_model[123] name
|
157
|
+
puts obj.name # Reads from cache...no Redis call made
|
158
|
+
obj.name="MyName" # Issues redis call: HSET my_model[123] name "MyName"
|
159
|
+
puts obj.name # Reads from cache...no Redis call made
|
160
|
+
|
161
|
+
You can flush the cache and force a reread from Redis at any time:
|
162
|
+
|
163
|
+
obj=MyModel.find(123) #<<<--- Assumes an object with this 'id' was previously created
|
164
|
+
puts obj.name # Issues Redis call: HGET my_model[123] name
|
165
|
+
puts obj.name # Reads from cache...no Redis call made
|
166
|
+
obj.name="MyName" # Issues redis call: HSET my_model[123] name "MyName"
|
167
|
+
puts obj.name # Reads from cache...no Redis call made
|
168
|
+
obj.flush
|
169
|
+
puts obj.name # Issues Redis call: HGET my_model[123] name
|
170
|
+
|
171
|
+
Cache Pre-Read
|
172
|
+
--------------
|
173
|
+
You can force an object to always pre-read all values into the cache at object create by specifying an object in the
|
174
|
+
model class:
|
175
|
+
|
176
|
+
class MyModel < Redisted::Base
|
177
|
+
cache_on_first_get # <<<--- Tells Redisted to always cache when an object is instantiated from Redis
|
178
|
+
field :name, type: :string
|
179
|
+
field :size, type: :integer
|
180
|
+
field :created, type: :datetime
|
181
|
+
end
|
182
|
+
obj=MyModel.find(123) # Issues Redisc all: HMGET my_model[123] name size created
|
183
|
+
puts obj.name # Reads from cache...no Redis call made
|
184
|
+
|
185
|
+
You can specify which keys you want to pre-read in a couple different ways:
|
186
|
+
|
187
|
+
pre_cache_all # Pre-read all keys
|
188
|
+
pre_cache_all keys: :all # Same as above
|
189
|
+
pre_cache_all keys: [:name,:size] # Only pre-read name &size, and not created (in this example)
|
190
|
+
pre_cache_all except: [:content] # Only pre-read name &size, and not created (in this example)
|
191
|
+
|
192
|
+
You can specify when the pre-read occurs individually:
|
193
|
+
|
194
|
+
pre_cache_all when: :create # Load the cache when the object is first created (via find, or any other way)
|
195
|
+
pre_cache_all when: :first_read # Defer loading the cache until the first pre-cachable field is read from the object, then load the entire cache
|
196
|
+
|
197
|
+
List of Fields
|
198
|
+
--------------
|
199
|
+
You can get a list of available fields from either a class or an object as follows:
|
200
|
+
|
201
|
+
obj=MyModel.new
|
202
|
+
field_list=MyModel.fields
|
203
|
+
or:
|
204
|
+
field_list=obj.fields
|
205
|
+
|
206
|
+
This will return a hash with each key representing a field, and the value being a list of options provided during the
|
207
|
+
field create. For instance:
|
208
|
+
|
209
|
+
class MyModel < Redisted::Base
|
210
|
+
field :name, type: :string
|
211
|
+
field :size, type: :integer, default: 0
|
212
|
+
field :created, type: :datetime
|
213
|
+
end
|
214
|
+
field_list=MyModel.fields
|
215
|
+
|
216
|
+
This will produce the following hash:
|
217
|
+
|
218
|
+
{
|
219
|
+
name: {
|
220
|
+
type: :string
|
221
|
+
},
|
222
|
+
size: {
|
223
|
+
type: :integer,
|
224
|
+
default: 0
|
225
|
+
},
|
226
|
+
created: {
|
227
|
+
type: :datetime
|
228
|
+
},
|
229
|
+
}
|
230
|
+
|
231
|
+
Note that there may be other values in field-specific hash, such as default option values, etc., and the list may
|
232
|
+
change over time. You should only assume that values you specifically add to the field definition appear in this
|
233
|
+
hash, and that other values may or may not be present.
|
234
|
+
|
235
|
+
Basic Find
|
236
|
+
==========
|
237
|
+
|
238
|
+
The easiest way to locate and open an object from Redis is to use find. Find expects one parameter, the 'id' of the object
|
239
|
+
you wish to open:
|
240
|
+
|
241
|
+
class MyModel < Redisted::Base
|
242
|
+
field :name, type: :string
|
243
|
+
field :size, type: :integer, default: 0
|
244
|
+
field :created, type: :datetime
|
245
|
+
end
|
246
|
+
obj=MyModel.find(1963)
|
247
|
+
puts obj.id # Returns 1963
|
248
|
+
puts obj.name # Returns the name of the object with 1963
|
249
|
+
...
|
250
|
+
|
251
|
+
You can also pass an array of 'id' values, and find will return an array of objects:
|
252
|
+
|
253
|
+
class MyModel < Redisted::Base
|
254
|
+
field :name, type: :string
|
255
|
+
field :size, type: :integer, default: 0
|
256
|
+
field :created, type: :datetime
|
257
|
+
end
|
258
|
+
objs=MyModel.find([1963,1982,1994])
|
259
|
+
puts objs[0].id # Returns 1963
|
260
|
+
puts objs[0].name # Returns the name of object with id 1963
|
261
|
+
puts objs[1].id # Returns 1982
|
262
|
+
puts objs[1].name # Returns the name of object with id 1982
|
263
|
+
puts objs[2].id # Returns 1994
|
264
|
+
puts objs[2].name # Returns the name of object with id 1994
|
265
|
+
|
266
|
+
Delete and Destroy
|
267
|
+
==================
|
268
|
+
|
269
|
+
You can delete an object by calling the delete method on an instance:
|
270
|
+
|
271
|
+
class MyModel < Redisted::Base
|
272
|
+
field :name, type: :string
|
273
|
+
field :size, type: :integer, default: 0
|
274
|
+
end
|
275
|
+
obj=MyModel.create({name: "test", size: 15}) # Redis call: HMSET my_model:1122 name "test" size "15"
|
276
|
+
obj.delete # Redis call: DEL my_model:1122
|
277
|
+
|
278
|
+
Destroy does the same thing, but you can specify standard callbacks to be run (see callbacks, below).
|
279
|
+
|
280
|
+
Indices
|
281
|
+
=======
|
282
|
+
|
283
|
+
Indices in Redisted are different than in other similar packages. Indices are *required* in order to filter, sort, or
|
284
|
+
determine uniqueness of items in a model. The entire filter/sort UI is dependent on the creation of indices.
|
285
|
+
|
286
|
+
Unique Index
|
287
|
+
------------
|
288
|
+
|
289
|
+
A unique index is an index that specifies a field or group of fields who's value must be unique across all items
|
290
|
+
of the given model. It is specified as such:
|
291
|
+
|
292
|
+
class MyModel < Redisted::Base
|
293
|
+
field :name, type: :string
|
294
|
+
field :test_int, type: :integer
|
295
|
+
|
296
|
+
index :name, unique: true
|
297
|
+
end
|
298
|
+
|
299
|
+
In this example, all instances of MyModel must maintain a unique "name" field. This information is persisted in
|
300
|
+
Redis using a *SET*. The set is stored in a key specific to the model and the index. The redis key for the "name"
|
301
|
+
unique index in Redis for the model above may look like this:
|
302
|
+
|
303
|
+
SET: my_model[name]
|
304
|
+
|
305
|
+
Every time a new MyModel instance is persisted, a member is added to this SET. Everytime it is deleted, the corresponding
|
306
|
+
member is deleted. Each time the value of "name" changes, the old member is removed and a new one is added. All of this
|
307
|
+
occurs atomically using optimistic locking on this set key and the model hash key.
|
308
|
+
|
309
|
+
If you try and create/persiste an instance of the model where the "name"is not unique, an exception will be raised.
|
310
|
+
If you are performing saves one change at a time (the default), the check is made at the time when the assignment
|
311
|
+
is made to the name field. If you are using cached writes, it is performed at the time of the save.
|
312
|
+
|
313
|
+
The above syntax creates a unique index named "name" that is unique across the single field "name". You can specify
|
314
|
+
unique indices in other ways as well:
|
315
|
+
|
316
|
+
index :ti, unique: :test_int
|
317
|
+
# Creates an index named "ti" that is unique across the field "test_int"
|
318
|
+
index :idxkey, unique: [:test_int,:name]
|
319
|
+
# Creates an index named "idxkey" that is unique across "test_int"/"name" field pair (the pair of values must be
|
320
|
+
# unique).
|
321
|
+
|
322
|
+
The index name must be unique across all indexes for a given model.
|
323
|
+
|
324
|
+
|
325
|
+
Filter/Sort Index
|
326
|
+
-----------------
|
327
|
+
A filter/sort index is an index that can be used to return a filtered set of items and/or sorted in a particular way.
|
328
|
+
|
329
|
+
Here is a simple example:
|
330
|
+
|
331
|
+
class MyModel < Redisted::Base
|
332
|
+
field :name, type: :string
|
333
|
+
field :test_int, type: :integer
|
334
|
+
|
335
|
+
index :odd, includes: ->(elem){(elem.test_int%2)!=0}
|
336
|
+
index :even, includes: ->(elem){(elem.test_int%2)==0}
|
337
|
+
end
|
338
|
+
|
339
|
+
This model creates two indexes, one that contains a list of all model instances where test_int is odd, the other
|
340
|
+
contains a list of all model instances where test_int is even.
|
341
|
+
|
342
|
+
With the above specification, you can then use the following commands:
|
343
|
+
|
344
|
+
MyModel.by_odd.all # Returns an array of all instances of MyModel where test_int is odd...
|
345
|
+
MyModel.by_even.all # Returns an array of all instances of MyModel where test_int is even...
|
346
|
+
|
347
|
+
The order of the returned results is not specified. Using this example, the indexes are maintained in SORTED
|
348
|
+
SETS (ZADD/ZREM/etc.). The name of the keys are:
|
349
|
+
|
350
|
+
my_model[odd]
|
351
|
+
my_model[even]
|
352
|
+
|
353
|
+
Each key will contain a member referring to the 'id' value of the instance that is contained in this index. For
|
354
|
+
this example, the sort "score" is set to 1 (more on this later). Each time an instance is created or modified, the
|
355
|
+
lambda function for the index specified in the model declation is executed. If the function returns true, then an
|
356
|
+
entry for this instance is added to the index. If it is false, any existing entry for this instance is removed. When
|
357
|
+
an instance is deleted, it's entry (if it exists) is deleted.
|
358
|
+
|
359
|
+
You may specify a sort index as follows:
|
360
|
+
|
361
|
+
class MyModel < Redisted::Base
|
362
|
+
field :name, type: :string
|
363
|
+
field :test_int, type: :integer
|
364
|
+
|
365
|
+
index :asc, order: ->(elem){elem.test_int},fields: :test_int
|
366
|
+
end
|
367
|
+
|
368
|
+
This creates an index named asc, that is implemented as a SORTED SET with the following key:
|
369
|
+
|
370
|
+
my_model[asc]
|
371
|
+
|
372
|
+
In this case, *all* instances of MyModel have their 'id' value inserted as a member in this set, and their sort
|
373
|
+
"weight" is set to the value returned by the lambda function (order:).
|
374
|
+
|
375
|
+
With the above specification, when you use the index like so:
|
376
|
+
|
377
|
+
MyModel.by_asc.all
|
378
|
+
|
379
|
+
You will get an array of all objects in the model, sorted in ascending order by "test_int".
|
380
|
+
|
381
|
+
Note that the lambda function can perform any calculation and return any result. For a reverse sort, you can simply
|
382
|
+
put a minus sign in front of the equation.
|
383
|
+
|
384
|
+
While the lambda can contain any information, the value of the Redis "weight" (as returned by the method) is only
|
385
|
+
recalculated if one of the fields specified in the fields: parameter (which may be an array of fields) has changed
|
386
|
+
values. Thus, it's important to include in this array all fields that are used for the order calculation.
|
387
|
+
|
388
|
+
An index may be both a filter and a sort index, by combining the syntax:
|
389
|
+
|
390
|
+
index :sortedodd,includes: ->(elem){(elem.test_int%2)!=0}, order: ->(elem){elem.test_int}, fields: :test_int
|
391
|
+
|
392
|
+
In this case, the entry will only be included in the index if the "includes" lambda returns true, and when inserted
|
393
|
+
it will be given a sort "weight" of the value returned by the "order" lambda. The "weight"is only recalculated if
|
394
|
+
the "test_int" field changes value.
|
395
|
+
|
396
|
+
When using indexes, you can mix and match them in a single call. For instance, using all of the above indexes, you can
|
397
|
+
perform the following lookups:
|
398
|
+
|
399
|
+
MyModel.by_odd.all
|
400
|
+
# Returns an array of all instances of MyModel where test_int is odd...
|
401
|
+
MyModel.by_asc.all
|
402
|
+
# Returns all instances of MyModel sorted by test_int.
|
403
|
+
MyModel.by_odd.by_asc.all
|
404
|
+
# Returns all instances of MyModel where test_int is odd, sorted by test_int.
|
405
|
+
MyModel.by_sortedodd.all
|
406
|
+
# Same as above: Returns all instances of MyModel where test_int is odd, sorted by test_int.
|
407
|
+
|
408
|
+
Match Index
|
409
|
+
-----------
|
410
|
+
|
411
|
+
The above indexes provide a very high performant way of doing static filters. It works well when the specific
|
412
|
+
queries are known in advance. This is because the indexes contain a specific set of static keys, and membership in the
|
413
|
+
index is determined at the time the model is saved.
|
414
|
+
|
415
|
+
When possible, these indexes are efficient and powerful. However, it's not always possible to do a query this way.
|
416
|
+
|
417
|
+
That is why there are match indexes. A match index is used when a value needed for the query is not known ahead
|
418
|
+
of time (at model save time). Here is the declaration of a match index:
|
419
|
+
|
420
|
+
class MyModel < Redisted::Base
|
421
|
+
field :name, type: :string
|
422
|
+
field :test_int, type: :string
|
423
|
+
field :provider, type: :string
|
424
|
+
|
425
|
+
index :provider, match: ->(elem){(elem.provider)}
|
426
|
+
end
|
427
|
+
|
428
|
+
With an index like this, you can perform queries such as the following:
|
429
|
+
|
430
|
+
a=MyModel.by_provider("cable").all
|
431
|
+
or:
|
432
|
+
a=MyModel.by_provider("satellite").all
|
433
|
+
|
434
|
+
At model creation/save time, several SORTED SETS are created for this index, one for each unique value returned
|
435
|
+
by the various objects when they call the "match" lambda. For instance, in the above example, if the only values
|
436
|
+
that "provider" is ever set to are "cable", "satellite", or "overair", then the following keys are generated:
|
437
|
+
|
438
|
+
my_model[provider]:cable
|
439
|
+
my_model[provider]:satellite
|
440
|
+
my_model[provider]:overair
|
441
|
+
|
442
|
+
Each key is a SORTED SET who's members are the 'id' values of objects where the "match" lambda returns the specified
|
443
|
+
value. So, when you call 'by_provider("cable")', you include the 'my_model[provider]:cable' key in the query.
|
444
|
+
|
445
|
+
The individual keys are created the first time a value is returned by the "match" function (i.e., when the first
|
446
|
+
member is inserted). If a query is run for a value that does not yet have a key, such as the following:
|
447
|
+
|
448
|
+
MyModel.by_provider("xyzzy").all
|
449
|
+
|
450
|
+
then the query will act as if it returns an empty set (which it is).
|
451
|
+
|
452
|
+
|
453
|
+
|
454
|
+
Incomplete Queries
|
455
|
+
------------------
|
456
|
+
Just like in ActiveRecord, you can store intermediate results of a nested query, and use the results later. For
|
457
|
+
instance, each of these does the exact same thing:
|
458
|
+
|
459
|
+
In line:
|
460
|
+
|
461
|
+
MyModel.by_odd.by_asc.all
|
462
|
+
|
463
|
+
Stored in a variable each step:
|
464
|
+
|
465
|
+
a=MyModel.by_odd
|
466
|
+
a=a.by_asc
|
467
|
+
a.all
|
468
|
+
|
469
|
+
Storing the query and processing it later:
|
470
|
+
|
471
|
+
a=MyModel.by_odd.by_asc
|
472
|
+
...
|
473
|
+
a.all
|
474
|
+
|
475
|
+
Starting with a blank query:
|
476
|
+
|
477
|
+
a=MyModel.scoped
|
478
|
+
a=a.by_odd
|
479
|
+
a=a.by_asc
|
480
|
+
a.all
|
481
|
+
|
482
|
+
The query is performed by calling ZINTERSTORE on each of the sorted sets. However, these ZINTERSTORE calls are not
|
483
|
+
performed until the ".all" is executed. This way you can setup a query in a controller, for instance, and it only
|
484
|
+
will execute if the view issues the ".all" on it.
|
485
|
+
|
486
|
+
Besides ".all", you can also use ".each":
|
487
|
+
|
488
|
+
MyModel.by_odd.by_asc.each do |model|
|
489
|
+
# 'model' contains each instance that matches the query
|
490
|
+
end
|
491
|
+
|
492
|
+
|
493
|
+
Updating An Index
|
494
|
+
-----------------
|
495
|
+
Given how indexes are created and handled, it is recommended that you do *not* modify the lambda function of an
|
496
|
+
index after the index has been created and populated. Doing so is inviting problems, since the index is not
|
497
|
+
regenerated when the function changes.
|
498
|
+
|
499
|
+
If you must change the meaning of an index, we recommend you actually create a new index and begin using that, then
|
500
|
+
obsolete the old one when it is not used any more. Using scopes (discussed below) rather than raw index can help
|
501
|
+
make this easier.
|
502
|
+
|
503
|
+
If you *must* change the lambda function, then you should rebuild the indexes for the model from scratch immediately
|
504
|
+
after changing the function.
|
505
|
+
|
506
|
+
From the command line:
|
507
|
+
|
508
|
+
rake redisted:recalculate_all
|
509
|
+
|
510
|
+
Programatically (one model):
|
511
|
+
|
512
|
+
MyModel.recalculate
|
513
|
+
|
514
|
+
Programatically (all models):
|
515
|
+
|
516
|
+
Redisted::recalculate_all
|
517
|
+
|
518
|
+
For each index, this will cause the old index to be destroyed, and a new one to be regenerated. Depending on the
|
519
|
+
number and complexity of the indexes, and the number of objects in the model, this could take some time. Since
|
520
|
+
indexes in redisted are *required* to do queries, rather than simply providing performance enhancements, access
|
521
|
+
to the query functionality of the model is offline while this reindex occurs, and hence should be performed during
|
522
|
+
a maintenance window.
|
523
|
+
|
524
|
+
TODO: Need a way to specify to *pause* query calls rather than failing them during an index recalcuate.
|
525
|
+
|
526
|
+
TODO: Note that this entire recalculate interfact does not yet exist.
|
527
|
+
|
528
|
+
Index Uniqueness
|
529
|
+
----------------
|
530
|
+
|
531
|
+
The names of the indexes must be unique across all indexes for this model. This goes for unique, filter/sort,
|
532
|
+
and 'match' indexes.
|
533
|
+
|
534
|
+
Scopes
|
535
|
+
======
|
536
|
+
|
537
|
+
Using scopes, you can specify a complex query and refer to it using a simple identifer. For example, combining
|
538
|
+
much of the above:
|
539
|
+
|
540
|
+
class MyModel < Redisted::Base
|
541
|
+
field :name, type: :string
|
542
|
+
field :test_int, type: :integer
|
543
|
+
field :provider, type: :string
|
544
|
+
|
545
|
+
index :odd, includes: ->(elem){(elem.test_int%2)!=0}
|
546
|
+
index :even, includes: ->(elem){(elem.test_int%2)==0}
|
547
|
+
index :provider, match: ->(elem){(elem.provider)}
|
548
|
+
index :asc, order: ->(elem){elem.test_int},fields: :test_int
|
549
|
+
|
550
|
+
scope :sorted_odd, ->{by_odd.by_asc}
|
551
|
+
scope :odd_providers, ->(name){by_odd.provider(name)}
|
552
|
+
end
|
553
|
+
|
554
|
+
Then, you can use them in queries:
|
555
|
+
|
556
|
+
MyModel.sorted_odd.all
|
557
|
+
MyModel.odd_providers.all
|
558
|
+
|
559
|
+
Validations
|
560
|
+
===========
|
561
|
+
|
562
|
+
Standard ActiveModel validations work with Redisted. Such as:
|
563
|
+
|
564
|
+
class MyModel < Redisted::Base
|
565
|
+
field :name, type: :string
|
566
|
+
field :test_int, type: :integer
|
567
|
+
field :provider, type: :string
|
568
|
+
|
569
|
+
validates_length_of :name, :minumum=>5
|
570
|
+
validates_length_of :provider, :maximum=>20
|
571
|
+
end
|
572
|
+
|
573
|
+
TODO:Note that "validates_uniqueness_of" is not yet supported...
|
574
|
+
|
575
|
+
References (aka Relationships)
|
576
|
+
==============================
|
577
|
+
Redisted is great for creating models that are "in between" other non-Redisted models, such as ActiveRecord models or
|
578
|
+
Mongoid models. Redis in general is great for creating integrated maps of values, such as object references. In fact,
|
579
|
+
Redisted was created out of a need for a very fast way of assocating large number of 'tags' to a large number of
|
580
|
+
'messages' stored in Mongoid very efficiently.
|
581
|
+
|
582
|
+
As such, the relation mechanims in Redisted are optimized for creating not only references to other Redisted models,
|
583
|
+
but to models of other backend stores, such as ActiveRecord and Mongoid.
|
584
|
+
|
585
|
+
There are only two reference types, references_one and references_many. Here is an example:
|
586
|
+
|
587
|
+
class MyModel < Redisted::Base
|
588
|
+
field :name, type: :string
|
589
|
+
field :test_int, type: :integer
|
590
|
+
field :provider, type: :string
|
591
|
+
|
592
|
+
references_one :user
|
593
|
+
references_many :message
|
594
|
+
|
595
|
+
references_one :another_class, as: :aclass
|
596
|
+
references_many :another_class, as: :theclasses
|
597
|
+
end
|
598
|
+
|
599
|
+
The "references_one" creates a reference to a single object of another model. The model does not have to be
|
600
|
+
a redisted model (but it can be), it can be ActiveRecord, Mongoid, anything that accepts a ".find(id)" call.
|
601
|
+
|
602
|
+
With a references_one field, you can:
|
603
|
+
|
604
|
+
u=ARUser.create
|
605
|
+
mm=MyModel.create
|
606
|
+
|
607
|
+
mm.user=u
|
608
|
+
or:
|
609
|
+
mm.user_id=u.id
|
610
|
+
|
611
|
+
u=mm.user # Returns an instance of ARUser
|
612
|
+
u=mm.user.xxx # Call method 'xxx' on the instance of ARUser
|
613
|
+
|
614
|
+
With a refererences_many field, you can:
|
615
|
+
|
616
|
+
m1=ARMessage.create
|
617
|
+
m2=ARMessage.create
|
618
|
+
mm=MyModel.create
|
619
|
+
|
620
|
+
mm.message << m1
|
621
|
+
mm.message << m2
|
622
|
+
|
623
|
+
mm.message[0] # Returns an instance of ARMessage (m1)
|
624
|
+
mm.message[1] # Returns an instance of ARMessage (m2)
|
625
|
+
|
626
|
+
This all will work as is, without any changes to the ARUser model. However, you will probably want to install
|
627
|
+
a destroy callback in ARUser so that the reference is removed if 'u' is destroyed. You can do that using
|
628
|
+
the following code:
|
629
|
+
|
630
|
+
class ARUser
|
631
|
+
Redisted::on_destroy :my_model,:user
|
632
|
+
end
|
633
|
+
class ARMessage
|
634
|
+
Redisted::on_destroy :my_model,:message
|
635
|
+
|
636
|
+
|
637
|
+
Callbacks
|
638
|
+
=========
|
639
|
+
|
640
|
+
Callbacks work the same as ActiveRecord. Redisted supports callbacks on create, update, save, and destroy.
|
641
|
+
For update and save, the callbacks are called anytime a value is written to Redisted. So, if the model is setup
|
642
|
+
so that each field update forces a write to Redisted (the default), then these two callbacks are called on each
|
643
|
+
field update.
|