Class: RedisOrm::Base

Inherits:
Object
  • Object
show all
Extended by:
Associations::BelongsTo, Associations::HasMany, Associations::HasOne
Includes:
ActiveModel::Validations, ActiveModelBehavior, Associations::HasManyHelper, Utils
Defined in:
lib/redis_orm/redis_orm.rb

Constant Summary collapse

@@properties =
Hash.new{|h,k| h[k] = []}
@@indices =

compound indices are available too

Hash.new{|h,k| h[k] = []}
@@associations =
Hash.new{|h,k| h[k] = []}
@@callbacks =
Hash.new{|h,k| h[k] = {}}
@@use_uuid_as_id =
{}
@@descendants =
[]
@@expire =
Hash.new{|h,k| h[k] = {}}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Associations::BelongsTo

belongs_to

Methods included from Associations::HasMany

has_many

Methods included from Associations::HasOne

has_one

Methods included from Utils

#calculate_key_for_zset

Methods included from ActiveModelBehavior

included

Constructor Details

#initialize(attributes = {}, id = nil, persisted = false) ⇒ Base

Returns a new instance of Base.



467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/redis_orm/redis_orm.rb', line 467

def initialize(attributes = {}, id = nil, persisted = false)
  @persisted = persisted

  # if this model uses uuid then id is a string otherwise it should be casted to Integer class
  id = @@use_uuid_as_id[model_name] ? id : id.to_i

  instance_variable_set(:"@id", id) if id

  # when object is created with empty attribute set @#{prop[:name]}_changes array properly
  @@properties[model_name].each do |prop|
    if prop[:options][:default]
      instance_variable_set :"@#{prop[:name]}_changes", [prop[:options][:default]]
    else
      instance_variable_set :"@#{prop[:name]}_changes", []
    end
  end

  # cast all attributes' keys to symbols
  attributes = attributes.inject({}){|sum, el| sum.merge({el[0].to_sym => el[1]})} if attributes.is_a?(Hash)

  # get all names of properties to assign only those attributes from attributes hash whose key are in prop_names 
  # we're not using *self.respond_to?("#{key}=".to_sym)* since *belongs_to* and other assocs could create their own methods 
  # with *key=* name, that in turn will mess up indices
  if attributes.is_a?(Hash) && !attributes.empty?        
    @@properties[model_name].each do |property|
      if !(value = attributes[property[:name]]).nil? # check for nil because we want to pass falses too (and value could be 'false')
        value = Marshal.load(value) if ["Array", "Hash"].include?(property[:class]) && value.is_a?(String)
        self.send("#{property[:name]}=".to_sym, value)
      end
    end
  end
  self
end

Instance Attribute Details

#persistedObject

Returns the value of attribute persisted.



50
51
52
# File 'lib/redis_orm/redis_orm.rb', line 50

def persisted
  @persisted
end

Class Method Details

.after_create(callback) ⇒ Object



353
354
355
# File 'lib/redis_orm/redis_orm.rb', line 353

def after_create(callback)        
  @@callbacks[model_name][:after_create] << callback
end

.after_destroy(callback) ⇒ Object



361
362
363
# File 'lib/redis_orm/redis_orm.rb', line 361

def after_destroy(callback)
  @@callbacks[model_name][:after_destroy] << callback
end

.after_save(callback) ⇒ Object



345
346
347
# File 'lib/redis_orm/redis_orm.rb', line 345

def after_save(callback)        
  @@callbacks[model_name][:after_save] << callback
end

.all(options = {}) ⇒ Object

TODO refactor this messy function



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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
# File 'lib/redis_orm/redis_orm.rb', line 200

