redis_orm 0.6.2 → 0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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