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 +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
|