def all(options = {})
  limit = if options[:limit] && options[:offset]
    [options[:offset].to_i, options[:limit].to_i]
  elsif options[:limit]
    [0, options[:limit].to_i]
  else
    [0, -1]
  end
  
  order_max_limit = Time.now.to_f
  ids_key = "#{model_name}:ids"
  index = nil

  prepared_index = if !options[:conditions].blank? && options[:conditions].is_a?(Hash)
    properties = options[:conditions].collect{|key, value| key}
    
    # if some condition includes object => get only the id of this object
    conds = options[:conditions].inject({}) do |sum, item|
      key, value = item
      if value.respond_to?(:model_name)
        sum.merge!({key => value.id})
      else
        sum.merge!({key => value})
      end
    end

    index = find_indices(properties, :first => true)
    
    raise NotIndexFound if !index

    construct_prepared_index(index, conds)
  else
    if options[:order] && options[:order].is_a?(Array)
      model_name
    else
      ids_key
    end
  end

  order_by_property_is_string = false
  
  # if not array => created_at native order (in which ids were pushed to "#{model_name}:ids" set by default)
  direction = if !options[:order].blank?
    property = {}
    dir = if options[:order].is_a?(Array)
      property = @@properties[model_name].detect{|prop| prop[:name].to_s == options[:order].first.to_s}
      # for String values max limit for search key could be 1.0, but for Numeric values there's actually no limit
      order_max_limit = 100_000_000_000
      ids_key = "#{prepared_index}:#{options[:order].first}_ids"
      options[:order].size == 2 ? options[:order].last : 'asc'
    else
      property = @@properties[model_name].detect{|prop| prop[:name].to_s == options[:order].to_s}
      ids_key = prepared_index
      options[:order]
    end
    if property && property[:class].eql?("String") && property[:options][:sortable]
      order_by_property_is_string = true
    end
    dir
  else
    ids_key = prepared_index
    'asc'
  end
  
  if order_by_property_is_string
    if direction.to_s == 'desc'
      ids_length = $redis.llen(ids_key)
      limit = if options[:offset] && options[:limit]
        [(ids_length - options[:offset].to_i - options[:limit].to_i), (ids_length - options[:offset].to_i - 1)]
      elsif options[:limit]
        [ids_length - options[:limit].to_i, ids_length]
      elsif options[:offset]
        [0, (ids_length - options[:offset].to_i - 1)]
      else
        [0, -1]
      end
      $redis.lrange(ids_key, *limit).reverse.compact.collect{|id| find(id.split(':').last)}
    else
      limit = if options[:offset] && options[:limit]
        [options[:offset].to_i, (options[:offset].to_i + options[:limit].to_i)]
      elsif options[:limit]
        [0, options[:limit].to_i - 1]
      elsif options[:offset]
        [options[:offset].to_i, -1]
      else
        [0, -1]
      end
      $redis.lrange(ids_key, *limit).compact.collect{|id| find(id.split(':').last)}
    end
  else
    if index && index[:options][:unique]
      id = $redis.get prepared_index
      model_name.to_s.camelize.constantize.find(id)
    else
      if direction.to_s == 'desc'
        $redis.zrevrangebyscore(ids_key, order_max_limit, 0, :limit => limit).compact.collect{|id| find(id)}
      else
        $redis.zrangebyscore(ids_key, 0, order_max_limit, :limit => limit).compact.collect{|id| find(id)}
      end
    end
  end
end

.before_create(callback) ⇒ Object



357
358
359
# File 'lib/redis_orm/redis_orm.rb', line 357

def before_create(callback)
  @@callbacks[model_name][:before_create] << callback
end

.before_destroy(callback) ⇒ Object



365
366
367
# File 'lib/redis_orm/redis_orm.rb', line 365

def before_destroy(callback)        
  @@callbacks[model_name][:before_destroy] << callback
end

.before_save(callback) ⇒ Object



349
350
351
# File 'lib/redis_orm/redis_orm.rb', line 349

def before_save(callback)        
  @@callbacks[model_name][:before_save] << callback
end

.construct_prepared_index(index, conditions_hash) ⇒ Object



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/redis_orm/redis_orm.rb', line 180

def construct_prepared_index(index, conditions_hash)
  prepared_index = model_name.to_s
 
  # in order not to depend on order of keys in *:conditions* hash we rather interate over the index itself and find corresponding values in *:conditions* hash
  if index[:name].is_a?(Array)
    index[:name].each do |key|
      # raise if User.find_by_firstname_and_castname => there's no *castname* in User's properties
      raise ArgumentsMismatch if !@@properties[model_name].detect{|p| p[:name] == key.to_sym}
      prepared_index += ":#{key}:#{conditions_hash[key]}"
    end
  else
    prepared_index += ":#{index[:name]}:#{conditions_hash[index[:name]]}"
  end
        
  prepared_index.downcase! if index[:options][:case_insensitive]
  
  prepared_index
