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 +15 -7
- data/Gemfile +7 -5
- data/Manifest +8 -0
- data/README.md +33 -1
- data/Rakefile +3 -3
- data/lib/rails/generators/redis_orm/model/model_generator.rb +21 -0
- data/lib/rails/generators/redis_orm/model/templates/model.rb.erb +5 -0
- data/lib/redis_orm/associations/has_many.rb +10 -5
- data/lib/redis_orm/associations/has_many_helper.rb +1 -1
- data/lib/redis_orm/associations/has_many_proxy.rb +18 -7
- data/lib/redis_orm/associations/has_one.rb +4 -2
- data/lib/redis_orm/redis_orm.rb +266 -208
- data/redis_orm.gemspec +15 -15
- data/spec/generators/model_generator_spec.rb +29 -0
- data/spec/spec_helper.rb +17 -0
- data/test/basic_functionality_test.rb +39 -8
- data/test/classes/article_with_comments.rb +8 -0
- data/test/classes/expire_user.rb +8 -0
- data/test/classes/expire_user_with_predicate.rb +13 -0
- data/test/classes/profile.rb +1 -0
- data/test/exceptions_test.rb +12 -0
- data/test/expire_records_test.rb +64 -0
- data/test/options_test.rb +2 -2
- data/test/redis.conf +8 -8
- data/test/test_helper.rb +3 -0
- data/test/uuid_as_id_test.rb +4 -4
- metadata +60 -24
data/CHANGELOG
CHANGED
@@ -1,10 +1,18 @@
|
|
1
|
-
v0.
|
2
|
-
|
3
|
-
*
|
4
|
-
*
|
5
|
-
*
|
6
|
-
|
7
|
-
* fixed bug
|
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
|
-
|
4
|
-
gem 'activesupport', '>= 3.0.0'
|
5
|
-
gem 'redis', '>= 2.2.0'
|
6
|
-
gem 'uuid', '>= 2.3.2'
|
3
|
+
gemspec
|
7
4
|
|
8
|
-
|
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.
|
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
|
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
|
@@ -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
|
-
|
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(
|
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(
|
6
|
+
def initialize(receiver_model_name, reciever_id, foreign_models, options)
|
7
7
|
@records = [] #records.to_a
|
8
|
-
@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
|
-
|
54
|
-
|
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
|
-
|
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(
|
34
|
+
$redis.del(reference_key)
|
34
35
|
elsif assoc_with_record.model_name == foreign_model.to_s
|
35
|
-
$redis.set(
|
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
|
data/lib/redis_orm/redis_orm.rb
CHANGED
@@ -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
|
-
|
81
|
-
value
|
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
|
92
|
+
value.to_i
|
88
93
|
elsif Float == class_name
|
89
|
-
value
|
94
|
+
value.to_f
|
90
95
|
elsif RedisOrm::Boolean == class_name
|
91
|
-
|
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
|
-
|
460
|
-
|
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
|
-
|
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
|
-
#
|
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
|
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
|
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
|
-
|
618
|
-
|
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
|
-
|
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
|
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 =
|
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
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
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
|
-
|
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
|
-
|
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
|