redis_orm 0.6.2 → 0.7

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG CHANGED
@@ -1,10 +1,18 @@
1
- v0.6.2 [23-05-2011]
2
- * adds an ability to specify indices on *has_one* assoc
3
- * adds an ability to create indices on belongs_to association
4
- * fixed error with updating indices in :belongs_to assoc with :as option;
5
- * tests refactoring, now all tests are run with Rake::TestTask,
6
- * moved all classes and modules from test cases to special folders
7
- * fixed bug: :default values should be properly transformed to right classes (if :default values are wrong) so when comparing them to other/stored instances they'll be the same
1
+ v0.7 [03-05-2013]
2
+ FEATURES
3
+ * implemented Array and Hash properties types
4
+ * added ability to specify an *expire* value for the record via method of the class and added inline *expire_in* key that can be used while saving objects (referencial keys in expireable record also expireables)
5
+ * Add model generator [Tatsuya Sato]
6
+ BUGS
7
+ * fixed a bug with Date property implementation
8
+ * refactored *save* method
9
+
10
+ v0.6.2 [23-05-2012]
11
+ * adds an ability to specify/create indices on *has_one* and *belongs_to* associations
12
+ * fixed error with updating indices in *belongs_to* association with :as option
13
+ * tests refactoring, now all tests are run with Rake::TestTask
14
+ * moved all classes and modules from test cases to special folders (test/classes, test/modules)
15
+ * fixed bug: :default values should be properly transformed to the right classes (if :default values are wrong) so when comparing them to other/stored instances they'll be the same
8
16
 
9
17
  v0.6.1 [05-12-2011]
10
18
  * rewritten sortable functionality for attributes which values are strings
data/Gemfile CHANGED
@@ -1,8 +1,10 @@
1
1
  source 'http://rubygems.org'
2
2
 
3
- gem 'activemodel', '>= 3.0.0'
4
- gem 'activesupport', '>= 3.0.0'
5
- gem 'redis', '>= 2.2.0'
6
- gem 'uuid', '>= 2.3.2'
3
+ gemspec
7
4
 
8
- gem 'rspec', '>= 2.5.0'
5
+ group :test do
6
+ gem 'rspec', '>= 2.5.0'
7
+ #gem 'linecache19', :git => 'git://github.com/mark-moseley/linecache'
8
+ #gem 'ruby-debug-base19x', '~> 0.11.30.pre4'
9
+ #gem 'ruby-debug19'
10
+ end
data/Manifest CHANGED
@@ -6,6 +6,8 @@ README.md
6
6
  Rakefile
7
7
  TODO
8
8
  benchmarks/sortable_benchmark.rb
9
+ lib/rails/generators/redis_orm/model/model_generator.rb
10
+ lib/rails/generators/redis_orm/model/templates/model.rb.erb
9
11
  lib/redis_orm.rb
10
12
  lib/redis_orm/active_model_behavior.rb
11
13
  lib/redis_orm/associations/belongs_to.rb
@@ -16,6 +18,8 @@ lib/redis_orm/associations/has_one.rb
16
18
  lib/redis_orm/redis_orm.rb
17
19
  lib/redis_orm/utils.rb
18
20
  redis_orm.gemspec
21
+ spec/generators/model_generator_spec.rb
22
+ spec/spec_helper.rb
19
23
  test/association_indices_test.rb
20
24
  test/associations_test.rb
21
25
  test/atomicity_test.rb
@@ -24,6 +28,7 @@ test/callbacks_test.rb
24
28
  test/changes_array_test.rb
25
29
  test/classes/album.rb
26
30
  test/classes/article.rb
31
+ test/classes/article_with_comments.rb
27
32
  test/classes/book.rb
28
33
  test/classes/catalog_item.rb
29
34
  test/classes/category.rb
@@ -36,6 +41,8 @@ test/classes/cutout_aggregator.rb
36
41
  test/classes/default_user.rb
37
42
  test/classes/dynamic_finder_user.rb
38
43
  test/classes/empty_person.rb
44
+ test/classes/expire_user.rb
45
+ test/classes/expire_user_with_predicate.rb
39
46
  test/classes/giftcard.rb
40
47
  test/classes/jigsaw.rb
41
48
  test/classes/location.rb
@@ -53,6 +60,7 @@ test/classes/uuid_timestamp.rb
53
60
  test/classes/uuid_user.rb
54
61
  test/dynamic_finders_test.rb
55
62
  test/exceptions_test.rb
63
+ test/expire_records_test.rb
56
64
  test/has_one_has_many_test.rb
57
65
  test/indices_test.rb
58
66
  test/modules/belongs_to_model_within_module.rb
data/README.md CHANGED
@@ -82,8 +82,11 @@ Supported property types:
82
82
  * **RedisOrm::Boolean**
83
83
  there is no Boolean class in Ruby so it's a special class to store TrueClass or FalseClass objects
84
84
 
85
- * **Time**
85
+ * **Time** or **DateTime**
86
86
 
87
+ * **Array** or **Hash**
88
+ RedisOrm automatically will handle serializing/deserializing arrays and hashes into strings using Marshal class
89
+
87
90
  Following options are available in property declaration:
88
91
 
89
92
  * **:default**
@@ -96,6 +99,31 @@ Following options are available in property declaration:
96
99
 
97
100
  *Note* that when you're using :sortable option redis_orm maintains one additional list per attribute. Also note that the #create method could be 3 times slower in some cases (this will be improved in future releases), while the #find performance is basically the same (see the "benchmarks/sortable_benchmark.rb").
98
101
 
102
+ ## Expiring record after certain period of time
103
+
104
+ You could expire record stored in Redis by specifying TTL in seconds invoking *expire* method of the class like this:
105
+
106
+ ```ruby
107
+ class PhantomUser < RedisOrm::Base
108
+ property :name, String
109
+ property :persist, RedisOrm::Boolean, :default => true
110
+
111
+ expire 15.minutes.from_now
112
+ end
113
+ ```
114
+
115
+ Also you could specify a condition when *expire* would be set on record's key:
116
+
117
+ ```ruby
118
+ expire 15.minutes.from_now, :if => Proc.new {|r| !r.persist?}
119
+ ```
120
+
121
+ Also you could override class method *expire* by using *expire_in* key when saving object:
122
+
123
+ ```ruby
124
+ ExpireUser.create :name => "Ghost record", :expire_in => 50.minutes.from_now
125
+ ```
126
+
99
127
  ## Searching records by the value
100
128
 
101
129
  Usually it's done via declaring an index and using *:conditions* hash or dynamic finders. For example:
@@ -540,6 +568,10 @@ end
540
568
 
541
569
  To run all tests just invoke *rake test*
542
570
 