end

.countObject



144
145
146
# File 'lib/redis_orm/redis_orm.rb', line 144

def count
  $redis.zcard("#{model_name}:ids").to_i
end

.create(options = {}) ⇒ Object Also known as: create!



369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/redis_orm/redis_orm.rb', line 369

def create(options = {})
  obj = new(options, nil, false)
  obj.save

  # make possible binding related models while creating class instance
  options.each do |k, v|
    if @@associations[model_name].detect{|h| h[:foreign_model] == k || h[:options][:as] == k}
      obj.send("#{k}=", v)
    end
  end
  
  $redis.expire(obj.__redis_record_key, options[:expire_in].to_i) if !options[:expire_in].blank?

  obj
end

.descendantsObject



70
71
72
# File 'lib/redis_orm/redis_orm.rb', line 70

def descendants
  @@descendants
end

.expire(seconds, options = {}) ⇒ Object



135
136
137
# File 'lib/redis_orm/redis_orm.rb', line 135

def expire(seconds, options = {})
  @@expire[model_name] = {:seconds => seconds, :options => options}
end

.find(*args) ⇒ Object



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'lib/redis_orm/redis_orm.rb', line 303

def find(*args)
  if args.first.is_a?(Array)
    return [] if args.first.empty?
    args.first.inject([]) do |array, id|
      record = $redis.hgetall "#{model_name}:#{id}"
      if record && !record.empty?
        array << new(record, id, true)
      end
    end
  else
    return nil if args.empty? || args.first.nil?
    case first = args.shift
      when :all
        options = args.last
        options = {} if !options.is_a?(Hash)
        all(options)
      when :first
        options = args.last
        options = {} if !options.is_a?(Hash)
        all(options.merge({:limit => 1}))[0]
      when :last
        options = args.last
        options = {} if !options.is_a?(Hash)
        reversed = options[:order] == 'desc' ? 'asc' : 'desc'
        all(options.merge({:limit => 1, :order => reversed}))[0]
      else
        id = first
        record = $redis.hgetall "#{model_name}:#{id}"
        record && record.empty? ? nil : new(record, id, true)
    end
  end        
end

.find!(*args) ⇒ Object



336
337
338
339
340
341
342
343
# File 'lib/redis_orm/redis_orm.rb', line 336

def find!(*args)
  result = find(*args)
  if result.nil?
    raise RecordNotFound
  else
    result
  end
end

.find_indices(properties, options = {}) ⇒ Object



166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/redis_orm/redis_orm.rb', line 166

def find_indices(properties, options = {})
  properties.map!{|p| p.to_sym}
  method = options[:first] ? :detect : :select
  
  @@indices[model_name].send(method) do |models_index|
    if models_index[:name].is_a?(Array) && models_index[:name].size == properties.size
      # check the elements not taking into account their order
      (models_index[:name] & properties).size == properties.size
    elsif !models_index[:name].is_a?(Array) && properties.size == 1
      models_index[:name] == properties[0]
    end
  end
end

.first(options = {}) ⇒ Object



148
149
150
151
152
153
154
155
# File 'lib/redis_orm/redis_orm.rb', line 148

def first(options = {})
  if options.empty?
    id = $redis.zrangebyscore("#{model_name}:ids", 0, Time.now.to_f, :limit => [0, 1])
    id.empty? ? nil : find(id[0])
  else
    find(:first, options)
  end
end

.index(name, options = {}) ⇒ Object

options currently supports

*unique* Boolean
*case_insensitive* Boolean


77
78
79
# File 'lib/redis_orm/redis_orm.rb', line 77

def index(name, options = {})
  @@indices[model_name] << {:name => name, :options => options}
end

.inherited(from) ⇒ Object



62
63
64
65
66
67
68
# File 'lib/redis_orm/redis_orm.rb', line 62

def inherited(from)
  [:after_save, :before_save, :after_create, :before_create, :after_destroy, :before_destroy].each do |callback_name|
    @@callbacks[from.model_name][callback_name] = []
  end
  
  @@descendants << from
