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 =
[]

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.



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/redis_orm/redis_orm.rb', line 425

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
 
  if attributes.is_a?(Hash) && !attributes.empty?        
    attributes.each do |key, value|
      self.send("#{key}=".to_sym, value) if self.respond_to?("#{key}=".to_sym)
    end
  end
  self
end

Instance Attribute Details

#persistedObject

Returns the value of attribute persisted.



42
43
44
# File 'lib/redis_orm/redis_orm.rb', line 42

def persisted
  @persisted
end

Class Method Details

.after_create(callback) ⇒ Object



333
334
335
# File 'lib/redis_orm/redis_orm.rb', line 333

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

.after_destroy(callback) ⇒ Object



341
342
343
# File 'lib/redis_orm/redis_orm.rb', line 341

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

.after_save(callback) ⇒ Object



325
326
327
# File 'lib/redis_orm/redis_orm.rb', line 325

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

.all(options = {}) ⇒ Object

TODO refactor this messy function



191
192
193
194
195
196
197
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
# File 'lib/redis_orm/redis_orm.rb', line 191

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}
    index = find_indices(properties, :first => true)
    
    raise NotIndexFound if !index

    construct_prepared_index(index, options[:conditions])
  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



337
338
339
# File 'lib/redis_orm/redis_orm.rb', line 337

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

.before_destroy(callback) ⇒ Object



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

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

.before_save(callback) ⇒ Object



329
330
331
# File 'lib/redis_orm/redis_orm.rb', line 329

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

.construct_prepared_index(index, conditions_hash) ⇒ Object



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/redis_orm/redis_orm.rb', line 171

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



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

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

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



349
350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/redis_orm/redis_orm.rb', line 349

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

  obj
end

.descendantsObject



61
62
63
# File 'lib/redis_orm/redis_orm.rb', line 61

def descendants
  @@descendants
end

.find(*args) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# File 'lib/redis_orm/redis_orm.rb', line 283

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



316
317
318
319
320
321
322
323
# File 'lib/redis_orm/redis_orm.rb', line 316

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

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



157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/redis_orm/redis_orm.rb', line 157

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



139
140
141
142
143
144
145
146
# File 'lib/redis_orm/redis_orm.rb', line 139

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


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

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

.inherited(from) ⇒ Object



53
54
55
56
57
58
59
# File 'lib/redis_orm/redis_orm.rb', line 53

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



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

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



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/redis_orm/redis_orm.rb', line 366

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



72
73
74
75
76
77
78
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
# File 'lib/redis_orm/redis_orm.rb', line 72

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 value 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 Time == class_name
      value = begin
        value.to_s.to_time(:local)
      rescue ArgumentError => e
        nil
      end
    elsif Integer == class_name
      value = value.to_i
    elsif Float == class_name
      value = value.to_f
    elsif RedisOrm::Boolean == class_name
      value = ((value == "false" || value == false) ? false : true)
    end
    value
  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



118
119
120
121
122
123
124
125
126
127
128
# File 'lib/redis_orm/redis_orm.rb', line 118

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



130
131
132
133
# File 'lib/redis_orm/redis_orm.rb', line 130

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

Instance Method Details

#==(other) ⇒ Object



468
469
470
471
472
473
474
475
476
477
# File 'lib/redis_orm/redis_orm.rb', line 468

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

#destroyObject



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
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
# File 'lib/redis_orm/redis_orm.rb', line 726

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



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

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



416
417
418
# File 'lib/redis_orm/redis_orm.rb', line 416

def get_associations
  @@associations[self.model_name]
end

#get_indicesObject

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



421
422
423
# File 'lib/redis_orm/redis_orm.rb', line 421

def get_indices
  @@indices[self.model_name]
end

#get_next_idObject



483
484
485
486
487
488
489
# File 'lib/redis_orm/redis_orm.rb', line 483

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



450
451
452
# File 'lib/redis_orm/redis_orm.rb', line 450

def id
  @id
end

#persisted?Boolean

Returns:



479
480
481
# File 'lib/redis_orm/redis_orm.rb', line 479

def persisted?
  @persisted
end

#saveObject



491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
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
575
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
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
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
# File 'lib/redis_orm/redis_orm.rb', line 491