571
+ ## Contributors
572
+
573
+ [Tatsuya Sato](https://github.com/satoryu)
574
+
543
575
  Copyright © 2011 Dmitrii Samoilov, released under the MIT license
544
576
 
545
577
  Permission is hereby granted, free of charge, to any person obtaining a copy
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
- require 'rubygems'
1
+ #require 'rubygems'
2
2
  require 'rake'
3
3
  require 'rake/testtask'
4
4
 
5
5
  #=begin
6
6
  require 'echoe'
7
7
 
8
- Echoe.new('redis_orm', '0.6.2') do |p|
8
+ Echoe.new('redis_orm', '0.7') do |p|
9
9
  p.description = "ORM for Redis (advanced key-value storage) with ActiveRecord API"
10
10
  p.url = "https://github.com/german/redis_orm"
11
11
  p.author = "Dmitrii Samoilov"
@@ -20,6 +20,6 @@ task :default => :test
20
20
  desc 'Test the redis_orm functionality'
21
21
  Rake::TestTask.new(:test) do |t|
22
22
  t.libs << 'lib'
23
- t.test_files = FileList['test/*_test.rb']
23
+ t.test_files = FileList['test/**/*_test.rb', 'spec/**/*_spec.rb']
24
24
  t.verbose = true
25
25
  end
@@ -0,0 +1,21 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/named_base'
3
+
4
+ module RedisOrm
5
+ module Generators
6
+ class ModelGenerator < ::Rails::Generators::NamedBase
7
+ source_root File.expand_path('../templates', __FILE__)
8
+
9
+ desc "Creates a RedisOrm model"
10
+ argument :attributes, type: :array, default: [], banner: "field:type field:type"
11
+
12
+ check_class_collision
13
+
14
+ def create_model_file
15
+ template "model.rb.erb", File.join('app/models', class_path, "#{file_name}.rb")
16
+ end
17
+
18
+ hook_for :test_framework
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,5 @@
1
+ class <%= class_name %> < RedisOrm::Base
2
+ <% attributes.each do |attr| %>
3
+ property :<%= attr.name %>, <%= attr.type.to_s.camelcase %>
4
+ <% end -%>
5
+ end
@@ -7,7 +7,7 @@ module RedisOrm
7
7
  def has_many(foreign_models, options = {})
8
8
  class_associations = class_variable_get(:"@@associations")
9
9
  class_associations[model_name] << {:type => :has_many, :foreign_models => foreign_models, :options => options}
10
-
10
+
11
11
  foreign_models_name = options[:as] ? options[:as].to_sym : foreign_models.to_sym
12
12
 
13
13
  define_method foreign_models_name.to_sym do
@@ -41,8 +41,10 @@ module RedisOrm
41
41
 
42
42
  records.to_a.each do |record|
43
43
  # we use here *foreign_models_name* not *record.model_name.pluralize* because of the :as option
44
- $redis.zadd("#{model_name}:#{id}:#{foreign_models_name}", Time.now.to_f, record.id)
45
-
44
+ key = "#{model_name}:#{id}:#{foreign_models_name}"
45
+ $redis.zadd(key, Time.now.to_f, record.id)
46
+ set_expire_on_reference_key(key)
47
+
46
48
  record.get_indices.each do |index|
47
49
  save_index_for_associated_record(index, record, [model_name, id, record.model_name.pluralize]) # record.model_name.pluralize => foreign_models_name
48
50
  end
@@ -54,17 +56,20 @@ module RedisOrm
54
56
  assoc_foreign_models_name = assoc[:options][:as] ? assoc[:options][:as] : model_name.pluralize
55
57
  key = "#{record.model_name}:#{record.id}:#{assoc_foreign_models_name}"
56
58
  $redis.zadd(key, Time.now.to_f, id) if !$redis.zrank(key, id)
59
+ set_expire_on_reference_key(key)
57
60
  end
58
61
 
59
62
  # check whether *record* object has *has_one* declaration and it states *self.model_name*
60
63
  if assoc = record.get_associations.detect{|h| [:has_one, :belongs_to].include?(h[:type]) && h[:foreign_model] == model_name.to_sym}
61
64
  foreign_model_name = assoc[:options][:as] ? assoc[:options][:as] : model_name
65
+ key = "#{record.model_name}:#{record.id}:#{foreign_model_name}"
62
66
  # overwrite assoc anyway so we don't need to check record.send(model_name.to_sym).nil? here
63
- $redis.set("#{record.model_name}:#{record.id}:#{foreign_model_name}", id)
67
+ $redis.set(key, id)
68
+ set_expire_on_reference_key(key)
64
69
  end
65
70
  end
66
71
  end
67
72
  end
68
73
  end
69
74
  end
70
- end
75
+ end
@@ -1,7 +1,7 @@
1
1
  module RedisOrm
2
2
  module Associations
3
3
  module HasManyHelper
4
- private
4
+ private
5
5
  def save_index_for_associated_record(index, record, inception)
6
6
  prepared_index = if index[:name].is_a?(Array) # TODO sort alphabetically
7
7
  index[:name].inject(inception) do |sum, index_part|
@@ -3,15 +3,19 @@ module RedisOrm
3
3
  class HasManyProxy
4
4
  include HasManyHelper
5
5
 
6
- def initialize(reciever_model_name, reciever_id, foreign_models, options)
6
+ def initialize(receiver_model_name, reciever_id, foreign_models, options)
7
7
  @records = [] #records.to_a
8
- @reciever_model_name = reciever_model_name
8
+ @reciever_model_name = receiver_model_name
9
9
  @reciever_id = reciever_id
10
10
  @foreign_models = foreign_models
11
11
  @options = options
12
12
  @fetched = false
13
13
  end
14
14
 
15
+ def receiver_instance
16
+ @receiver_instance ||= @reciever_model_name.camelize.constantize.find(@reciever_id)
17
+ end
18
+
15
19
  def fetch
16
20
  @records = @foreign_models.to_s.singularize.camelize.constantize.find($redis.zrevrangebyscore __key__, Time.now.to_f, 0)
17
21
  @fetched = true
@@ -31,8 +35,10 @@ module RedisOrm
31
35
  # user.avatars << Avatar.find(23) => user:1:avatars => [23]
32
36
  def <<(new_records)
33
37
  new_records.to_a.each do |record|
34
- $redis.zadd(__key__, Time.now.to_f, record.id)
35
-
38
+ $redis.zadd(__key__, Time.now.to_f, record.id)
39
+
40
+ receiver_instance.set_expire_on_reference_key(__key__)
41
+
36
42
  record.get_indices.each do |index|
37
43
  save_index_for_associated_record(index, record, [@reciever_model_name, @reciever_id, record.model_name.pluralize]) # record.model_name.pluralize => @foreign_models
38
44
  end
@@ -50,8 +56,11 @@ module RedisOrm
50
56
  @reciever_model_name.pluralize
51
57
  end
52
58
 
53
- if !$redis.zrank("#{record.model_name}:#{record.id}:#{pluralized_reciever_model_name}", @reciever_id)
54
- $redis.zadd("#{record.model_name}:#{record.id}:#{pluralized_reciever_model_name}", Time.now.to_f, @reciever_id)
59
+ reference_key = "#{record.model_name}:#{record.id}:#{pluralized_reciever_model_name}"
60
+
61
+ if !$redis.zrank(reference_key, @reciever_id)
62
+ $redis.zadd(reference_key, Time.now.to_f, @reciever_id)
63
+ receiver_instance.set_expire_on_reference_key(reference_key)
55
64
  end
56
65
  # check whether *record* object has *has_one* declaration and TODO it states *self.model_name* and there is no record yet from the *record*'s side (in order not to provoke recursion)
57
66
  elsif has_one_assoc = record_associations.detect{|h| [:has_one, :belongs_to].include?(h[:type]) && h[:foreign_model] == @reciever_model_name.to_sym}
@@ -61,7 +70,9 @@ module RedisOrm
61
70
  @reciever_model_name
62
71
  end
63
72
  if record.send(reciever_model_name).nil?
64
- $redis.set("#{record.model_name}:#{record.id}:#{reciever_model_name}", @reciever_id)
73
+ key = "#{record.model_name}:#{record.id}:#{reciever_model_name}"
74
+ $redis.set(key, @reciever_id)
75
+ receiver_instance.set_expire_on_reference_key(key)
65
76
  end
66
77
  end
67
78
  end
@@ -29,10 +29,12 @@ module RedisOrm
29
29
  # we need to store this to clear old associations later
30
30
  old_assoc = self.send(foreign_model_name)
31
31
 
32
+ reference_key = "#{model_name}:#{id}:#{foreign_model_name}"
32
33
  if assoc_with_record.nil?
33
- $redis.del("#{model_name}:#{id}:#{foreign_model_name}")
34
+ $redis.del(reference_key)
34
35
  elsif assoc_with_record.model_name == foreign_model.to_s
35
- $redis.set("#{model_name}:#{id}:#{foreign_model_name}", assoc_with_record.id)
36
+ $redis.set(reference_key, assoc_with_record.id)
37
+ set_expire_on_reference_key(reference_key)
36
38
  else
37
39
  raise TypeMismatchError
38
40
  end
@@ -5,6 +5,14 @@ require 'active_support/inflector/transliterate'
5
5
  require 'active_support/inflector/methods'
6
6
  require 'active_support/inflections'
7
7
  require 'active_support/core_ext/string/inflections'
8
+
9
+ require 'active_support/core_ext/time/acts_like'
10
+ require 'active_support/core_ext/time/calculations'
11
+ require 'active_support/core_ext/time/conversions'
12
+ require 'active_support/core_ext/time/marshal'
13
+ require 'active_support/core_ext/time/zones'
14
+
15
+ require 'active_support/core_ext/numeric'
8
16
  require 'active_support/core_ext/time/calculations' # local_time for to_time(:local)
9
17
  require 'active_support/core_ext/string/conversions' # to_time
10
18
 
@@ -47,7 +55,8 @@ module RedisOrm
47
55
  @@callbacks = Hash.new{|h,k| h[k] = {}}
48
56
  @@use_uuid_as_id = {}
49
57
  @@descendants = []
50
-
58
+ @@expire = Hash.new{|h,k| h[k] = {}}
59
+
51
60
  class << self
52
61
 
53
62
  def inherited(from)
@@ -76,21 +85,18 @@ module RedisOrm
76
85
  value = instance_variable_get(:"@#{property_name}")
77
86
 
78
87
  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
79
-
80
- if Time == class_name
81
- value = begin
82
- value.to_s.to_time(:local)
83
- rescue ArgumentError => e
84
- nil
85
- end
88
+ if /DateTime|Time/ =~ class_name.to_s
89
+ # we're using to_datetime here because to_time doesn't manage timezone correctly
90
+ value.to_s.to_datetime rescue nil
86
91
  elsif Integer == class_name
87
- value = value.to_i
92
+ value.to_i
88
93
  elsif Float == class_name
89
- value = value.to_f
94
+ value.to_f
90
95
  elsif RedisOrm::Boolean == class_name
91
- value = ((value == "false" || value == false) ? false : true)
96
+ ((value == "false" || value == false) ? false : true)
97
+ else
98
+ value
92
99
  end
93
- value
94
100
  end
95
101
 
96
102
  send(:define_method, "#{property_name}=".to_sym) do |value|
@@ -102,7 +108,6 @@ module RedisOrm
102
108
  else
103
109
  instance_variable_set(:"@#{property_name}_changes", [value])
104
110
  end
105
-
106
111
  instance_variable_set(:"@#{property_name}", value)
107
112
  end
108
113
 
@@ -127,6 +132,10 @@ module RedisOrm
127
132
  end
128
133
  end
129
134
 
135
+ def expire(seconds, options = {})
136
+ @@expire[model_name] = {:seconds => seconds, :options => options}
137
+ end
138
+
130
139
  def use_uuid_as_id
131
140
  @@use_uuid_as_id[model_name] = true
132
141
  @@uuid = UUID.new
@@ -251,7 +260,7 @@ module RedisOrm
251
260
  ids_key = prepared_index
252
261
  'asc'
253
262
  end
254
-
263
+
255
264
  if order_by_property_is_string
256
265
  if direction.to_s == 'desc'
257
266
  ids_length = $redis.llen(ids_key)
@@ -367,6 +376,8 @@ module RedisOrm
367
376
  obj.send("#{k}=", v)
368
377
  end
369
378
  end
379
+
380
+ $redis.expire(obj.__redis_record_key, options[:expire_in].to_i) if !options[:expire_in].blank?
370
381
 
371
382
  obj
372
383
  end
@@ -422,7 +433,27 @@ module RedisOrm
422
433
  def to_a
423
434
  [self]
424
435
  end
425
-
436
+
437
+ def __redis_record_key
438
+ "#{model_name}:#{id}"
439
+ end
440
+
441
+ def set_expire_on_reference_key(key)
442
+ class_expire = @@expire[model_name]
443
+
444
+ # if class method *expire* was invoked and number of seconds was specified then set expiry date on the HSET record key
445
+ if class_expire[:seconds]
446
+ set_expire = true
447
+
448
+ if class_expire[:options][:if] && class_expire[:options][:if].class == Proc
449
+ # *self* here refers to the instance of class which has_one association
450
+ set_expire = class_expire[:options][:if][self] # invoking specified *:if* Proc with current record as *self*
451
+ end
452
+
453
+ $redis.expire(key, class_expire[:seconds].to_i) if set_expire
454
+ end
455
+ end
456
+
426
457
  # is called from RedisOrm::Associations::HasMany to save backlinks to saved records
427
458
  def get_associations
428
459
  @@associations[self.model_name]
@@ -450,14 +481,18 @@ module RedisOrm
450
481
  end
451
482
  end
452
483
 
484
+ # cast all attributes' keys to symbols
485
+ attributes = attributes.inject({}){|sum, el| sum.merge({el[0].to_sym => el[1]})} if attributes.is_a?(Hash)
486
+
453
487
  # get all names of properties to assign only those attributes from attributes hash whose key are in prop_names
454
488
  # we're not using *self.respond_to?("#{key}=".to_sym)* since *belongs_to* and other assocs could create their own methods
455
489
  # with *key=* name, that in turn will mess up indices
456
- prop_names = @@properties[model_name].collect{|m| m[:name]}
457
-
458
490
  if attributes.is_a?(Hash) && !attributes.empty?
459
- attributes.each do |key, value|
460
- self.send("#{key}=".to_sym, value) if prop_names.include?(key.to_sym)
491
+ @@properties[model_name].each do |property|
492
+ if !(value = attributes[property[:name]]).nil? # check for nil because we want to pass falses too (and value could be 'false')
493
+ value = Marshal.load(value) if ["Array", "Hash"].include?(property[:class]) && value.is_a?(String)
494
+ self.send("#{property[:name]}=".to_sym, value)
495
+ end
461
496
  end
462
497
  end
463
498
  self
@@ -502,104 +537,20 @@ module RedisOrm
502
537
  else
503
538
  $redis.incr("#{model_name}:id")
504
539
  end
505
- end
540
+ end
506
541
 
507
542
  def save
508
543
  return false if !valid?
509
544
 
510
- # store here initial persisted flag so we could invoke :after_create callbacks in the end of the function
545
+ _check_mismatched_types_for_values
546
+
547
+ # store here initial persisted flag so we could invoke :after_create callbacks in the end of *save* function
511
548
  was_persisted = persisted?
512
549
 
513
550
  if persisted? # then there might be old indices
514
- # check whether there's old indices exists and if yes - delete them
515
- @@properties[model_name].each do |prop|
516
- # if there were no changes for current property skip it (indices remains the same)
517
- next if ! self.send(:"#{prop[:name]}_changed?")
518
-
519
- prev_prop_value = instance_variable_get(:"@#{prop[:name]}_changes").first
520
- prop_value = instance_variable_get(:"@#{prop[:name]}")
521
- # TODO DRY in destroy also
522
- if prop[:options][:sortable]
523
- if prop[:class].eql?("String")
524
- $redis.lrem "#{model_name}:#{prop[:name]}_ids", 1, "#{prev_prop_value}:#{@id}"
525
- # remove id from every indexed property
526
- @@indices[model_name].each do |index|
527
- $redis.lrem "#{construct_prepared_index(index)}:#{prop[:name]}_ids", 1, "#{prop_value}:#{@id}"
528
- end
529
- else
530
- $redis.zrem "#{model_name}:#{prop[:name]}_ids", @id
531
- # remove id from every indexed property
532
- @@indices[model_name].each do |index|
533
- $redis.zrem "#{construct_prepared_index(index)}:#{prop[:name]}_ids", @id
534
- end
535
- end
536
- end
537
-
538
- indices = @@indices[model_name].inject([]) do |sum, models_index|
539
- if models_index[:name].is_a?(Array)
540
- if models_index[:name].include?(prop[:name])
541
- sum << models_index
542
- else
543
- sum
544
- end
545
- else
546
- if models_index[:name] == prop[:name]
547
- sum << models_index
548
- else
549
- sum
550
- end
551
- end
552
- end
553
-
554
- if !indices.empty?
555
- indices.each do |index|
556
- if index[:name].is_a?(Array)
557
- keys_to_delete = if index[:name].index(prop) == 0
558
- $redis.keys "#{model_name}:#{prop[:name]}#{prev_prop_value}*"
559
- else
560
- $redis.keys "#{model_name}:*#{prop[:name]}:#{prev_prop_value}*"
561
- end
562
-
563
- keys_to_delete.each{|key| $redis.del(key)}
564
- else
565
- key_to_delete = "#{model_name}:#{prop[:name]}:#{prev_prop_value}"
566
- $redis.del key_to_delete
567
- end
568
-
569
- # also we need to delete associated records *indices*
570
- if !@@associations[model_name].empty?
571
- @@associations[model_name].each do |assoc|
572
- if :belongs_to == assoc[:type]
573
- # if association has :as option use it, otherwise use standard :foreign_model
574
- foreign_model_name = assoc[:options][:as] ? assoc[:options][:as].to_sym : assoc[:foreign_model].to_sym
575
- if !self.send(foreign_model_name).nil?
576
- if index[:name].is_a?(Array)
577
- keys_to_delete = if index[:name].index(prop) == 0
578
- $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}#{prev_prop_value}*"
579
- else
580
- $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:*#{prop[:name]}:#{prev_prop_value}*"
581
- end
582
-
583
- keys_to_delete.each{|key| $redis.del(key)}
584
- else
585
- beginning_of_the_key = "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}:"
586
-
587
- $redis.del(beginning_of_the_key + prev_prop_value.to_s)
588
-
589
- 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)
590
- end
591
- end
592
- end
593
- end
594
- end # deleting associated records *indices*
595
-
596
- end
597
- end
598
- end
551
+ _check_indices_for_persisted # remove old indices if needed
599
552
  else # !persisted?