end

.last(options = {}) ⇒ Object



157
158
159
160
161
162
163
164
# File 'lib/redis_orm/redis_orm.rb', line 157

def last(options = {})
  if options.empty?
    id = $redis.zrevrangebyscore("#{model_name}:ids", Time.now.to_f, 0, :limit => [0, 1])
    id.empty? ? nil : find(id[0])
  else
    find(:last, options)
  end
end

.method_missing(method_name, *args, &block) ⇒ Object

dynamic finders



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
# File 'lib/redis_orm/redis_orm.rb', line 388

def method_missing(method_name, *args, &block)
  if method_name =~ /^find_(all_)?by_(\w*)/
    
    index = if $2
      properties = $2.split('_and_')
      raise ArgumentsMismatch if properties.size != args.size
      properties_hash = {}
      properties.each_with_index do |prop, i| 
        properties_hash.merge!({prop.to_sym => args[i]})
      end
      find_indices(properties, :first => true)
    end

    raise NotIndexFound if !index
    
    prepared_index = construct_prepared_index(index, properties_hash)

    if method_name =~ /^find_by_(\w*)/
      id = if index[:options][:unique]            
        $redis.get prepared_index
      else
        $redis.zrangebyscore(prepared_index, 0, Time.now.to_f, :limit => [0, 1])[0]
      end
      model_name.to_s.camelize.constantize.find(id)
    elsif method_name =~ /^find_all_by_(\w*)/
      records = []          

      if index[:options][:unique]            
        id = $redis.get prepared_index
        records << model_name.to_s.camelize.constantize.find(id)
      else
        ids = $redis.zrangebyscore(prepared_index, 0, Time.now.to_f)
        records += model_name.to_s.camelize.constantize.find(ids)
      end          

      records
    else
      nil
    end
  end
end

.property(property_name, class_name, options = {}) ⇒ Object



81
82
83
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
112
113
114
115
116
117
118
119
120
121
# File 'lib/redis_orm/redis_orm.rb', line 81

def property(property_name, class_name, options = {})
  @@properties[model_name] << {:name => property_name, :class => class_name.to_s, :options => options}

  send(:define_method, property_name) do
    value = instance_variable_get(:"@#{property_name}")

    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
    if /DateTime|Time/ =~ class_name.to_s            
      # we're using to_datetime here because to_time doesn't manage timezone correctly
      value.to_s.to_datetime rescue nil
    elsif Integer == class_name
      value.to_i
    elsif Float == class_name
      value.to_f
    elsif RedisOrm::Boolean == class_name
      ((value == "false" || value == false) ? false : true)
    else
      value
    end
  end
    
  send(:define_method, "#{property_name}=".to_sym) do |value|
    if instance_variable_get(:"@#{property_name}_changes") && !instance_variable_get(:"@#{property_name}_changes").empty?
      initial_value = instance_variable_get(:"@#{property_name}_changes")[0]
      instance_variable_set(:"@#{property_name}_changes", [initial_value, value])
    elsif instance_variable_get(:"@#{property_name}")
      instance_variable_set(:"@#{property_name}_changes", [self.send(property_name), value])
    else
      instance_variable_set(:"@#{property_name}_changes", [value])
    end
    instance_variable_set(:"@#{property_name}", value)
  end
  
  send(:define_method, "#{property_name}_changes".to_sym) do
    instance_variable_get(:"@#{property_name}_changes")
  end
  
  send(:define_method, "#{property_name}_changed?".to_sym) do
    instance_variable_get(:"@#{property_name}_changes").size > 1
  end
end

.timestampsObject



123
124
125
126
127
128
129
130
131
132
133
# File 'lib/redis_orm/redis_orm.rb', line 123

def timestamps
  #if !@@properties[model_name].detect{|p| p[:name] == :created_at && p[:class] == "Time"}
  if !instance_methods.include?(:created_at) && !instance_methods.include?(:"created_at=")
    property :created_at, Time
  end
  
  #if !@@properties[model_name].detect{|p| p[:name] == :modified_at && p[:class] == "Time"}
  if !instance_methods.include?(:modified_at) && !instance_methods.include?(:"modified_at=")
    property :modified_at, Time
  end