def save
  return false if !valid?

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

  if persisted? # then there might be old indices
    # check whether there's old indices exists and if yes - delete them
    @@properties[model_name].each do |prop|
      # if there were no changes for current property skip it (indices remains the same)
      next if ! self.send(:"#{prop[:name]}_changed?")
      
      prev_prop_value = instance_variable_get(:"@#{prop[:name]}_changes").first
      prop_value = instance_variable_get(:"@#{prop[:name]}")
      # TODO DRY in destroy also
      if prop[:options][:sortable]
        if prop[:class].eql?("String")
          $redis.lrem "#{model_name}:#{prop[:name]}_ids", 1, "#{prev_prop_value}:#{@id}"
          # remove id from every indexed property
          @@indices[model_name].each do |index|
            $redis.lrem "#{construct_prepared_index(index)}:#{prop[:name]}_ids", 1, "#{prop_value}:#{@id}"
          end
        else
          $redis.zrem "#{model_name}:#{prop[:name]}_ids", @id
          # remove id from every indexed property
          @@indices[model_name].each do |index|
            $redis.zrem "#{construct_prepared_index(index)}:#{prop[:name]}_ids", @id
          end
        end
      end

      indices = @@indices[model_name].inject([]) do |sum, models_index|
        if models_index[:name].is_a?(Array)
          if models_index[:name].include?(prop[:name])
            sum << models_index
          else
            sum
          end
        else
          if models_index[:name] == prop[:name]
            sum << models_index
          else
            sum
          end
        end
      end

      if !indices.empty?
        indices.each do |index|
          if index[:name].is_a?(Array)
            keys_to_delete = if index[:name].index(prop) == 0
              $redis.keys "#{model_name}:#{prop[:name]}#{prev_prop_value}*"
            else
              $redis.keys "#{model_name}:*#{prop[:name]}:#{prev_prop_value}*"
            end

            keys_to_delete.each{|key| $redis.del(key)}
          else
            key_to_delete = "#{model_name}:#{prop[:name]}:#{prev_prop_value}"
            $redis.del key_to_delete
          end

          # also we need to delete associated records *indices*
          if !@@associations[model_name].empty?
            @@associations[model_name].each do |assoc|
              if :belongs_to == assoc[:type]
                if !self.send(assoc[:foreign_model]).nil?
                  if index[:name].is_a?(Array)
                    keys_to_delete = if index[:name].index(prop) == 0
                      $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}#{prev_prop_value}*"
                    else
                      $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:*#{prop[:name]}:#{prev_prop_value}*"
                    end

                    keys_to_delete.each{|key| $redis.del(key)}
                  else
                    beginning_of_the_key = "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}:"

                    $redis.del(beginning_of_the_key + prev_prop_value.to_s)

                    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)
                  end
                end
              end
            end
          end # deleting associated records *indices*

        end
      end
    end
  else # !persisted?        
    @@callbacks[model_name][:before_create].each do |callback|
      self.send(callback)
    end
 
    @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 do |callback|
    self.send(callback)
  end

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

  @@properties[model_name].each do |prop|
    prop_value = self.send(prop[:name].to_sym)
    
    if prop_value.nil? && !prop[:options][:default].nil?
      prop_value = prop[:options][:default]
      # set instance variable in order to properly save indexes here
      self.instance_variable_set(:"@#{prop[:name]}", prop[:options][:default])
      instance_variable_set :"@#{prop[:name]}_changes", [prop[:options][:default]]
    end

    $redis.hset("#{model_name}:#{id}", prop[:name].to_s, prop_value)

    # reducing @#{prop[:name]}_changes array to the last value
    prop_changes = instance_variable_get :"@#{prop[:name]}_changes"

    if prop_changes && prop_changes.size > 2
      instance_variable_set :"@#{prop[:name]}_changes", [prop_changes.last]
    end
    
    # if some property need to be sortable add id of the record to the appropriate sorted set
    if prop[:options][:sortable]
      property_value = instance_variable_get(:"@#{prop[:name]}").to_s
      if prop[:class].eql?("String")
        sortable_key = "#{model_name}:#{prop[:name]}_ids"
        el_or_position_to_insert = find_position_to_insert(sortable_key, property_value)
        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}")
        # add to every indexed property
        @@indices[model_name].each do |index|
          sortable_key = "#{construct_prepared_index(index)}:#{prop[:name]}_ids"
          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}")
        end
      else
        score = case prop[:class]
          when "Integer"; property_value.to_f
          when "Float"; property_value.to_f
          when "RedisOrm::Boolean"; (property_value == true ? 1.0 : 0.0)
          when "Time"; property_value.to_f
        end
        $redis.zadd "#{model_name}:#{prop[:name]}_ids", score, @id
        # add to every indexed property
        @@indices[model_name].each do |index|
          $redis.zadd "#{construct_prepared_index(index)}:#{prop[:name]}_ids", score, @id
        end
      end
    end
  end

  # save new indices in order to sort by finders
  # city:name:Chicago => 1
  @@indices[model_name].each do |index|
    prepared_index = construct_prepared_index(index) # instance method not class one!

    if index[:options][:unique]
      $redis.set(prepared_index, @id)
    else
      $redis.zadd(prepared_index, Time.now.to_f, @id)
    end
  end

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

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

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

#to_aObject

could be invoked from has_many module (<< method)



411
412
413
# File 'lib/redis_orm/redis_orm.rb', line 411

def to_a
  [self]
end

#to_sObject



456
457
458
459
460
461
462
463
464
465
466
# File 'lib/redis_orm/redis_orm.rb', line 456

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



721
722
723
724
# File 'lib/redis_orm/redis_orm.rb', line 721

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



712
713
714
715
716
717
718
719
# File 'lib/redis_orm/redis_orm.rb', line 712

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