600
- @@callbacks[model_name][:before_create].each do |callback|
601
- self.send(callback)
602
- end
553
+ @@callbacks[model_name][:before_create].each{ |callback| self.send(callback) }
603
554
 
604
555
  @id = get_next_id
605
556
  $redis.zadd "#{model_name}:ids", Time.now.to_f, @id
@@ -607,103 +558,21 @@ module RedisOrm
607
558
  self.created_at = Time.now if respond_to? :created_at
608
559
  end
609
560
 
610
- @@callbacks[model_name][:before_save].each do |callback|
611
- self.send(callback)
612
- end
561
+ @@callbacks[model_name][:before_save].each{ |callback| self.send(callback) }
613
562
 
614
563
  # automatically update *modified_at* property if it was defined
615
564
  self.modified_at = Time.now if respond_to? :modified_at
616
565
 
617
- @@properties[model_name].each do |prop|
618
- prop_value = self.send(prop[:name].to_sym)
619
-
620
- if prop_value.nil? && !prop[:options][:default].nil?
621
- prop_value = prop[:options][:default]
622
-
623
- # cast prop_value to proper class if they are not in it
624
- # for example 'property :wage, Float, :sortable => true, :default => 20_000' turn 20_000 to 20_000.0
625
- if prop[:class] != prop_value.class.to_s
626
- prop_value = case prop[:class]
627
- when 'Time'
628
- begin
629
- value.to_s.to_time(:local)
630
- rescue ArgumentError => e
631
- nil
632
- end
633
- when 'Integer'
634
- prop_value.to_i
635
- when 'Float'
636
- prop_value.to_f
637
- when 'RedisOrm::Boolean'
638
- (prop_value == "false" || prop_value == false) ? false : true
639
- end
640
- end
566
+ _save_to_redis # main work done here
567
+ _save_new_indices
641
568
 