end

.use_uuid_as_idObject



139
140
141
142
# File 'lib/redis_orm/redis_orm.rb', line 139

def use_uuid_as_id
  @@use_uuid_as_id[model_name] = true
  @@uuid = UUID.new
end

Instance Method Details

#==(other) ⇒ Object



519
520
521
522
523
524
525
526
527
528
# File 'lib/redis_orm/redis_orm.rb', line 519

def ==(other)
  raise "this object could be comparable only with object of the same class" if other.class != self.class
  same = true
  @@properties[model_name].each do |prop|
    self_var = instance_variable_get(:"@#{prop[:name]}")
    same = false if other.send(prop[:name]).to_s != self_var.to_s
  end
  same = false if self.id != other.id
  same
end

#__redis_record_keyObject



437
438
439
# File 'lib/redis_orm/redis_orm.rb', line 437

def __redis_record_key
  "#{model_name}:#{id}"
end

#destroyObject



633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
# File 'lib/redis_orm/redis_orm.rb', line 633

def destroy
  @@callbacks[model_name][:before_destroy].each do |callback|
    self.send(callback)
  end

  @@properties[model_name].each do |prop|
    property_value = instance_variable_get(:"@#{prop[:name]}").to_s
    $redis.hdel("#{model_name}:#{@id}", prop[:name].to_s)
    
    if prop[:options][:sortable]
      if prop[:class].eql?("String")
        $redis.lrem "#{model_name}:#{prop[:name]}_ids", 1, "#{property_value}:#{@id}"
      else
        $redis.zrem "#{model_name}:#{prop[:name]}_ids", @id
      end
    end
  end

  $redis.zrem "#{model_name}:ids", @id

  # also we need to delete *indices* of associated records
  if !@@associations[model_name].empty?
    @@associations[model_name].each do |assoc|        
      if :belongs_to == assoc[:type]
        # if assoc has :as option
        foreign_model_name = assoc[:options][:as] ? assoc[:options][:as].to_sym : assoc[:foreign_model].to_sym
        
        if !self.send(foreign_model_name).nil?
          @@indices[model_name].each do |index|
            keys_to_delete = if index[:name].is_a?(Array)
              full_index = index[:name].inject([]){|sum, index_part| sum << index_part}.join(':')
              $redis.keys "#{foreign_model_name}:#{self.send(foreign_model_name).id}:#{model_name.to_s.pluralize}:#{full_index}:*"
            else
              ["#{foreign_model_name}:#{self.send(foreign_model_name).id}:#{model_name.to_s.pluralize}:#{index[:name]}:#{self.send(index[:name])}"]
            end
            keys_to_delete.each do |key| 
              index[:options][:unique] ? $redis.del(key) : $redis.zrem(key, @id)
            end
          end
        end
      end
    end
  end
  
  # also we need to delete *links* to associated records
  if !@@associations[model_name].empty?
    @@associations[model_name].each do |assoc|

      foreign_model  = ""
      records = []

      case assoc[:type]
        when :belongs_to
          foreign_model = assoc[:foreign_model].to_s
          foreign_model_name = assoc[:options][:as] ? assoc[:options][:as] : assoc[:foreign_model]
          if assoc[:options][:polymorphic]
            records << self.send(foreign_model_name)
            # get real foreign_model's name in order to delete backlinks properly
            foreign_model = $redis.get("#{model_name}:#{id}:#{foreign_model_name}_type")
            $redis.del("#{model_name}:#{id}:#{foreign_model_name}_type")
            $redis.del("#{model_name}:#{id}:#{foreign_model_name}_id")
          else
            records << self.send(foreign_model_name)
            $redis.del "#{model_name}:#{@id}:#{assoc[:foreign_model]}"
          end
        when :has_one
          foreign_model = assoc[:foreign_model].to_s
          foreign_model_name = assoc[:options][:as] ? assoc[:options][:as] : assoc[:foreign_model]
          records << self.send(foreign_model_name)

          $redis.del "#{model_name}:#{@id}:#{assoc[:foreign_model]}"
        when :has_many
          foreign_model = assoc[:foreign_models].to_s.singularize
          foreign_models_name = assoc[:options][:as] ? assoc[:options][:as] : assoc[:foreign_models]
          records += self.send(foreign_models_name)

          # delete all members             
          $redis.zremrangebyscore "#{model_name}:#{@id}:#{assoc[:foreign_models]}", 0, Time.now.to_f
      end

      # check whether foreign_model also has an assoc to the destroying record
      # and remove an id of destroing record from each of associated sets
      if !records.compact.empty?
        records.compact.each do |record|
          # we make 3 different checks rather then 1 with elsif to ensure that all associations will be processed
          # it's covered in test/option_test in "should delete link to associated record when record was deleted" scenario
          # for if class Album; has_one :photo, :as => :front_photo; has_many :photos; end
          # end some photo from the album will be deleted w/o these checks only first has_one will be triggered
          if @@associations[foreign_model].detect{|h| h[:type] == :belongs_to && h[:foreign_model] == model_name.to_sym}
            $redis.del "#{foreign_model}:#{record.id}:#{model_name}"
          end

          if @@associations[foreign_model].detect{|h| h[:type] == :has_one && h[:foreign_model] == model_name.to_sym}
            $redis.del "#{foreign_model}:#{record.id}:#{model_name}"
          end

          if @@associations[foreign_model].detect{|h| h[:type] == :has_many && h[:foreign_models] == model_name.pluralize.to_sym}
            $redis.zrem "#{foreign_model}:#{record.id}:#{model_name.pluralize}", @id
          end
        end
      end

      if assoc[:options][:dependent] == :destroy
        if !records.compact.empty?
          records.compact.each do |r|
            r.destroy
          end
        end
      end
    end
  end

  # remove all associated indices
  @@indices[model_name].each do |index|
    prepared_index = _construct_prepared_index(index) # instance method not class one!

    if index[:options][:unique]
      $redis.del(prepared_index)
    else
      $redis.zremrangebyscore(prepared_index, 0, Time.now.to_f)
    end
  end

  @@callbacks[model_name][:after_destroy].each do |callback|
    self.send(callback)
  end

  true # if there were no errors just return true, so *if* conditions would work
