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.



465
466
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
# File 'lib/redis_orm/redis_orm.rb', line 465

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.



48
49
50
# File 'lib/redis_orm/redis_orm.rb', line 48

def persisted
  @persisted
end

Class Method Details

.after_create(callback) ⇒ Object



351
352
353
# File 'lib/redis_orm/redis_orm.rb', line 351

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

.after_destroy(callback) ⇒ Object



359
360
361
# File 'lib/redis_orm/redis_orm.rb', line 359

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

.after_save(callback) ⇒ Object



343
344
345
# File 'lib/redis_orm/redis_orm.rb', line 343

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

.all(options = {}) ⇒ Object

TODO refactor this messy function



198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
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
# File 'lib/redis_orm/redis_orm.rb', line 198

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



355
356
357
# File 'lib/redis_orm/redis_orm.rb', line 355

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

.before_destroy(callback) ⇒ Object



363
364
365
# File 'lib/redis_orm/redis_orm.rb', line 363

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

.before_save(callback) ⇒ Object



347
348
349
# File 'lib/redis_orm/redis_orm.rb', line 347

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

.construct_prepared_index(index, conditions_hash) ⇒ Object



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

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



142
143
144
# File 'lib/redis_orm/redis_orm.rb', line 142

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

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



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

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



68
69
70
# File 'lib/redis_orm/redis_orm.rb', line 68

def descendants
  @@descendants
end

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



133
134
135
# File 'lib/redis_orm/redis_orm.rb', line 133

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

.find(*args) ⇒ Object



301
302
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
# File 'lib/redis_orm/redis_orm.rb', line 301

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



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

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

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



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

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



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

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


75
76
77
# File 'lib/redis_orm/redis_orm.rb', line 75

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

.inherited(from) ⇒ Object



60
61
62
63
64
65
66
# File 'lib/redis_orm/redis_orm.rb', line 60

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



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

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



386
387
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
# File 'lib/redis_orm/redis_orm.rb', line 386

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



79
80
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
# File 'lib/redis_orm/redis_orm.rb', line 79

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



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

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



137
138
139
140
# File 'lib/redis_orm/redis_orm.rb', line 137

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

Instance Method Details

#==(other) ⇒ Object



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

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



435
436
437
# File 'lib/redis_orm/redis_orm.rb', line 435

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

#destroyObject



631
632
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
# File 'lib/redis_orm/redis_orm.rb', line 631

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



576
577
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
# File 'lib/redis_orm/redis_orm.rb', line 576

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



456
457
458
# File 'lib/redis_orm/redis_orm.rb', line 456

def get_associations
  @@associations[self.model_name]
end

#get_indicesObject

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



461
462
463
# File 'lib/redis_orm/redis_orm.rb', line 461

def get_indices
  @@indices[self.model_name]
end

#get_next_idObject



532
533
534
535
536
537
538
# File 'lib/redis_orm/redis_orm.rb', line 532

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



499
500
501
# File 'lib/redis_orm/redis_orm.rb', line 499

def id
  @id
end

#persisted?Boolean

Returns:



528
529
530
# File 'lib/redis_orm/redis_orm.rb', line 528

def persisted?
  @persisted
end

#saveObject



540
541
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
# File 'lib/redis_orm/redis_orm.rb', line 540

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



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

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)



431
432
433
# File 'lib/redis_orm/redis_orm.rb', line 431

def to_a
  [self]
end

#to_sObject



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

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



626
627
628
629
# File 'lib/redis_orm/redis_orm.rb', line 626

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



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

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