642
- # set instance variable in order to properly save indexes here
643
- self.instance_variable_set(:"@#{prop[:name]}", prop_value)
644
- instance_variable_set :"@#{prop[:name]}_changes", [prop_value]
645
- end
646
-
647
- $redis.hset("#{model_name}:#{id}", prop[:name].to_s, prop_value)
648
-
649
- # reducing @#{prop[:name]}_changes array to the last value
650
- prop_changes = instance_variable_get :"@#{prop[:name]}_changes"
651
-
652
- if prop_changes && prop_changes.size > 2
653
- instance_variable_set :"@#{prop[:name]}_changes", [prop_changes.last]
654
- end
655
-
656
- # if some property need to be sortable add id of the record to the appropriate sorted set
657
- if prop[:options][:sortable]
658
- property_value = instance_variable_get(:"@#{prop[:name]}").to_s
659
- if prop[:class].eql?("String")
660
- sortable_key = "#{model_name}:#{prop[:name]}_ids"
661
- el_or_position_to_insert = find_position_to_insert(sortable_key, property_value)
662
- 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}")
663
- # add to every indexed property
664
- @@indices[model_name].each do |index|
665
- sortable_key = "#{construct_prepared_index(index)}:#{prop[:name]}_ids"
666
- 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}")
667
- end
668
- else
669
- score = case prop[:class]
670
- when "Integer"; property_value.to_f
671
- when "Float"; property_value.to_f
672
- when "RedisOrm::Boolean"; (property_value == true ? 1.0 : 0.0)
673
- when "Time"; property_value.to_f
674
- end
675
- $redis.zadd "#{model_name}:#{prop[:name]}_ids", score, @id
676
- # add to every indexed property
677
- @@indices[model_name].each do |index|
678
- $redis.zadd "#{construct_prepared_index(index)}:#{prop[:name]}_ids", score, @id
679
- end
680
- end
681
- end
682
- end
683
-
684
- # save new indices (not *reference* onces (for example not these *belongs_to :note, :index => true*)) in order to sort by finders
685
- # city:name:Chicago => 1
686
- @@indices[model_name].reject{|index| index[:options][:reference]}.each do |index|
687
- prepared_index = construct_prepared_index(index) # instance method not class one!
688
-
689
- if index[:options][:unique]
690
- $redis.set(prepared_index, @id)
691
- else
692
- $redis.zadd(prepared_index, Time.now.to_f, @id)
693
- end
694
- end
695
-
696
- @@callbacks[model_name][:after_save].each do |callback|
697
- self.send(callback)
698
- end
569
+ @@callbacks[model_name][:after_save].each{ |callback| self.send(callback) }
699
570
 