end

#find_position_to_insert(sortable_key, value) ⇒ Object



578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
# File 'lib/redis_orm/redis_orm.rb', line 578

def find_position_to_insert(sortable_key, value)
  end_index = $redis.llen(sortable_key)

  return 0 if end_index == 0
  
  start_index = 0
  pivot_index = end_index / 2

  start_el = $redis.lindex(sortable_key, start_index)
  end_el   = $redis.lindex(sortable_key, end_index - 1)
  pivot_el = $redis.lindex(sortable_key, pivot_index)

  while start_index != end_index
    # aa..ab..ac..bd <- ad
    if start_el.split(':').first > value # Michael > Abe
      return 0
    elsif end_el.split(':').first < value # Abe < Todd 
      return end_el
    elsif start_el.split(':').first == value # Abe == Abe
      return start_el
    elsif pivot_el.split(':').first == value # Todd == Todd
      return pivot_el
    elsif end_el.split(':').first == value
      return end_el
    elsif (start_el.split(':').first < value) && (pivot_el.split(':').first > value)
      start_index = start_index
      prev_pivot_index = pivot_index
      pivot_index = start_index + ((end_index - pivot_index) / 2)
      end_index   = prev_pivot_index
    elsif (pivot_el.split(':').first < value) && (end_el.split(':').first > value) # M < V && Y > V
      start_index = pivot_index
      pivot_index = pivot_index + ((end_index - pivot_index) / 2)
      end_index   = end_index          
    end
    start_el = $redis.lindex(sortable_key, start_index)
    end_el   = $redis.lindex(sortable_key, end_index - 1)
    pivot_el = $redis.lindex(sortable_key, pivot_index)
  end
  start_el
end

#get_associationsObject

is called from RedisOrm::Associations::HasMany to save backlinks to saved records



458
459
460
# File 'lib/redis_orm/redis_orm.rb', line 458

def get_associations
  @@associations[self.model_name]
end

#get_indicesObject

