redis_orm 0.5.1 → 0.8
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/CHANGELOG +29 -0
- data/README.md +66 -2
- data/TODO +5 -0
- data/lib/rails/generators/redis_orm/model/model_generator.rb +21 -0
- data/lib/rails/generators/redis_orm/model/templates/model.rb.erb +5 -0
- data/lib/redis_orm.rb +8 -11
- data/lib/redis_orm/active_model_behavior.rb +0 -2
- data/lib/redis_orm/associations/belongs_to.rb +49 -11
- data/lib/redis_orm/associations/has_many.rb +29 -21
- data/lib/redis_orm/associations/has_many_helper.rb +1 -1
- data/lib/redis_orm/associations/has_many_proxy.rb +19 -8
- data/lib/redis_orm/associations/has_one.rb +36 -2
- data/lib/redis_orm/redis_orm.rb +486 -173
- data/lib/redis_orm/utils.rb +12 -0
- metadata +93 -80
- data/Manifest +0 -30
- data/Rakefile +0 -33
- data/redis_orm.gemspec +0 -45
- data/test/association_indices_test.rb +0 -145
- data/test/associations_test.rb +0 -306
- data/test/atomicity_test.rb +0 -64
- data/test/basic_functionality_test.rb +0 -173
- data/test/callbacks_test.rb +0 -119
- data/test/changes_array_test.rb +0 -31
- data/test/dynamic_finders_test.rb +0 -68
- data/test/exceptions_test.rb +0 -45
- data/test/has_one_has_many_test.rb +0 -54
- data/test/indices_test.rb +0 -81
- data/test/options_test.rb +0 -243
- data/test/polymorphic_test.rb +0 -104
- data/test/redis.conf +0 -417
- data/test/test_helper.rb +0 -25
- data/test/uuid_as_id_test.rb +0 -210
- data/test/validations_test.rb +0 -28
@@ -15,6 +15,10 @@ module RedisOrm
|
|
15
15
|
foreign_model.to_sym
|
16
16
|
end
|
17
17
|
|
18
|
+
if options[:index]
|
19
|
+
class_variable_get(:"@@indices")[model_name] << {:name => foreign_model_name, :options => {:reference => true}}
|
20
|
+
end
|
21
|
+
|
18
22
|
define_method foreign_model_name do
|
19
23
|
foreign_model.to_s.camelize.constantize.find($redis.get "#{model_name}:#{@id}:#{foreign_model_name}")
|
20
24
|
end
|
@@ -25,13 +29,43 @@ module RedisOrm
|
|
25
29
|
# we need to store this to clear old associations later
|
26
30
|
old_assoc = self.send(foreign_model_name)
|
27
31
|
|
32
|
+
reference_key = "#{model_name}:#{id}:#{foreign_model_name}"
|
28
33
|
if assoc_with_record.nil?
|
29
|
-
$redis.del(
|
34
|
+
$redis.del(reference_key)
|
30
35
|
elsif assoc_with_record.model_name == foreign_model.to_s
|
31
|
-
$redis.set(
|
36
|
+
$redis.set(reference_key, assoc_with_record.id)
|
37
|
+
set_expire_on_reference_key(reference_key)
|
32
38
|
else
|
33
39
|
raise TypeMismatchError
|
34
40
|
end
|
41
|
+
|
42
|
+
# handle indices for references
|
43
|
+
self.get_indices.select{|index| index[:options][:reference]}.each do |index|
|
44
|
+
# delete old reference that points to the old associated record
|
45
|
+
if !old_assoc.nil?
|
46
|
+
prepared_index = [self.model_name, index[:name], old_assoc.id].join(':')
|
47
|
+
prepared_index.downcase! if index[:options][:case_insensitive]
|
48
|
+
|
49
|
+
if index[:options][:unique]
|
50
|
+
$redis.del(prepared_index, id)
|
51
|
+
else
|
52
|
+
$redis.zrem(prepared_index, id)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# if new associated record is nil then skip to next index (since old associated record was already unreferenced)
|
57
|
+
next if assoc_with_record.nil?
|
58
|
+
|
59
|
+
prepared_index = [self.model_name, index[:name], assoc_with_record.id].join(':')
|
60
|
+
|
61
|
+
prepared_index.downcase! if index[:options][:case_insensitive]
|
62
|
+
|
63
|
+
if index[:options][:unique]
|
64
|
+
$redis.set(prepared_index, id)
|
65
|
+
else
|
66
|
+
$redis.zadd(prepared_index, Time.now.to_f, id)
|
67
|
+
end
|
68
|
+
end
|
35
69
|
|
36
70
|
if !options[:as]
|
37
71
|
if assoc_with_record.nil?
|
data/lib/redis_orm/redis_orm.rb
CHANGED
@@ -1,10 +1,16 @@
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
2
|
-
|
3
1
|
require 'active_support/inflector/inflections'
|
4
2
|
require 'active_support/inflector/transliterate'
|
5
3
|
require 'active_support/inflector/methods'
|
6
4
|
require 'active_support/inflections'
|
7
5
|
require 'active_support/core_ext/string/inflections'
|
6
|
+
|
7
|
+
require 'active_support/core_ext/time/acts_like'
|
8
|
+
require 'active_support/core_ext/time/calculations'
|
9
|
+
require 'active_support/core_ext/time/conversions'
|
10
|
+
#require 'active_support/core_ext/time/marshal'
|
11
|
+
require 'active_support/core_ext/time/zones'
|
12
|
+
|
13
|
+
require 'active_support/core_ext/numeric'
|
8
14
|
require 'active_support/core_ext/time/calculations' # local_time for to_time(:local)
|
9
15
|
require 'active_support/core_ext/string/conversions' # to_time
|
10
16
|
|
@@ -20,6 +26,9 @@ module RedisOrm
|
|
20
26
|
class NotIndexFound < StandardError
|
21
27
|
end
|
22
28
|
|
29
|
+
class RecordNotFound < StandardError
|
30
|
+
end
|
31
|
+
|
23
32
|
class TypeMismatchError < StandardError
|
24
33
|
end
|
25
34
|
|
@@ -29,7 +38,7 @@ module RedisOrm
|
|
29
38
|
class Base
|
30
39
|
include ActiveModel::Validations
|
31
40
|
include ActiveModelBehavior
|
32
|
-
|
41
|
+
include Utils
|
33
42
|
include Associations::HasManyHelper
|
34
43
|
|
35
44
|
extend Associations::BelongsTo
|
@@ -39,19 +48,27 @@ module RedisOrm
|
|
39
48
|
attr_accessor :persisted
|
40
49
|
|
41
50
|
@@properties = Hash.new{|h,k| h[k] = []}
|
42
|
-
@@indices = Hash.new{|h,k| h[k] = []} # compound indices are available too
|
51
|
+
@@indices = Hash.new{|h,k| h[k] = []} # compound indices are available too
|
43
52
|
@@associations = Hash.new{|h,k| h[k] = []}
|
44
53
|
@@callbacks = Hash.new{|h,k| h[k] = {}}
|
45
54
|
@@use_uuid_as_id = {}
|
46
|
-
|
55
|
+
@@descendants = []
|
56
|
+
@@expire = Hash.new{|h,k| h[k] = {}}
|
57
|
+
|
47
58
|
class << self
|
48
59
|
|
49
60
|
def inherited(from)
|
50
61
|
[:after_save, :before_save, :after_create, :before_create, :after_destroy, :before_destroy].each do |callback_name|
|
51
62
|
@@callbacks[from.model_name][callback_name] = []
|
52
63
|
end
|
64
|
+
|
65
|
+
@@descendants << from
|
66
|
+
end
|
67
|
+
|
68
|
+
def descendants
|
69
|
+
@@descendants
|
53
70
|
end
|
54
|
-
|
71
|
+
|
55
72
|
# *options* currently supports
|
56
73
|
# *unique* Boolean
|
57
74
|
# *case_insensitive* Boolean
|
@@ -65,22 +82,19 @@ module RedisOrm
|
|
65
82
|
send(:define_method, property_name) do
|
66
83
|
value = instance_variable_get(:"@#{property_name}")
|
67
84
|
|
68
|
-
return
|
69
|
-
|
70
|
-
|
71
|
-
value
|
72
|
-
value.to_s.to_time(:local)
|
73
|
-
rescue ArgumentError => e
|
74
|
-
nil
|
75
|
-
end
|
85
|
+
return nil if value.nil? # we must return nil here so :default option will work when saving, otherwise it'll return "" or 0 or 0.0
|
86
|
+
if /DateTime|Time/ =~ class_name.to_s
|
87
|
+
# we're using to_datetime here because to_time doesn't manage timezone correctly
|
88
|
+
value.to_s.to_datetime rescue nil
|
76
89
|
elsif Integer == class_name
|
77
|
-
value
|
90
|
+
value.to_i
|
78
91
|
elsif Float == class_name
|
79
|
-
value
|
92
|
+
value.to_f
|
80
93
|
elsif RedisOrm::Boolean == class_name
|
81
|
-
|
94
|
+
((value == "false" || value == false) ? false : true)
|
95
|
+
else
|
96
|
+
value
|
82
97
|
end
|
83
|
-
value
|
84
98
|
end
|
85
99
|
|
86
100
|
send(:define_method, "#{property_name}=".to_sym) do |value|
|
@@ -92,7 +106,6 @@ module RedisOrm
|
|
92
106
|
else
|
93
107
|
instance_variable_set(:"@#{property_name}_changes", [value])
|
94
108
|
end
|
95
|
-
|
96
109
|
instance_variable_set(:"@#{property_name}", value)
|
97
110
|
end
|
98
111
|
|
@@ -117,6 +130,10 @@ module RedisOrm
|
|
117
130
|
end
|
118
131
|
end
|
119
132
|
|
133
|
+
def expire(seconds, options = {})
|
134
|
+
@@expire[model_name] = {seconds: seconds, options: options}
|
135
|
+
end
|
136
|
+
|
120
137
|
def use_uuid_as_id
|
121
138
|
@@use_uuid_as_id[model_name] = true
|
122
139
|
@@uuid = UUID.new
|
@@ -126,20 +143,29 @@ module RedisOrm
|
|
126
143
|
$redis.zcard("#{model_name}:ids").to_i
|
127
144
|
end
|
128
145
|
|
129
|
-
def first
|
130
|
-
|
131
|
-
|
146
|
+
def first(options = {})
|
147
|
+
if options.empty?
|
148
|
+
id = $redis.zrangebyscore("#{model_name}:ids", 0, Time.now.to_f, :limit => [0, 1])
|
149
|
+
id.empty? ? nil : find(id[0])
|
150
|
+
else
|
151
|
+
find(:first, options)
|
152
|
+
end
|
132
153
|
end
|
133
154
|
|
134
|
-
def last
|
135
|
-
|
136
|
-
|
155
|
+
def last(options = {})
|
156
|
+
if options.empty?
|
157
|
+
id = $redis.zrevrangebyscore("#{model_name}:ids", Time.now.to_f, 0, :limit => [0, 1])
|
158
|
+
id.empty? ? nil : find(id[0])
|
159
|
+
else
|
160
|
+
find(:last, options)
|
161
|
+
end
|
137
162
|
end
|
138
|
-
|
139
|
-
def
|
163
|
+
|
164
|
+
def find_indices(properties, options = {})
|
140
165
|
properties.map!{|p| p.to_sym}
|
141
|
-
|
142
|
-
|
166
|
+
method = options[:first] ? :detect : :select
|
167
|
+
|
168
|
+
@@indices[model_name].send(method) do |models_index|
|
143
169
|
if models_index[:name].is_a?(Array) && models_index[:name].size == properties.size
|
144
170
|
# check the elements not taking into account their order
|
145
171
|
(models_index[:name] & properties).size == properties.size
|
@@ -168,36 +194,106 @@ module RedisOrm
|
|
168
194
|
prepared_index
|
169
195
|
end
|
170
196
|
|
197
|
+
# TODO refactor this messy function
|
171
198
|
def all(options = {})
|
172
199
|
limit = if options[:limit] && options[:offset]
|
173
200
|
[options[:offset].to_i, options[:limit].to_i]
|
174
201
|
elsif options[:limit]
|
175
202
|
[0, options[:limit].to_i]
|
203
|
+
else
|
204
|
+
[0, -1]
|
176
205
|
end
|
177
206
|
|
178
|
-
|
207
|
+
order_max_limit = Time.now.to_f
|
208
|
+
ids_key = "#{model_name}:ids"
|
209
|
+
index = nil
|
210
|
+
|
211
|
+
prepared_index = if !options[:conditions].blank? && options[:conditions].is_a?(Hash)
|
179
212
|
properties = options[:conditions].collect{|key, value| key}
|
180
|
-
index = find_index(properties)
|
181
213
|
|
182
|
-
|
214
|
+
# if some condition includes object => get only the id of this object
|
215
|
+
conds = options[:conditions].inject({}) do |sum, item|
|
216
|
+
key, value = item
|
217
|
+
if value.respond_to?(:model_name)
|
218
|
+
sum.merge!({key => value.id})
|
219
|
+
else
|
220
|
+
sum.merge!({key => value})
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
index = find_indices(properties, {first: true})
|
183
225
|
|
184
|
-
|
226
|
+
raise NotIndexFound if !index
|
185
227
|
|
186
|
-
|
228
|
+
construct_prepared_index(index, conds)
|
229
|
+
else
|
230
|
+
if options[:order] && options[:order].is_a?(Array)
|
231
|
+
model_name
|
232
|
+
else
|
233
|
+
ids_key
|
234
|
+
end
|
235
|
+
end
|
187
236
|
|
188
|
-
|
189
|
-
|
190
|
-
|
237
|
+
order_by_property_is_string = false
|
238
|
+
|
239
|
+
# if not array => created_at native order (in which ids were pushed to "#{model_name}:ids" set by default)
|
240
|
+
direction = if !options[:order].blank?
|
241
|
+
property = {}
|
242
|
+
dir = if options[:order].is_a?(Array)
|
243
|
+
property = @@properties[model_name].detect{|prop| prop[:name].to_s == options[:order].first.to_s}
|
244
|
+
# for String values max limit for search key could be 1.0, but for Numeric values there's actually no limit
|
245
|
+
order_max_limit = 100_000_000_000
|
246
|
+
ids_key = "#{prepared_index}:#{options[:order].first}_ids"
|
247
|
+
options[:order].size == 2 ? options[:order].last : 'asc'
|
248
|
+
else
|
249
|
+
property = @@properties[model_name].detect{|prop| prop[:name].to_s == options[:order].to_s}
|
250
|
+
ids_key = prepared_index
|
251
|
+
options[:order]
|
252
|
+
end
|
253
|
+
if property && property[:class].eql?("String") && property[:options][:sortable]
|
254
|
+
order_by_property_is_string = true
|
255
|
+
end
|
256
|
+
dir
|
257
|
+
else
|
258
|
+
ids_key = prepared_index
|
259
|
+
'asc'
|
260
|
+
end
|
261
|
+
|
262
|
+
if order_by_property_is_string
|
263
|
+
if direction.to_s == 'desc'
|
264
|
+
ids_length = $redis.llen(ids_key)
|
265
|
+
limit = if options[:offset] && options[:limit]
|
266
|
+
[(ids_length - options[:offset].to_i - options[:limit].to_i), (ids_length - options[:offset].to_i - 1)]
|
267
|
+
elsif options[:limit]
|
268
|
+
[ids_length - options[:limit].to_i, ids_length]
|
269
|
+
elsif options[:offset]
|
270
|
+
[0, (ids_length - options[:offset].to_i - 1)]
|
271
|
+
else
|
272
|
+
[0, -1]
|
273
|
+
end
|
274
|
+
$redis.lrange(ids_key, *limit).reverse.compact.collect{|id| find(id.split(':').last)}
|
191
275
|
else
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
276
|
+
limit = if options[:offset] && options[:limit]
|
277
|
+
[options[:offset].to_i, (options[:offset].to_i + options[:limit].to_i)]
|
278
|
+
elsif options[:limit]
|
279
|
+
[0, options[:limit].to_i - 1]
|
280
|
+
elsif options[:offset]
|
281
|
+
[options[:offset].to_i, -1]
|
282
|
+
else
|
283
|
+
[0, -1]
|
284
|
+
end
|
285
|
+
$redis.lrange(ids_key, *limit).compact.collect{|id| find(id.split(':').last)}
|
286
|
+
end
|
196
287
|
else
|
197
|
-
if options[:
|
198
|
-
$redis.
|
288
|
+
if index && index[:options][:unique]
|
289
|
+
id = $redis.get prepared_index
|
290
|
+
model_name.to_s.camelize.constantize.find(id)
|
199
291
|
else
|
200
|
-
|
292
|
+
if direction.to_s == 'desc'
|
293
|
+
$redis.zrevrangebyscore(ids_key, order_max_limit, 0, :limit => limit).compact.collect{|id| find(id)}
|
294
|
+
else
|
295
|
+
$redis.zrangebyscore(ids_key, 0, order_max_limit, :limit => limit).compact.collect{|id| find(id)}
|
296
|
+
end
|
201
297
|
end
|
202
298
|
end
|
203
299
|
end
|
@@ -235,6 +331,15 @@ module RedisOrm
|
|
235
331
|
end
|
236
332
|
end
|
237
333
|
|
334
|
+
def find!(*args)
|
335
|
+
result = find(*args)
|
336
|
+
if result.nil?
|
337
|
+
raise RecordNotFound
|
338
|
+
else
|
339
|
+
result
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
238
343
|
def after_save(callback)
|
239
344
|
@@callbacks[model_name][:after_save] << callback
|
240
345
|
end
|
@@ -262,9 +367,21 @@ module RedisOrm
|
|
262
367
|
def create(options = {})
|
263
368
|
obj = new(options, nil, false)
|
264
369
|
obj.save
|
370
|
+
|
371
|
+
# make possible binding related models while creating class instance
|
372
|
+
options.each do |k, v|
|
373
|
+
if @@associations[model_name].detect{|h| h[:foreign_model] == k || h[:options][:as] == k}
|
374
|
+
obj.send("#{k}=", v)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
$redis.expire(obj.__redis_record_key, options[:expire_in].to_i) if !options[:expire_in].blank?
|
379
|
+
|
265
380
|
obj
|
266
381
|
end
|
267
382
|
|
383
|
+
alias :create! :create
|
384
|
+
|
268
385
|
# dynamic finders
|
269
386
|
def method_missing(method_name, *args, &block)
|
270
387
|
if method_name =~ /^find_(all_)?by_(\w*)/
|
@@ -276,7 +393,7 @@ module RedisOrm
|
|
276
393
|
properties.each_with_index do |prop, i|
|
277
394
|
properties_hash.merge!({prop.to_sym => args[i]})
|
278
395
|
end
|
279
|
-
|
396
|
+
find_indices(properties, :first => true)
|
280
397
|
end
|
281
398
|
|
282
399
|
raise NotIndexFound if !index
|
@@ -314,7 +431,27 @@ module RedisOrm
|
|
314
431
|
def to_a
|
315
432
|
[self]
|
316
433
|
end
|
434
|
+
|
435
|
+
def __redis_record_key
|
436
|
+
"#{model_name}:#{id}"
|
437
|
+
end
|
438
|
+
|
439
|
+
def set_expire_on_reference_key(key)
|
440
|
+
class_expire = @@expire[model_name]
|
317
441
|
|
442
|
+
# if class method *expire* was invoked and number of seconds was specified then set expiry date on the HSET record key
|
443
|
+
if class_expire[:seconds]
|
444
|
+
set_expire = true
|
445
|
+
|
446
|
+
if class_expire[:options][:if] && class_expire[:options][:if].class == Proc
|
447
|
+
# *self* here refers to the instance of class which has_one association
|
448
|
+
set_expire = class_expire[:options][:if][self] # invoking specified *:if* Proc with current record as *self*
|
449
|
+
end
|
450
|
+
|
451
|
+
$redis.expire(key, class_expire[:seconds].to_i) if set_expire
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
318
455
|
# is called from RedisOrm::Associations::HasMany to save backlinks to saved records
|
319
456
|
def get_associations
|
320
457
|
@@associations[self.model_name]
|
@@ -328,6 +465,7 @@ module RedisOrm
|
|
328
465
|
def initialize(attributes = {}, id = nil, persisted = false)
|
329
466
|
@persisted = persisted
|
330
467
|
|
468
|
+
# if this model uses uuid then id is a string otherwise it should be casted to Integer class
|
331
469
|
id = @@use_uuid_as_id[model_name] ? id : id.to_i
|
332
470
|
|
333
471
|
instance_variable_set(:"@id", id) if id
|
@@ -340,10 +478,19 @@ module RedisOrm
|
|
340
478
|
instance_variable_set :"@#{prop[:name]}_changes", []
|
341
479
|
end
|
342
480
|
end
|
343
|
-
|
481
|
+
|
482
|
+
# cast all attributes' keys to symbols
|
483
|
+
attributes = attributes.inject({}){|sum, el| sum.merge({el[0].to_sym => el[1]})} if attributes.is_a?(Hash)
|
484
|
+
|
485
|
+
# get all names of properties to assign only those attributes from attributes hash whose key are in prop_names
|
486
|
+
# we're not using *self.respond_to?("#{key}=".to_sym)* since *belongs_to* and other assocs could create their own methods
|
487
|
+
# with *key=* name, that in turn will mess up indices
|
344
488
|
if attributes.is_a?(Hash) && !attributes.empty?
|
345
|
-
|
346
|
-
|
489
|
+
@@properties[model_name].each do |property|
|
490
|
+
if !(value = attributes[property[:name]]).nil? # check for nil because we want to pass falses too (and value could be 'false')
|
491
|
+
value = Marshal.load(value) if ["Array", "Hash"].include?(property[:class]) && value.is_a?(String)
|
492
|
+
self.send("#{property[:name]}=".to_sym, value)
|
493
|
+
end
|
347
494
|
end
|
348
495
|
end
|
349
496
|
self
|
@@ -353,6 +500,31 @@ module RedisOrm
|
|
353
500
|
@id
|
354
501
|
end
|
355
502
|
|
503
|
+
alias :to_key :id
|
504
|
+
|
505
|
+
def to_s
|
506
|
+
inspected = "<#{model_name.capitalize} id: #{@id}, "
|
507
|
+
inspected += @@properties[model_name].inject([]) do |sum, prop|
|
508
|
+
property_value = instance_variable_get(:"@#{prop[:name]}")
|
509
|
+
property_value = '"' + property_value.to_s + '"' if prop[:class].eql?("String")
|
510
|
+
property_value = 'nil' if property_value.nil?
|
511
|
+
sum << "#{prop[:name]}: " + property_value.to_s
|
512
|
+
end.join(', ')
|
513
|
+
inspected += ">"
|
514
|
+
inspected
|
515
|
+
end
|
516
|
+
|
517
|
+
def ==(other)
|
518
|
+
raise "this object could be comparable only with object of the same class" if other.class != self.class
|
519
|
+
same = true
|
520
|
+
@@properties[model_name].each do |prop|
|
521
|
+
self_var = instance_variable_get(:"@#{prop[:name]}")
|
522
|
+
same = false if other.send(prop[:name]).to_s != self_var.to_s
|
523
|
+
end
|
524
|
+
same = false if self.id != other.id
|
525
|
+
same
|
526
|
+
end
|
527
|
+
|
356
528
|
def persisted?
|
357
529
|
@persisted
|
358
530
|
end
|
@@ -363,86 +535,20 @@ module RedisOrm
|
|
363
535
|
else
|
364
536
|
$redis.incr("#{model_name}:id")
|
365
537
|
end
|
366
|
-
end
|
538
|
+
end
|
367
539
|
|
368
540
|
def save
|
369
541
|
return false if !valid?
|
370
542
|
|
371
|
-
|
543
|
+
_check_mismatched_types_for_values
|
544
|
+
|
545
|
+
# store here initial persisted flag so we could invoke :after_create callbacks in the end of *save* function
|
372
546
|
was_persisted = persisted?
|
373
547
|
|
374
548
|
if persisted? # then there might be old indices
|
375
|
-
#
|
376
|
-
@@properties[model_name].each do |prop|
|
377
|
-
# if there were no changes for current property skip it (indices remains the same)
|
378
|
-
next if ! self.send(:"#{prop[:name]}_changed?")
|
379
|
-
|
380
|
-
prev_prop_value = instance_variable_get(:"@#{prop[:name]}_changes").first
|
381
|
-
prop_value = instance_variable_get(:"@#{prop[:name]}")
|
382
|
-
|
383
|
-
indices = @@indices[model_name].inject([]) do |sum, models_index|
|
384
|
-
if models_index[:name].is_a?(Array)
|
385
|
-
if models_index[:name].include?(prop[:name])
|
386
|
-
sum << models_index
|
387
|
-
else
|
388
|
-
sum
|
389
|
-
end
|
390
|
-
else
|
391
|
-
if models_index[:name] == prop[:name]
|
392
|
-
sum << models_index
|
393
|
-
else
|
394
|
-
sum
|
395
|
-
end
|
396
|
-
end
|
397
|
-
end
|
398
|
-
|
399
|
-
if !indices.empty?
|
400
|
-
indices.each do |index|
|
401
|
-
if index[:name].is_a?(Array)
|
402
|
-
keys_to_delete = if index[:name].index(prop) == 0
|
403
|
-
$redis.keys "#{model_name}:#{prop[:name]}#{prev_prop_value}*"
|
404
|
-
else
|
405
|
-
$redis.keys "#{model_name}:*#{prop[:name]}:#{prev_prop_value}*"
|
406
|
-
end
|
407
|
-
|
408
|
-
keys_to_delete.each{|key| $redis.del(key)}
|
409
|
-
else
|
410
|
-
key_to_delete = "#{model_name}:#{prop[:name]}:#{prev_prop_value}"
|
411
|
-
$redis.del key_to_delete
|
412
|
-
end
|
413
|
-
|
414
|
-
# also we need to delete associated records *indices*
|
415
|
-
if !@@associations[model_name].empty?
|
416
|
-
@@associations[model_name].each do |assoc|
|
417
|
-
if :belongs_to == assoc[:type]
|
418
|
-
if !self.send(assoc[:foreign_model]).nil?
|
419
|
-
if index[:name].is_a?(Array)
|
420
|
-
keys_to_delete = if index[:name].index(prop) == 0
|
421
|
-
$redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}#{prev_prop_value}*"
|
422
|
-
else
|
423
|
-
$redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:*#{prop[:name]}:#{prev_prop_value}*"
|
424
|
-
end
|
425
|
-
|
426
|
-
keys_to_delete.each{|key| $redis.del(key)}
|
427
|
-
else
|
428
|
-
beginning_of_the_key = "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}:"
|
429
|
-
|
430
|
-
$redis.del(beginning_of_the_key + prev_prop_value.to_s)
|
431
|
-
|
432
|
-
index[:options][:unique] ? $redis.set((beginning_of_the_key + prop_value.to_s), @id) : $redis.zadd((beginning_of_the_key + prop_value.to_s), Time.now.to_f, @id)
|
433
|
-
end
|
434
|
-
end
|
435
|
-
end
|
436
|
-
end
|
437
|
-
end # deleting associated records *indices*
|
438
|
-
|
439
|
-
end
|
440
|
-
end
|
441
|
-
end
|
549
|
+
_check_indices_for_persisted # remove old indices if needed
|
442
550
|
else # !persisted?
|
443
|
-
@@callbacks[model_name][:before_create].each
|
444
|
-
self.send(callback)
|
445
|
-
end
|
551
|
+
@@callbacks[model_name][:before_create].each{ |callback| self.send(callback) }
|
446
552
|
|
447
553
|
@id = get_next_id
|
448
554
|
$redis.zadd "#{model_name}:ids", Time.now.to_f, @id
|
@@ -450,58 +556,64 @@ module RedisOrm
|
|
450
556
|
self.created_at = Time.now if respond_to? :created_at
|
451
557
|
end
|
452
558
|
|
453
|
-
@@callbacks[model_name][:before_save].each
|
454
|
-
self.send(callback)
|
455
|
-
end
|
559
|
+
@@callbacks[model_name][:before_save].each{ |callback| self.send(callback) }
|
456
560
|
|
457
561
|
# automatically update *modified_at* property if it was defined
|
458
562
|
self.modified_at = Time.now if respond_to? :modified_at
|
459
563
|
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
if prop_value.nil? && !prop[:options][:default].nil?
|
464
|
-
prop_value = prop[:options][:default]
|
465
|
-
# set instance variable in order to properly save indexes here
|
466
|
-
self.instance_variable_set(:"@#{prop[:name]}", prop[:options][:default])
|
467
|
-
instance_variable_set :"@#{prop[:name]}_changes", [prop[:options][:default]]
|
468
|
-
end
|
469
|
-
|
470
|
-
$redis.hset("#{model_name}:#{id}", prop[:name].to_s, prop_value)
|
564
|
+
_save_to_redis # main work done here
|
565
|
+
_save_new_indices
|
471
566
|
|
472
|
-
|
473
|
-
prop_changes = instance_variable_get :"@#{prop[:name]}_changes"
|
567
|
+
@@callbacks[model_name][:after_save].each{ |callback| self.send(callback) }
|
474
568
|
|
475
|
-
|
476
|
-
|
477
|
-
end
|
569
|
+
if ! was_persisted
|
570
|
+
@@callbacks[model_name][:after_create].each{ |callback| self.send(callback) }
|
478
571
|
end
|
479
572
|
|
480
|
-
#
|
481
|
-
|
482
|
-
@@indices[model_name].each do |index|
|
483
|
-
prepared_index = construct_prepared_index(index) # instance method not class one!
|
484
|
-
|
485
|
-
if index[:options][:unique]
|
486
|
-
$redis.set(prepared_index, @id)
|
487
|
-
else
|
488
|
-
$redis.zadd(prepared_index, Time.now.to_f, @id)
|
489
|
-
end
|
490
|
-
end
|
573
|
+
true # if there were no errors just return true, so *if obj.save* conditions would work
|
574
|
+
end
|
491
575
|
|
492
|
-
|
493
|
-
|
494
|
-
end
|
576
|
+
def find_position_to_insert(sortable_key, value)
|
577
|
+
end_index = $redis.llen(sortable_key)
|
495
578
|
|
496
|
-
if
|
497
|
-
|
498
|
-
|
579
|
+
return 0 if end_index == 0
|
580
|
+
|
581
|
+
start_index = 0
|
582
|
+
pivot_index = end_index / 2
|
583
|
+
|
584
|
+
start_el = $redis.lindex(sortable_key, start_index)
|
585
|
+
end_el = $redis.lindex(sortable_key, end_index - 1)
|
586
|
+
pivot_el = $redis.lindex(sortable_key, pivot_index)
|
587
|
+
|
588
|
+
while start_index != end_index
|
589
|
+
# aa..ab..ac..bd <- ad
|
590
|
+
if start_el.split(':').first > value # Michael > Abe
|
591
|
+
return 0
|
592
|
+
elsif end_el.split(':').first < value # Abe < Todd
|
593
|
+
return end_el
|
594
|
+
elsif start_el.split(':').first == value # Abe == Abe
|
595
|
+
return start_el
|
596
|
+
elsif pivot_el.split(':').first == value # Todd == Todd
|
597
|
+
return pivot_el
|
598
|
+
elsif end_el.split(':').first == value
|
599
|
+
return end_el
|
600
|
+
elsif (start_el.split(':').first < value) && (pivot_el.split(':').first > value)
|
601
|
+
start_index = start_index
|
602
|
+
prev_pivot_index = pivot_index
|
603
|
+
pivot_index = start_index + ((end_index - pivot_index) / 2)
|
604
|
+
end_index = prev_pivot_index
|
605
|
+
elsif (pivot_el.split(':').first < value) && (end_el.split(':').first > value) # M < V && Y > V
|
606
|
+
start_index = pivot_index
|
607
|
+
pivot_index = pivot_index + ((end_index - pivot_index) / 2)
|
608
|
+
end_index = end_index
|
499
609
|
end
|
610
|
+
start_el = $redis.lindex(sortable_key, start_index)
|
611
|
+
end_el = $redis.lindex(sortable_key, end_index - 1)
|
612
|
+
pivot_el = $redis.lindex(sortable_key, pivot_index)
|
500
613
|
end
|
501
|
-
|
502
|
-
true # if there were no errors just return true, so *if* conditions would work
|
614
|
+
start_el
|
503
615
|
end
|
504
|
-
|
616
|
+
|
505
617
|
def update_attributes(attributes)
|
506
618
|
if attributes.is_a?(Hash)
|
507
619
|
attributes.each do |key, value|
|
@@ -522,7 +634,16 @@ module RedisOrm
|
|
522
634
|
end
|
523
635
|
|
524
636
|
@@properties[model_name].each do |prop|
|
637
|
+
property_value = instance_variable_get(:"@#{prop[:name]}").to_s
|
525
638
|
$redis.hdel("#{model_name}:#{@id}", prop[:name].to_s)
|
639
|
+
|
640
|
+
if prop[:options][:sortable]
|
641
|
+
if prop[:class].eql?("String")
|
642
|
+
$redis.lrem "#{model_name}:#{prop[:name]}_ids", 1, "#{property_value}:#{@id}"
|
643
|
+
else
|
644
|
+
$redis.zrem "#{model_name}:#{prop[:name]}_ids", @id
|
645
|
+
end
|
646
|
+
end
|
526
647
|
end
|
527
648
|
|
528
649
|
$redis.zrem "#{model_name}:ids", @id
|
@@ -531,13 +652,16 @@ module RedisOrm
|
|
531
652
|
if !@@associations[model_name].empty?
|
532
653
|
@@associations[model_name].each do |assoc|
|
533
654
|
if :belongs_to == assoc[:type]
|
534
|
-
if
|
655
|
+
# if assoc has :as option
|
656
|
+
foreign_model_name = assoc[:options][:as] ? assoc[:options][:as].to_sym : assoc[:foreign_model].to_sym
|
657
|
+
|
658
|
+
if !self.send(foreign_model_name).nil?
|
535
659
|
@@indices[model_name].each do |index|
|
536
660
|
keys_to_delete = if index[:name].is_a?(Array)
|
537
661
|
full_index = index[:name].inject([]){|sum, index_part| sum << index_part}.join(':')
|
538
|
-
$redis.keys "#{
|
662
|
+
$redis.keys "#{foreign_model_name}:#{self.send(foreign_model_name).id}:#{model_name.to_s.pluralize}:#{full_index}:*"
|
539
663
|
else
|
540
|
-
["#{
|
664
|
+
["#{foreign_model_name}:#{self.send(foreign_model_name).id}:#{model_name.to_s.pluralize}:#{index[:name]}:#{self.send(index[:name])}"]
|
541
665
|
end
|
542
666
|
keys_to_delete.each do |key|
|
543
667
|
index[:options][:unique] ? $redis.del(key) : $redis.zrem(key, @id)
|
@@ -618,7 +742,7 @@ module RedisOrm
|
|
618
742
|
|
619
743
|
# remove all associated indices
|
620
744
|
@@indices[model_name].each do |index|
|
621
|
-
prepared_index =
|
745
|
+
prepared_index = _construct_prepared_index(index) # instance method not class one!
|
622
746
|
|
623
747
|
if index[:options][:unique]
|
624
748
|
$redis.del(prepared_index)
|
@@ -634,19 +758,208 @@ module RedisOrm
|
|
634
758
|
true # if there were no errors just return true, so *if* conditions would work
|
635
759
|
end
|
636
760
|
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
761
|
+
protected
|
762
|
+
def _construct_prepared_index(index)
|
763
|
+
prepared_index = if index[:name].is_a?(Array) # TODO sort alphabetically
|
764
|
+
index[:name].inject([model_name]) do |sum, index_part|
|
765
|
+
sum += [index_part, self.instance_variable_get(:"@#{index_part}")]
|
766
|
+
end.join(':')
|
767
|
+
else
|
768
|
+
[model_name, index[:name], self.instance_variable_get(:"@#{index[:name]}")].join(':')
|
769
|
+
end
|
770
|
+
|
771
|
+
prepared_index.downcase! if index[:options][:case_insensitive]
|
772
|
+
|
773
|
+
prepared_index
|
774
|
+
end
|
775
|
+
|
776
|
+
def _check_mismatched_types_for_values
|
777
|
+
# an exception should be raised before all saving procedures if wrong value type is specified (especcially true for Arrays and Hashes)
|
778
|
+
@@properties[model_name].each do |prop|
|
779
|
+
prop_value = self.send(prop[:name].to_sym)
|
780
|
+
|
781
|
+
if prop_value && prop[:class] != prop_value.class.to_s && ['Array', 'Hash'].include?(prop[:class].to_s)
|
782
|
+
raise TypeMismatchError
|
645
783
|
end
|
784
|
+
end
|
785
|
+
end
|
786
|
+
|
787
|
+
def _check_indices_for_persisted
|
788
|
+
# check whether there's old indices exists and if yes - delete them
|
789
|
+
@@properties[model_name].each do |prop|
|
790
|
+
# if there were no changes for current property skip it (indices remains the same)
|
791
|
+
next if ! self.send(:"#{prop[:name]}_changed?")
|
646
792
|
|
647
|
-
|
793
|
+
prev_prop_value = instance_variable_get(:"@#{prop[:name]}_changes").first
|
794
|
+
prop_value = instance_variable_get(:"@#{prop[:name]}")
|
795
|
+
# TODO DRY in destroy also
|
796
|
+
if prop[:options][:sortable]
|
797
|
+
if prop[:class].eql?("String")
|
798
|
+
$redis.lrem "#{model_name}:#{prop[:name]}_ids", 1, "#{prev_prop_value}:#{@id}"
|
799
|
+
# remove id from every indexed property
|
800
|
+
@@indices[model_name].each do |index|
|
801
|
+
$redis.lrem "#{_construct_prepared_index(index)}:#{prop[:name]}_ids", 1, "#{prop_value}:#{@id}"
|
802
|
+
end
|
803
|
+
else
|
804
|
+
$redis.zrem "#{model_name}:#{prop[:name]}_ids", @id
|
805
|
+
# remove id from every indexed property
|
806
|
+
@@indices[model_name].each do |index|
|
807
|
+
$redis.zrem "#{_construct_prepared_index(index)}:#{prop[:name]}_ids", @id
|
808
|
+
end
|
809
|
+
end
|
810
|
+
end
|
811
|
+
|
812
|
+
indices = @@indices[model_name].inject([]) do |sum, models_index|
|
813
|
+
if models_index[:name].is_a?(Array)
|
814
|
+
if models_index[:name].include?(prop[:name])
|
815
|
+
sum << models_index
|
816
|
+
else
|
817
|
+
sum
|
818
|
+
end
|
819
|
+
else
|
820
|
+
if models_index[:name] == prop[:name]
|
821
|
+
sum << models_index
|
822
|
+
else
|
823
|
+
sum
|
824
|
+
end
|
825
|
+
end
|
826
|
+
end
|
827
|
+
|
828
|
+
if !indices.empty?
|
829
|
+
indices.each do |index|
|
830
|
+
if index[:name].is_a?(Array)
|
831
|
+
keys_to_delete = if index[:name].index(prop) == 0
|
832
|
+
$redis.keys "#{model_name}:#{prop[:name]}#{prev_prop_value}*"
|
833
|
+
else
|
834
|
+
$redis.keys "#{model_name}:*#{prop[:name]}:#{prev_prop_value}*"
|
835
|
+
end
|
836
|
+
|
837
|
+
keys_to_delete.each{|key| $redis.del(key)}
|
838
|
+
else
|
839
|
+
key_to_delete = "#{model_name}:#{prop[:name]}:#{prev_prop_value}"
|
840
|
+
$redis.del key_to_delete
|
841
|
+
end
|
842
|
+
|
843
|
+
# also we need to delete associated records *indices*
|
844
|
+
if !@@associations[model_name].empty?
|
845
|
+
@@associations[model_name].each do |assoc|
|
846
|
+
if :belongs_to == assoc[:type]
|
847
|
+
# if association has :as option use it, otherwise use standard :foreign_model
|
848
|
+
foreign_model_name = assoc[:options][:as] ? assoc[:options][:as].to_sym : assoc[:foreign_model].to_sym
|
849
|
+
if !self.send(foreign_model_name).nil?
|
850
|
+
if index[:name].is_a?(Array)
|
851
|
+
keys_to_delete = if index[:name].index(prop) == 0
|
852
|
+
$redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}#{prev_prop_value}*"
|
853
|
+
else
|
854
|
+
$redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:*#{prop[:name]}:#{prev_prop_value}*"
|
855
|
+
end
|
856
|
+
|
857
|
+
keys_to_delete.each{|key| $redis.del(key)}
|
858
|
+
else
|
859
|
+
beginning_of_the_key = "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}:"
|
860
|
+
|
861
|
+
$redis.del(beginning_of_the_key + prev_prop_value.to_s)
|
862
|
+
|
863
|
+
index[:options][:unique] ? $redis.set((beginning_of_the_key + prop_value.to_s), @id) : $redis.zadd((beginning_of_the_key + prop_value.to_s), Time.now.to_f, @id)
|
864
|
+
end
|
865
|
+
end
|
866
|
+
end
|
867
|
+
end
|
868
|
+
end # deleting associated records *indices*
|
869
|
+
end
|
870
|
+
end
|
871
|
+
end
|
872
|
+
end
|
873
|
+
|
874
|
+
def _save_to_redis
|
875
|
+
@@properties[model_name].each do |prop|
|
876
|
+
prop_value = self.send(prop[:name].to_sym)
|
648
877
|
|
649
|
-
|
878
|
+
if prop_value.nil? && !prop[:options][:default].nil?
|
879
|
+
prop_value = prop[:options][:default]
|
880
|
+
|
881
|
+
# cast prop_value to proper class if they are not in it
|
882
|
+
# for example 'property :wage, Float, :sortable => true, :default => 20_000' turn 20_000 to 20_000.0
|
883
|
+
if prop[:class] != prop_value.class.to_s
|
884
|
+
prop_value = case prop[:class]
|
885
|
+
when 'Time'
|
886
|
+
begin
|
887
|
+
value.to_s.to_time(:local)
|
888
|
+
rescue ArgumentError => e
|
889
|
+
nil
|
890
|
+
end
|
891
|
+
when 'Integer'
|
892
|
+
prop_value.to_i
|
893
|
+
when 'Float'
|
894
|
+
prop_value.to_f
|
895
|
+
when 'RedisOrm::Boolean'
|
896
|
+
(prop_value == "false" || prop_value == false) ? false : true
|
897
|
+
end
|
898
|
+
end
|
899
|
+
|
900
|
+
# set instance variable in order to properly save indexes here
|
901
|
+
self.instance_variable_set(:"@#{prop[:name]}", prop_value)
|
902
|
+
instance_variable_set :"@#{prop[:name]}_changes", [prop_value]
|
903
|
+
end
|
904
|
+
|
905
|
+
# serialize array- and hash-type properties
|
906
|
+
if ['Array', 'Hash'].include?(prop[:class]) && !prop_value.is_a?(String)
|
907
|
+
prop_value = Marshal.dump(prop_value)
|
908
|
+
end
|
909
|
+
|
910
|
+
#TODO put out of loop
|
911
|
+
$redis.hset(__redis_record_key, prop[:name].to_s, prop_value)
|
912
|
+
|
913
|
+
set_expire_on_reference_key(__redis_record_key)
|
914
|
+
|
915
|
+
# reducing @#{prop[:name]}_changes array to the last value
|
916
|
+
prop_changes = instance_variable_get :"@#{prop[:name]}_changes"
|
917
|
+
|
918
|
+
if prop_changes && prop_changes.size > 2
|
919
|
+
instance_variable_set :"@#{prop[:name]}_changes", [prop_changes.last]
|
920
|
+
end
|
921
|
+
|
922
|
+
# if some property need to be sortable add id of the record to the appropriate sorted set
|
923
|
+
if prop[:options][:sortable]
|
924
|
+
property_value = instance_variable_get(:"@#{prop[:name]}").to_s
|
925
|
+
if prop[:class].eql?("String")
|
926
|
+
sortable_key = "#{model_name}:#{prop[:name]}_ids"
|
927
|
+
el_or_position_to_insert = find_position_to_insert(sortable_key, property_value)
|
928
|
+
el_or_position_to_insert == 0 ? $redis.lpush(sortable_key, "#{property_value}:#{@id}") : $redis.linsert(sortable_key, "AFTER", el_or_position_to_insert, "#{property_value}:#{@id}")
|
929
|
+
# add to every indexed property
|
930
|
+
@@indices[model_name].each do |index|
|
931
|
+
sortable_key = "#{_construct_prepared_index(index)}:#{prop[:name]}_ids"
|
932
|
+
el_or_position_to_insert == 0 ? $redis.lpush(sortable_key, "#{property_value}:#{@id}") : $redis.linsert(sortable_key, "AFTER", el_or_position_to_insert, "#{property_value}:#{@id}")
|
933
|
+
end
|
934
|
+
else
|
935
|
+
score = case prop[:class]
|
936
|
+
when "Integer"; property_value.to_f
|
937
|
+
when "Float"; property_value.to_f
|
938
|
+
when "RedisOrm::Boolean"; (property_value == true ? 1.0 : 0.0)
|
939
|
+
when "Time"; property_value.to_f
|
940
|
+
end
|
941
|
+
$redis.zadd "#{model_name}:#{prop[:name]}_ids", score, @id
|
942
|
+
# add to every indexed property
|
943
|
+
@@indices[model_name].each do |index|
|
944
|
+
$redis.zadd "#{_construct_prepared_index(index)}:#{prop[:name]}_ids", score, @id
|
945
|
+
end
|
946
|
+
end
|
947
|
+
end
|
650
948
|
end
|
949
|
+
end
|
950
|
+
|
951
|
+
def _save_new_indices
|
952
|
+
# save new indices (not *reference* onces (for example not these *belongs_to :note, :index => true*)) in order to sort by finders
|
953
|
+
# city:name:Chicago => 1
|
954
|
+
@@indices[model_name].reject{|index| index[:options][:reference]}.each do |index|
|
955
|
+
prepared_index = _construct_prepared_index(index) # instance method not class one!
|
956
|
+
|
957
|
+
if index[:options][:unique]
|
958
|
+
$redis.set(prepared_index, @id)
|
959
|
+
else
|
960
|
+
$redis.zadd(prepared_index, Time.now.to_f, @id)
|
961
|
+
end
|
962
|
+
end
|
963
|
+
end
|
651
964
|
end
|
652
965
|
end
|