700
571
  if ! was_persisted
701
- @@callbacks[model_name][:after_create].each do |callback|
702
- self.send(callback)
703
- end
572
+ @@callbacks[model_name][:after_create].each{ |callback| self.send(callback) }
704
573
  end
705
574
 
706
- true # if there were no errors just return true, so *if* conditions would work
575
+ true # if there were no errors just return true, so *if obj.save* conditions would work
707
576
  end
708
577
 
709
578
  def find_position_to_insert(sortable_key, value)
@@ -875,7 +744,7 @@ module RedisOrm
875
744
 
876
745
  # remove all associated indices
877
746
  @@indices[model_name].each do |index|
878
- prepared_index = construct_prepared_index(index) # instance method not class one!
747
+ prepared_index = _construct_prepared_index(index) # instance method not class one!
879
748
 
880
749
  if index[:options][:unique]
881
750
  $redis.del(prepared_index)
@@ -891,19 +760,208 @@ module RedisOrm
891
760
  true # if there were no errors just return true, so *if* conditions would work
892
761
  end
893
762
 
894
- protected
895
- def construct_prepared_index(index)
896
- prepared_index = if index[:name].is_a?(Array) # TODO sort alphabetically
897
- index[:name].inject([model_name]) do |sum, index_part|
898
- sum += [index_part, self.instance_variable_get(:"@#{index_part}")]
899
- end.join(':')
900
- else
901
- [model_name, index[:name], self.instance_variable_get(:"@#{index[:name]}")].join(':')
763
+ protected
764
+ def _construct_prepared_index(index)
765
+ prepared_index = if index[:name].is_a?(Array) # TODO sort alphabetically
766
+ index[:name].inject([model_name]) do |sum, index_part|
767
+ sum += [index_part, self.instance_variable_get(:"@#{index_part}")]
768
+ end.join(':')
769
+ else
770
+ [model_name, index[:name], self.instance_variable_get(:"@#{index[:name]}")].join(':')
771
+ end
772
+
773
+ prepared_index.downcase! if index[:options][:case_insensitive]
774
+
775
+ prepared_index
776
+ end
777
+
778
+ def _check_mismatched_types_for_values
779
+ # an exception should be raised before all saving procedures if wrong value type is specified (especcially true for Arrays and Hashes)
780
+ @@properties[model_name].each do |prop|
781
+ prop_value = self.send(prop[:name].to_sym)
782
+
783
+ if prop_value && prop[:class] != prop_value.class.to_s && ['Array', 'Hash'].include?(prop[:class].to_s)
784
+ raise TypeMismatchError
902
785
  end