is called from RedisOrm::Associations::HasMany to correctly save indices for associated records



463
464
465
# File 'lib/redis_orm/redis_orm.rb', line 463

def get_indices
  @@indices[self.model_name]
end

#get_next_idObject



534
535
536
537
538
539
540
# File 'lib/redis_orm/redis_orm.rb', line 534

def get_next_id
  if @@use_uuid_as_id[model_name]
    @@uuid.generate(:compact)
  else
    $redis.incr("#{model_name}:id")
  end
end

#idObject Also known as: to_key



501
502
503
# File 'lib/redis_orm/redis_orm.rb', line 501

def id
  @id
end

#persisted?Boolean

Returns:



530
531
532
# File 'lib/redis_orm/redis_orm.rb', line 530

def persisted?
  @persisted
end

#saveObject



542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
# File 'lib/redis_orm/redis_orm.rb', line 542

def save
  return false if !valid?

  _check_mismatched_types_for_values
  
  # store here initial persisted flag so we could invoke :after_create callbacks in the end of *save* function
  was_persisted = persisted?

  if persisted? # then there might be old indices
    _check_indices_for_persisted # remove old indices if needed
  else # !persisted?        
    @@callbacks[model_name][:before_create].each{ |callback| self.send(callback) }
 
    @id = get_next_id
    $redis.zadd "#{model_name}:ids", Time.now.to_f, @id
    @persisted = true
    self.created_at = Time.now if respond_to? :created_at
  end

  @@callbacks[model_name][:before_save].each{ |callback| self.send(callback) }

  # automatically update *modified_at* property if it was defined
  self.modified_at = Time.now if respond_to? :modified_at

  _save_to_redis # main work done here
  _save_new_indices

  @@callbacks[model_name][:after_save].each{ |callback| self.send(callback) }

  if ! was_persisted
    @@callbacks[model_name][:after_create].each{ |callback| self.send(callback) }
  end

  true # if there were no errors just return true, so *if obj.save* conditions would work
end

#set_expire_on_reference_key(key) ⇒ Object



441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# File 'lib/redis_orm/redis_orm.rb', line 441

def set_expire_on_reference_key(key)
  class_expire = @@expire[model_name]

  # if class method *expire* was invoked and number of seconds was specified then set expiry date on the HSET record key
  if class_expire[:seconds]
    set_expire = true

    if class_expire[:options][:if] && class_expire[:options][:if].class == Proc
      # *self* here refers to the instance of class which has_one association
      set_expire = class_expire[:options][:if][self]  # invoking specified *:if* Proc with current record as *self* 
    end

    $redis.expire(key, class_expire[:seconds].to_i) if set_expire
  end
end

#to_aObject

could be invoked from has_many module (<< method)



433
434
435
# File 'lib/redis_orm/redis_orm.rb', line 433

def to_a
  [self]
end

#to_sObject



507
508
509
510
511
512
513
514
515
516
517
# File 'lib/redis_orm/redis_orm.rb', line 507

def to_s
  inspected = "<#{model_name.capitalize} id: #{@id}, "
  inspected += @@properties[model_name].inject([]) do |sum, prop|
    property_value = instance_variable_get(:"@#{prop[:name]}")
    property_value = '"' + property_value.to_s + '"' if prop[:class].eql?("String")
    property_value = 'nil' if property_value.nil?
    sum << "#{prop[:name]}: " + property_value.to_s
  end.join(', ')
  inspected += ">"
  inspected
end

#update_attribute(attribute_name, attribute_value) ⇒ Object



628
629
630
631
# File 'lib/redis_orm/redis_orm.rb', line 628

def update_attribute(attribute_name, attribute_value)
  self.send("#{attribute_name}=".to_sym, attribute_value) if self.respond_to?("#{attribute_name}=".to_sym)
  save
end

#update_attributes(attributes) ⇒ Object



619
620
621
622
623
624
625
626
# File 'lib/redis_orm/redis_orm.rb', line 619

def update_attributes(attributes)
  if attributes.is_a?(Hash)
    attributes.each do |key, value|
      self.send("#{key}=".to_sym, value) if self.respond_to?("#{key}=".to_sym)
    end
  end
  save
end