786
+ end
787
+ end
788
+
789
+ def _check_indices_for_persisted
790
+ # check whether there's old indices exists and if yes - delete them
791
+ @@properties[model_name].each do |prop|
792
+ # if there were no changes for current property skip it (indices remains the same)
793
+ next if ! self.send(:"#{prop[:name]}_changed?")
903
794
 
904
- prepared_index.downcase! if index[:options][:case_insensitive]
795
+ prev_prop_value = instance_variable_get(:"@#{prop[:name]}_changes").first
796
+ prop_value = instance_variable_get(:"@#{prop[:name]}")
797
+ # TODO DRY in destroy also
798
+ if prop[:options][:sortable]
799
+ if prop[:class].eql?("String")
800
+ $redis.lrem "#{model_name}:#{prop[:name]}_ids", 1, "#{prev_prop_value}:#{@id}"
801
+ # remove id from every indexed property
802
+ @@indices[model_name].each do |index|
803
+ $redis.lrem "#{_construct_prepared_index(index)}:#{prop[:name]}_ids", 1, "#{prop_value}:#{@id}"
804
+ end
805
+ else
806
+ $redis.zrem "#{model_name}:#{prop[:name]}_ids", @id
807
+ # remove id from every indexed property
808
+ @@indices[model_name].each do |index|
809
+ $redis.zrem "#{_construct_prepared_index(index)}:#{prop[:name]}_ids", @id
810
+ end
811
+ end
812
+ end
813
+
814
+ indices = @@indices[model_name].inject([]) do |sum, models_index|
815
+ if models_index[:name].is_a?(Array)
816
+ if models_index[:name].include?(prop[:name])
817
+ sum << models_index
818
+ else
819
+ sum
820
+ end
821
+ else
822
+ if models_index[:name] == prop[:name]
823
+ sum << models_index
824
+ else
825
+ sum
826
+ end
827
+ end
828
+ end
829
+
830
+ if !indices.empty?
831
+ indices.each do |index|
832
+ if index[:name].is_a?(Array)
833
+ keys_to_delete = if index[:name].index(prop) == 0
834
+ $redis.keys "#{model_name}:#{prop[:name]}#{prev_prop_value}*"
835
+ else
836
+ $redis.keys "#{model_name}:*#{prop[:name]}:#{prev_prop_value}*"
837
+ end
838
+
839
+ keys_to_delete.each{|key| $redis.del(key)}
840
+ else
841
+ key_to_delete = "#{model_name}:#{prop[:name]}:#{prev_prop_value}"
842
+ $redis.del key_to_delete
843
+ end
844
+
845
+ # also we need to delete associated records *indices*
846
+ if !@@associations[model_name].empty?
847
+ @@associations[model_name].each do |assoc|
848
+ if :belongs_to == assoc[:type]
849
+ # if association has :as option use it, otherwise use standard :foreign_model
850
+ foreign_model_name = assoc[:options][:as] ? assoc[:options][:as].to_sym : assoc[:foreign_model].to_sym
851
+ if !self.send(foreign_model_name).nil?
852
+ if index[:name].is_a?(Array)
853
+ keys_to_delete = if index[:name].index(prop) == 0
854
+ $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}#{prev_prop_value}*"
855
+ else
856
+ $redis.keys "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:*#{prop[:name]}:#{prev_prop_value}*"
857
+ end
858
+
859
+ keys_to_delete.each{|key| $redis.del(key)}
860
+ else
861
+ beginning_of_the_key = "#{assoc[:foreign_model]}:#{self.send(assoc[:foreign_model]).id}:#{model_name.to_s.pluralize}:#{prop[:name]}:"
862
+
863
+ $redis.del(beginning_of_the_key + prev_prop_value.to_s)
864
+
865
+ 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)
866
+ end
867
+ end
868
+ end
869
+ end
870
+ end # deleting associated records *indices*
871
+ end
872
+ end
873
+ end
874
+ end
875
+
876
+ def _save_to_redis
877
+ @@properties[model_name].each do |prop|
878
+ prop_value = self.send(prop[:name].to_sym)
905
879
 
906
- prepared_index
880
+ if prop_value.nil? && !prop[:options][:default].nil?
881
+ prop_value = prop[:options][:default]
882
+
883
+ # cast prop_value to proper class if they are not in it
884
+ # for example 'property :wage, Float, :sortable => true, :default => 20_000' turn 20_000 to 20_000.0
885
+ if prop[:class] != prop_value.class.to_s
886
+ prop_value = case prop[:class]
887
+ when 'Time'
888
+ begin
889
+ value.to_s.to_time(:local)
890
+ rescue ArgumentError => e
891
+ nil
892
+ end
893
+ when 'Integer'
894
+ prop_value.to_i
895
+ when 'Float'
896
+ prop_value.to_f
897
+ when 'RedisOrm::Boolean'
898
+ (prop_value == "false" || prop_value == false) ? false : true
899
+ end
900
+ end
901
+
902
+ # set instance variable in order to properly save indexes here
903
+ self.instance_variable_set(:"@#{prop[:name]}", prop_value)
904
+ instance_variable_set :"@#{prop[:name]}_changes", [prop_value]
905
+ end
906
+
907
+ # serialize array- and hash-type properties
908
+ if ['Array', 'Hash'].include?(prop[:class]) && !prop_value.is_a?(String)
909
+ prop_value = Marshal.dump(prop_value)
910
+ end
911
+
912
+ #TODO put out of loop
913
+ $redis.hset(__redis_record_key, prop[:name].to_s, prop_value)
914
+
915
+ set_expire_on_reference_key(__redis_record_key)
916
+
917
+ # reducing @#{prop[:name]}_changes array to the last value
918
+ prop_changes = instance_variable_get :"@#{prop[:name]}_changes"
919
+
920
+ if prop_changes && prop_changes.size > 2
921
+ instance_variable_set :"@#{prop[:name]}_changes", [prop_changes.last]
922
+ end
923
+
924
+ # if some property need to be sortable add id of the record to the appropriate sorted set
925
+ if prop[:options][:sortable]
926
+ property_value = instance_variable_get(:"@#{prop[:name]}").to_s
927
+ if prop[:class].eql?("String")
928
+ sortable_key = "#{model_name}:#{prop[:name]}_ids"
929
+ el_or_position_to_insert = find_position_to_insert(sortable_key, property_value)
930
+ 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}")
931
+ # add to every indexed property
932
+ @@indices[model_name].each do |index|
933
+ sortable_key = "#{_construct_prepared_index(index)}:#{prop[:name]}_ids"
934
+ 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}")
935
+ end
936
+ else
937
+ score = case prop[:class]
938
+ when "Integer"; property_value.to_f
939
+ when "Float"; property_value.to_f
940
+ when "RedisOrm::Boolean"; (property_value == true ? 1.0 : 0.0)
941
+ when "Time"; property_value.to_f
942
+ end
943
+ $redis.zadd "#{model_name}:#{prop[:name]}_ids", score, @id
944
+ # add to every indexed property
945
+ @@indices[model_name].each do |index|
946
+ $redis.zadd "#{_construct_prepared_index(index)}:#{prop[:name]}_ids", score, @id
947
+ end
948
+ end
949
+ end
907
950
  end
951
+ end
952
+
953
+ def _save_new_indices
954
+ # save new indices (not *reference* onces (for example not these *belongs_to :note, :index => true*)) in order to sort by finders
955
+ # city:name:Chicago => 1
956
+ @@indices[model_name].reject{|index| index[:options][:reference]}.each do |index|
957
+ prepared_index = _construct_prepared_index(index) # instance method not class one!
958
+
959
+ if index[:options][:unique]
960
+ $redis.set(prepared_index, @id)
961
+ else
962
+ $redis.zadd(prepared_index, Time.now.to_f, @id)
963
+ end
964
+ end
965
+ end
908
966
  end
909
967
  end