redis_orm 0.1
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/LICENSE +19 -0
- data/Manifest +23 -0
- data/README.md +332 -0
- data/Rakefile +32 -0
- data/lib/redis_orm.rb +20 -0
- data/lib/redis_orm/active_model_behavior.rb +20 -0
- data/lib/redis_orm/associations/belongs_to.rb +48 -0
- data/lib/redis_orm/associations/has_many.rb +54 -0
- data/lib/redis_orm/associations/has_many_proxy.rb +118 -0
- data/lib/redis_orm/associations/has_one.rb +52 -0
- data/lib/redis_orm/redis_orm.rb +502 -0
- data/redis_orm.gemspec +42 -0
- data/test/associations_test.rb +226 -0
- data/test/basic_functionality_test.rb +166 -0
- data/test/callbacks_test.rb +140 -0
- data/test/changes_array_test.rb +47 -0
- data/test/dynamic_finders_test.rb +89 -0
- data/test/exceptions_test.rb +63 -0
- data/test/has_one_has_many_test.rb +75 -0
- data/test/indices_test.rb +76 -0
- data/test/options_test.rb +186 -0
- data/test/redis.conf +417 -0
- data/test/validations_test.rb +49 -0
- metadata +138 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
module RedisOrm
|
2
|
+
module Associations
|
3
|
+
module BelongsTo
|
4
|
+
# class Avatar < RedisOrm::Base
|
5
|
+
# belongs_to :user
|
6
|
+
# end
|
7
|
+
#
|
8
|
+
# class User < RedisOrm::Base
|
9
|
+
# has_many :avatars
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# avatar.user => avatar:234:user => 1 => User.find(1)
|
13
|
+
def belongs_to(foreign_model, options = {})
|
14
|
+
class_associations = class_variable_get(:"@@associations")
|
15
|
+
class_variable_get(:"@@associations")[model_name] << {:type => :belongs_to, :foreign_model => foreign_model, :options => options}
|
16
|
+
|
17
|
+
foreign_model_name = if options[:as]
|
18
|
+
options[:as].to_sym
|
19
|
+
else
|
20
|
+
foreign_model.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method foreign_model_name.to_sym do
|
24
|
+
foreign_model.to_s.camelize.constantize.find($redis.get "#{model_name}:#{@id}:#{foreign_model_name}")
|
25
|
+
end
|
26
|
+
|
27
|
+
# look = Look.create :title => 'test'
|
28
|
+
# look.user = User.find(1) => look:23:user => 1
|
29
|
+
define_method "#{foreign_model_name}=" do |assoc_with_record|
|
30
|
+
if assoc_with_record.model_name == foreign_model.to_s
|
31
|
+
$redis.set("#{model_name}:#{id}:#{foreign_model_name}", assoc_with_record.id)
|
32
|
+
else
|
33
|
+
raise TypeMismatchError
|
34
|
+
end
|
35
|
+
|
36
|
+
# check whether *assoc_with_record* object has *has_many* declaration and TODO it states *self.model_name* in plural and there is no record yet from the *assoc_with_record*'s side (in order not to provoke recursion)
|
37
|
+
if class_associations[assoc_with_record.model_name].detect{|h| h[:type] == :has_many && h[:foreign_models] == model_name.pluralize.to_sym} && !$redis.zrank("#{assoc_with_record.model_name}:#{assoc_with_record.id}:#{model_name.pluralize}", self.id)
|
38
|
+
assoc_with_record.send(model_name.pluralize.to_sym).send(:"<<", self)
|
39
|
+
|
40
|
+
# check whether *assoc_with_record* object has *has_one* declaration and TODO it states *self.model_name* and there is no record yet from the *assoc_with_record*'s side (in order not to provoke recursion)
|
41
|
+
elsif class_associations[assoc_with_record.model_name].detect{|h| h[:type] == :has_one && h[:foreign_model] == model_name.to_sym} && assoc_with_record.send(model_name.to_sym).nil?
|
42
|
+
assoc_with_record.send("#{model_name}=", self)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module RedisOrm
|
2
|
+
module Associations
|
3
|
+
module HasMany
|
4
|
+
# user.avatars => user:1:avatars => [1, 22, 234] => Avatar.find([1, 22, 234])
|
5
|
+
# options
|
6
|
+
# *:dependant* key: either *destroy* or *nullify* (default)
|
7
|
+
def has_many(foreign_models, options = {})
|
8
|
+
class_associations = class_variable_get(:"@@associations")
|
9
|
+
class_associations[model_name] << {:type => :has_many, :foreign_models => foreign_models, :options => options}
|
10
|
+
|
11
|
+
foreign_models_name = if options[:as]
|
12
|
+
options[:as].to_sym
|
13
|
+
else
|
14
|
+
foreign_models.to_sym
|
15
|
+
end
|
16
|
+
|
17
|
+
define_method foreign_models_name.to_sym do
|
18
|
+
Associations::HasManyProxy.new(model_name, id, foreign_models, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
# user = User.find(1)
|
22
|
+
# user.avatars = Avatar.find(23) => user:1:avatars => [23]
|
23
|
+
define_method "#{foreign_models_name}=" do |records|
|
24
|
+
if !options[:as]
|
25
|
+
# clear old assocs from related models side
|
26
|
+
self.send(foreign_models).to_a.each do |record|
|
27
|
+
$redis.zrem "#{record.model_name}:#{record.id}:#{model_name.pluralize}", id
|
28
|
+
end
|
29
|
+
|
30
|
+
# clear old assocs from this model side
|
31
|
+
$redis.zremrangebyscore "#{model_name}:#{id}:#{records[0].model_name.pluralize}", 0, Time.now.to_f
|
32
|
+
end
|
33
|
+
|
34
|
+
records.to_a.each do |record|
|
35
|
+
# we use here *foreign_models_name* not *record.model_name.pluralize* because of the :as option
|
36
|
+
$redis.zadd("#{model_name}:#{id}:#{foreign_models_name}", Time.now.to_f, record.id)
|
37
|
+
|
38
|
+
if !options[:as]
|
39
|
+
# article.comments = [comment1, comment2]
|
40
|
+
# iterate through the array of comments and create backlink
|
41
|
+
# check whether *record* object has *has_many* declaration and TODO it states *self.model_name* in plural
|
42
|
+
if class_associations[record.model_name].detect{|h| h[:type] == :has_many && h[:foreign_models] == model_name.pluralize.to_sym} #&& !$redis.zrank("#{record.model_name}:#{record.id}:#{model_name.pluralize}", id)#record.model_name.to_s.camelize.constantize.find(id).nil?
|
43
|
+
$redis.zadd("#{record.model_name}:#{record.id}:#{model_name.pluralize}", Time.now.to_f, id)
|
44
|
+
# check whether *record* object has *has_one* declaration and TODO it states *self.model_name*
|
45
|
+
elsif record.get_associations.detect{|h| [:has_one, :belongs_to].include?(h[:type]) && h[:foreign_model] == model_name.to_sym} # overwrite assoc anyway so we don't need to check record.send(model_name.to_sym).nil? here
|
46
|
+
$redis.set("#{record.model_name}:#{record.id}:#{model_name}", id)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module RedisOrm
|
2
|
+
module Associations
|
3
|
+
class HasManyProxy
|
4
|
+
def initialize(reciever_model_name, reciever_id, foreign_models, options)
|
5
|
+
@records = [] #records.to_a
|
6
|
+
@reciever_model_name = reciever_model_name
|
7
|
+
@reciever_id = reciever_id
|
8
|
+
@foreign_models = foreign_models
|
9
|
+
@options = options
|
10
|
+
@fetched = false
|
11
|
+
end
|
12
|
+
|
13
|
+
def fetch
|
14
|
+
@records = @foreign_models.to_s.singularize.camelize.constantize.find($redis.zrevrangebyscore __key__, Time.now.to_f, 0)
|
15
|
+
@fetched = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](index)
|
19
|
+
fetch if !@fetched
|
20
|
+
@records[index]
|
21
|
+
end
|
22
|
+
|
23
|
+
# user = User.find(1)
|
24
|
+
# user.avatars << Avatar.find(23) => user:1:avatars => [23]
|
25
|
+
def <<(new_records)
|
26
|
+
new_records.to_a.each do |record|
|
27
|
+
$redis.zadd(__key__, Time.now.to_f, record.id)
|
28
|
+
|
29
|
+
if !@options[:as]
|
30
|
+
record_associations = record.get_associations
|
31
|
+
|
32
|
+
# article.comments << [comment1, comment2]
|
33
|
+
# iterate through the array of comments and create backlink
|
34
|
+
# check whether *record* object has *has_many* declaration and TODO it states *self.model_name* in plural and there is no record yet from the *record*'s side (in order not to provoke recursion)
|
35
|
+
if has_many_assoc = record_associations.detect{|h| h[:type] == :has_many && h[:foreign_models] == @reciever_model_name.pluralize.to_sym}
|
36
|
+
pluralized_reciever_model_name = if has_many_assoc[:options][:as]
|
37
|
+
has_many_assoc[:options][:as].pluralize
|
38
|
+
else
|
39
|
+
@reciever_model_name.pluralize
|
40
|
+
end
|
41
|
+
|
42
|
+
if !$redis.zrank("#{record.model_name}:#{record.id}:#{pluralized_reciever_model_name}", @reciever_id)
|
43
|
+
$redis.zadd("#{record.model_name}:#{record.id}:#{pluralized_reciever_model_name}", Time.now.to_f, @reciever_id)
|
44
|
+
end
|
45
|
+
# 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)
|
46
|
+
elsif has_one_assoc = record_associations.detect{|h| [:has_one, :belongs_to].include?(h[:type]) && h[:foreign_model] == @reciever_model_name.to_sym}
|
47
|
+
reciever_model_name = if has_one_assoc[:options][:as]
|
48
|
+
has_one_assoc[:options][:as].to_sym
|
49
|
+
else
|
50
|
+
@reciever_model_name
|
51
|
+
end
|
52
|
+
if record.send(reciever_model_name).nil?
|
53
|
+
$redis.set("#{record.model_name}:#{record.id}:#{reciever_model_name}", @reciever_id)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def all(options = {})
|
61
|
+
if options[:limit] || options[:offset] || options[:order]
|
62
|
+
limit = if options[:limit] && options[:offset]
|
63
|
+
[options[:offset].to_i, options[:limit].to_i]
|
64
|
+
elsif options[:limit]
|
65
|
+
[0, options[:limit].to_i]
|
66
|
+
end
|
67
|
+
|
68
|
+
record_ids = if options[:order].to_s == 'desc'
|
69
|
+
$redis.zrevrangebyscore(__key__, Time.now.to_f, 0, :limit => limit)
|
70
|
+
else
|
71
|
+
$redis.zrangebyscore(__key__, 0, Time.now.to_f, :limit => limit)
|
72
|
+
end
|
73
|
+
@fetched = true
|
74
|
+
@records = @foreign_models.to_s.singularize.camelize.constantize.find(record_ids)
|
75
|
+
else
|
76
|
+
fetch if !@fetched
|
77
|
+
@records
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def find(token = nil, options = {})
|
82
|
+
if token.is_a?(String) || token.is_a?(Integer)
|
83
|
+
record_id = $redis.zrank(__key__, token.to_i)
|
84
|
+
if record_id
|
85
|
+
@fetched = true
|
86
|
+
@records = @foreign_models.to_s.singularize.camelize.constantize.find(token)
|
87
|
+
else
|
88
|
+
nil
|
89
|
+
end
|
90
|
+
elsif token == :all
|
91
|
+
all(options)
|
92
|
+
elsif token == :first
|
93
|
+
all(options.merge({:limit => 1}))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def delete(id)
|
98
|
+
$redis.zrem(__key__, id.to_i)
|
99
|
+
end
|
100
|
+
|
101
|
+
def count
|
102
|
+
$redis.zcard __key__
|
103
|
+
end
|
104
|
+
|
105
|
+
def method_missing(method_name, *args, &block)
|
106
|
+
fetch if !@fetched
|
107
|
+
@records.send(method_name, *args, &block)
|
108
|
+
end
|
109
|
+
|
110
|
+
protected
|
111
|
+
|
112
|
+
# helper method
|
113
|
+
def __key__
|
114
|
+
@options[:as] ? "#{@reciever_model_name}:#{@reciever_id}:#{@options[:as]}" : "#{@reciever_model_name}:#{@reciever_id}:#{@foreign_models}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module RedisOrm
|
2
|
+
module Associations
|
3
|
+
module HasOne
|
4
|
+
# user.avatars => user:1:avatars => [1, 22, 234] => Avatar.find([1, 22, 234])
|
5
|
+
# *options* is a hash and can hold:
|
6
|
+
# *:as* key
|
7
|
+
# *:dependant* key: either *destroy* or *nullify* (default)
|
8
|
+
def has_one(foreign_model, options = {})
|
9
|
+
class_associations = class_variable_get(:"@@associations")
|
10
|
+
class_associations[model_name] << {:type => :has_one, :foreign_model => foreign_model, :options => options}
|
11
|
+
|
12
|
+
foreign_model_name = if options[:as]
|
13
|
+
options[:as].to_sym
|
14
|
+
else
|
15
|
+
foreign_model.to_sym
|
16
|
+
end
|
17
|
+
|
18
|
+
define_method foreign_model_name do
|
19
|
+
foreign_model.to_s.camelize.constantize.find($redis.get "#{model_name}:#{@id}:#{foreign_model_name}")
|
20
|
+
end
|
21
|
+
|
22
|
+
# profile = Profile.create :title => 'test'
|
23
|
+
# user.profile = profile => user:23:profile => 1
|
24
|
+
define_method "#{foreign_model_name}=" do |assoc_with_record|
|
25
|
+
# we need to store this to clear old associations later
|
26
|
+
old_assoc = self.send(foreign_model_name)
|
27
|
+
|
28
|
+
if assoc_with_record.model_name == foreign_model.to_s
|
29
|
+
$redis.set("#{model_name}:#{id}:#{foreign_model_name}", assoc_with_record.id)
|
30
|
+
else
|
31
|
+
raise TypeMismatchError
|
32
|
+
end
|
33
|
+
|
34
|
+
# check whether *assoc_with_record* object has *belongs_to* declaration and TODO it states *self.model_name* and there is no record yet from the *assoc_with_record*'s side (in order not to provoke recursion)
|
35
|
+
if class_associations[assoc_with_record.model_name].detect{|h| [:belongs_to, :has_one].include?(h[:type]) && h[:foreign_model] == model_name.to_sym} && assoc_with_record.send(model_name.to_sym).nil?
|
36
|
+
assoc_with_record.send("#{model_name}=", self)
|
37
|
+
elsif class_associations[assoc_with_record.model_name].detect{|h| :has_many == h[:type] && h[:foreign_models] == model_name.to_s.pluralize.to_sym} && !$redis.zrank("#{assoc_with_record.model_name}:#{assoc_with_record.id}:#{model_name.pluralize}", self.id)
|
38
|
+
# remove old assoc
|
39
|
+
# $redis.zrank "city:2:profiles", 12
|
40
|
+
if old_assoc
|
41
|
+
#puts 'key - ' + "#{assoc_with_record.model_name}:#{old_assoc.id}:#{model_name.to_s.pluralize}"
|
42
|
+
#puts 'self.id - ' + self.id.to_s
|
43
|
+
$redis.zrem "#{assoc_with_record.model_name}:#{old_assoc.id}:#{model_name.to_s.pluralize}", self.id
|
44
|
+
end
|
45
|
+
# create/add new ones
|
46
|
+
assoc_with_record.send(model_name.pluralize.to_sym).send(:"<<", self)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,502 @@
|
|
1
|
+
require 'active_support/inflector/inflections'
|
2
|
+
require 'active_support/inflector/transliterate'
|
3
|
+
require 'active_support/inflector/methods'
|
4
|
+
require 'active_support/inflections'
|
5
|
+
require 'active_support/core_ext/string/inflections'
|
6
|
+
require 'active_support/core_ext/time/calculations' # local_time for to_time(:local)
|
7
|
+
require 'active_support/core_ext/string/conversions' # to_time
|
8
|
+
|
9
|
+
require 'active_model/validator'
|
10
|
+
require 'active_model/validations'
|
11
|
+
|
12
|
+
module RedisOrm
|
13
|
+
# there is no Boolean class in Ruby so defining a special class to specify TrueClass or FalseClass objects
|
14
|
+
class Boolean
|
15
|
+
end
|
16
|
+
|
17
|
+
# it's raised when found request was initiated on the property/properties which have no index on it
|
18
|
+
class NotIndexFound < StandardError
|
19
|
+
end
|
20
|
+
|
21
|
+
class TypeMismatchError < StandardError
|
22
|
+
end
|
23
|
+
|
24
|
+
class ArgumentsMismatch < StandardError
|
25
|
+
end
|
26
|
+
|
27
|
+
class Base
|
28
|
+
include ActiveModel::Validations
|
29
|
+
include ActiveModelBehavior
|
30
|
+
|
31
|
+
extend Associations::BelongsTo
|
32
|
+
extend Associations::HasMany
|
33
|
+
extend Associations::HasOne
|
34
|
+
|
35
|
+
attr_accessor :persisted
|
36
|
+
|
37
|
+
@@properties = Hash.new{|h,k| h[k] = []}
|
38
|
+
@@indices = Hash.new{|h,k| h[k] = []} # compound indices are available too
|
39
|
+
@@associations = Hash.new{|h,k| h[k] = []}
|
40
|
+
@@callbacks = Hash.new{|h,k| h[k] = {}}
|
41
|
+
|
42
|
+
class << self
|
43
|
+
|
44
|
+
def inherited(from)
|
45
|
+
[:after_save, :before_save, :after_create, :before_create, :after_destroy, :before_destroy].each do |callback_name|
|
46
|
+
@@callbacks[from.model_name][callback_name] = []
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# *options* currently supports
|
51
|
+
# *unique* Boolean
|
52
|
+
# *case_insensitive* Boolean TODO
|
53
|
+
def index(name, options = {})
|
54
|
+
@@indices[model_name] << {:name => name, :options => options}
|
55
|
+
end
|
56
|
+
|
57
|
+
def property(property_name, class_name, options = {})
|
58
|
+
@@properties[model_name] << {:name => property_name, :class => class_name.to_s, :options => options}
|
59
|
+
|
60
|
+
send(:define_method, property_name) do
|
61
|
+
value = instance_variable_get(:"@#{property_name}")
|
62
|
+
|
63
|
+
return value if value.nil? # we must return nil here so :default option will work when saving, otherwise it'll return "" or 0 or 0.0
|
64
|
+
|
65
|
+
if Time == class_name
|
66
|
+
value = begin
|
67
|
+
value.to_s.to_time(:local)
|
68
|
+
rescue ArgumentError => e
|
69
|
+
nil
|
70
|
+
end
|
71
|
+
elsif Integer == class_name
|
72
|
+
value = value.to_i
|
73
|
+
elsif Float == class_name
|
74
|
+
value = value.to_f
|
75
|
+
elsif RedisOrm::Boolean == class_name
|
76
|
+
value = (value == "false" ? false : true)
|
77
|
+
end
|
78
|
+
value
|
79
|
+
end
|
80
|
+
|
81
|
+
send(:define_method, :"#{property_name}=") do |value|
|
82
|
+
if instance_variable_get(:"@#{property_name}_changes") && !instance_variable_get(:"@#{property_name}_changes").empty?
|
83
|
+
initial_value = instance_variable_get(:"@#{property_name}_changes")[0]
|
84
|
+
instance_variable_set(:"@#{property_name}_changes", [initial_value, value])
|
85
|
+
elsif instance_variable_get(:"@#{property_name}")
|
86
|
+
instance_variable_set(:"@#{property_name}_changes", [self.send(property_name), value])
|
87
|
+
else
|
88
|
+
instance_variable_set(:"@#{property_name}_changes", [value])
|
89
|
+
end
|
90
|
+
|
91
|
+
instance_variable_set(:"@#{property_name}", value)
|
92
|
+
end
|
93
|
+
|
94
|
+
send(:define_method, :"#{property_name}_changes") do
|
95
|
+
instance_variable_get(:"@#{property_name}_changes")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def count
|
100
|
+
$redis.zcard("#{model_name}:ids").to_i
|
101
|
+
end
|
102
|
+
|
103
|
+
def first
|
104
|
+
id = $redis.zrangebyscore("#{model_name}:ids", 0, Time.now.to_f, :limit => [0, 1])
|
105
|
+
id.empty? ? nil : find(id[0])
|
106
|
+
end
|
107
|
+
|
108
|
+
def last
|
109
|
+
id = $redis.zrevrangebyscore("#{model_name}:ids", Time.now.to_f, 0, :limit => [0, 1])
|
110
|
+
id.empty? ? nil : find(id[0])
|
111
|
+
end
|
112
|
+
|
113
|
+
def all(options = {})
|
114
|
+
limit = if options[:limit] && options[:offset]
|
115
|
+
[options[:offset].to_i, options[:limit].to_i]
|
116
|
+
elsif options[:limit]
|
117
|
+
[0, options[:limit].to_i]
|
118
|
+
end
|
119
|
+
|
120
|
+
if options[:order].to_s == 'desc'
|
121
|
+
$redis.zrevrangebyscore("#{model_name}:ids", Time.now.to_f, 0, :limit => limit).compact.collect{|id| find(id)}
|
122
|
+
else
|
123
|
+
$redis.zrangebyscore("#{model_name}:ids", 0, Time.now.to_f, :limit => limit).compact.collect{|id| find(id)}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def find(ids)
|
128
|
+
if ids.is_a?(Hash)
|
129
|
+
all(ids)
|
130
|
+
elsif ids.is_a?(Array)
|
131
|
+
return [] if ids.empty?
|
132
|
+
ids.inject([]) do |array, id|
|
133
|
+
record = $redis.hgetall "#{model_name}:#{id}"
|
134
|
+
if record && !record.empty?
|
135
|
+
array << new(record, id, true)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
else
|
139
|
+
return nil if ids.nil?
|
140
|
+
id = ids
|
141
|
+
record = $redis.hgetall "#{model_name}:#{id}"
|
142
|
+
if record && record.empty?
|
143
|
+
nil
|
144
|
+
else
|
145
|
+
new(record, id, true)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def after_save(callback)
|
151
|
+
@@callbacks[model_name][:after_save] << callback
|
152
|
+
end
|
153
|
+
|
154
|
+
def before_save(callback)
|
155
|
+
@@callbacks[model_name][:before_save] << callback
|
156
|
+
end
|
157
|
+
|
158
|
+
def after_create(callback)
|
159
|
+
@@callbacks[model_name][:after_create] << callback
|
160
|
+
end
|
161
|
+
|
162
|
+
def before_create(callback)
|
163
|
+
@@callbacks[model_name][:before_create] << callback
|
164
|
+
end
|
165
|
+
|
166
|
+
def after_destroy(callback)
|
167
|
+
@@callbacks[model_name][:after_destroy] << callback
|
168
|
+
end
|
169
|
+
|
170
|
+
def before_destroy(callback)
|
171
|
+
@@callbacks[model_name][:before_destroy] << callback
|
172
|
+
end
|
173
|
+
|
174
|
+
def create(options = {})
|
175
|
+
obj = new(options, nil, false)
|
176
|
+
obj.save
|
177
|
+
obj
|
178
|
+
end
|
179
|
+
|
180
|
+
# dynamic finders
|
181
|
+
def method_missing(method_name, *args, &block)
|
182
|
+
if method_name =~ /^find_(all_)?by_(\w*)/
|
183
|
+
prepared_index = model_name.to_s
|
184
|
+
index = if $2
|
185
|
+
properties = $2.split('_and_')
|
186
|
+
raise ArgumentsMismatch if properties.size != args.size
|
187
|
+
|
188
|
+
properties.each_with_index do |prop, i|
|
189
|
+
# raise if User.find_by_firstname_and_castname => there's no *castname* in User's properties
|
190
|
+
raise ArgumentsMismatch if !@@properties[model_name].detect{|p| p[:name] == prop.to_sym}
|
191
|
+
prepared_index += ":#{prop}:#{args[i].to_s}"
|
192
|
+
end
|
193
|
+
|
194
|
+
@@indices[model_name].detect do |models_index|
|
195
|
+
if models_index[:name].is_a?(Array) && models_index[:name].size == properties.size
|
196
|
+
models_index[:name] == properties.map{|p| p.to_sym}
|
197
|
+
elsif !models_index[:name].is_a?(Array) && properties.size == 1
|
198
|
+
models_index[:name] == properties[0].to_sym
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
raise NotIndexFound if !index
|
204
|
+
|
205
|
+
if method_name =~ /^find_by_(\w*)/
|
206
|
+
id = if index[:options][:unique]
|
207
|
+
$redis.get prepared_index
|
208
|
+
else
|
209
|
+
$redis.zrangebyscore(prepared_index, 0, Time.now.to_f, :limit => [0, 1])[0]
|
210
|
+
end
|
211
|
+
model_name.to_s.camelize.constantize.find(id)
|
212
|
+
elsif method_name =~ /^find_all_by_(\w*)/
|
213
|
+
records = []
|
214
|
+
|
215
|
+
if index[:options][:unique]
|
216
|
+
id = $redis.get prepared_index
|
217
|
+
records << model_name.to_s.camelize.constantize.find(id)
|
218
|
+
else
|
219
|
+
ids = $redis.zrangebyscore(prepared_index, 0, Time.now.to_f)
|
220
|
+
records += model_name.to_s.camelize.constantize.find(ids)
|
221
|
+
end
|
222
|
+
|
223
|
+
records
|
224
|
+
else
|
225
|
+
nil
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
# could be invoked from has_many module (<< method)
|
233
|
+
def to_a
|
234
|
+
[self]
|
235
|
+
end
|
236
|
+
|
237
|
+
# is called from RedisOrm::Associations::HasMany to save backlinks to saved records
|
238
|
+
def get_associations
|
239
|
+
@@associations[self.model_name]
|
240
|
+
end
|
241
|
+
|
242
|
+
def initialize(attributes = {}, id = nil, persisted = false)
|
243
|
+
@persisted = persisted
|
244
|
+
|
245
|
+
instance_variable_set(:"@id", id.to_i) if id
|
246
|
+
|
247
|
+
# when object is created with empty attribute set @#{prop[:name]}_changes array properly
|
248
|
+
@@properties[model_name].each do |prop|
|
249
|
+
if prop[:options][:default]
|
250
|
+
instance_variable_set :"@#{prop[:name]}_changes", [prop[:options][:default]]
|
251
|
+
else
|
252
|
+
instance_variable_set :"@#{prop[:name]}_changes", []
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
if attributes.is_a?(Hash) && !attributes.empty?
|
257
|
+
attributes.each do |key, value|
|
258
|
+
self.send("#{key}=".to_sym, value) if self.respond_to?("#{key}=".to_sym)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
self
|
262
|
+
end
|
263
|
+
|
264
|
+
def id
|
265
|
+
@id
|
266
|
+
end
|
267
|
+
|
268
|
+
def persisted?
|
269
|
+
@persisted
|
270
|
+
end
|
271
|
+
|
272
|
+
def save
|
273
|
+
return false if !valid?
|
274
|
+
|
275
|
+
# store here initial persisted flag so we could invoke :after_create callbacks in the end of the function
|
276
|
+
was_persisted = persisted?
|
277
|
+
|
278
|
+
if persisted? # then there might be old indices
|
279
|
+
# check whether there's old indices exists and if yes - delete them
|
280
|
+
@@properties[model_name].each do |prop|
|
281
|
+
prop_changes = instance_variable_get :"@#{prop[:name]}_changes"
|
282
|
+
|
283
|
+
next if prop_changes.size < 2
|
284
|
+
prev_prop_value = prop_changes.first
|
285
|
+
|
286
|
+
indices = @@indices[model_name].inject([]) do |sum, models_index|
|
287
|
+
if models_index[:name].is_a?(Array)
|
288
|
+
if models_index[:name].include?(prop[:name])
|
289
|
+
sum << models_index
|
290
|
+
else
|
291
|
+
sum
|
292
|
+
end
|
293
|
+
else
|
294
|
+
if models_index[:name] == prop[:name]
|
295
|
+
sum << models_index
|
296
|
+
else
|
297
|
+
sum
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
if !indices.empty?
|
303
|
+
indices.each do |index|
|
304
|
+
if index[:name].is_a?(Array)
|
305
|
+
keys_to_delete = if index[:name].index(prop) == 0
|
306
|
+
$redis.keys "#{model_name}:#{prop[:name]}#{prev_prop_value}*"
|
307
|
+
else
|
308
|
+
$redis.keys "#{model_name}:*#{prop[:name]}:#{prev_prop_value}*"
|
309
|
+
end
|
310
|
+
|
311
|
+
keys_to_delete.each{|key| $redis.del(key)}
|
312
|
+
else
|
313
|
+
key_to_delete = "#{model_name}:#{prop[:name]}:#{prev_prop_value}"
|
314
|
+
$redis.del key_to_delete
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|
318
|
+
end
|
319
|
+
else # !persisted?
|
320
|
+
@@callbacks[model_name][:before_create].each do |callback|
|
321
|
+
self.send(callback)
|
322
|
+
end
|
323
|
+
|
324
|
+
@id = $redis.incr("#{model_name}:id")
|
325
|
+
$redis.zadd "#{model_name}:ids", Time.now.to_f, @id
|
326
|
+
@persisted = true
|
327
|
+
|
328
|
+
if @@properties[model_name].detect{|p| p[:name] == :created_at }
|
329
|
+
self.created_at = Time.now
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
@@callbacks[model_name][:before_save].each do |callback|
|
334
|
+
self.send(callback)
|
335
|
+
end
|
336
|
+
|
337
|
+
# automatically update *modified_at* property if it was defined
|
338
|
+
if @@properties[model_name].detect{|p| p[:name] == :modified_at }
|
339
|
+
self.modified_at = Time.now
|
340
|
+
end
|
341
|
+
|
342
|
+
@@properties[model_name].each do |prop|
|
343
|
+
prop_value = self.send(prop[:name].to_sym)
|
344
|
+
|
345
|
+
if prop_value.nil? && !prop[:options][:default].nil?
|
346
|
+
prop_value = prop[:options][:default]
|
347
|
+
# set instance variable in order to properly save indexes here
|
348
|
+
self.instance_variable_set(:"@#{prop[:name]}", prop[:options][:default])
|
349
|
+
end
|
350
|
+
|
351
|
+
$redis.hset("#{model_name}:#{id}", prop[:name].to_s, prop_value)
|
352
|
+
|
353
|
+
# reducing @#{prop[:name]}_changes array to the last value
|
354
|
+
prop_changes = instance_variable_get :"@#{prop[:name]}_changes"
|
355
|
+
if prop_changes && prop_changes.size > 2
|
356
|
+
instance_variable_set :"@#{prop[:name]}_changes", [prop_changes.last]
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
# save new indices in order to sort by finders
|
361
|
+
# city:name:Харьков => 1
|
362
|
+
@@indices[model_name].each do |index|
|
363
|
+
prepared_index = if index[:name].is_a?(Array) # TODO sort alphabetically
|
364
|
+
index[:name].inject([model_name]) do |sum, index_part|
|
365
|
+
sum += [index_part, self.instance_variable_get(:"@#{index_part}").to_s]
|
366
|
+
end.join(':')
|
367
|
+
else
|
368
|
+
[model_name, index[:name], self.instance_variable_get(:"@#{index[:name]}").to_s].join(':')
|
369
|
+
end
|
370
|
+
|
371
|
+
if index[:options][:unique]
|
372
|
+
$redis.set(prepared_index, @id)
|
373
|
+
else
|
374
|
+
$redis.zadd(prepared_index, Time.now.to_f, @id)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
@@callbacks[model_name][:after_save].each do |callback|
|
379
|
+
self.send(callback)
|
380
|
+
end
|
381
|
+
|
382
|
+
if ! was_persisted
|
383
|
+
@@callbacks[model_name][:after_create].each do |callback|
|
384
|
+
self.send(callback)
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
true # if there were no errors just return true, so *if* conditions would work
|
389
|
+
end
|
390
|
+
|
391
|
+
def update_attributes(attributes)
|
392
|
+
if attributes.is_a?(Hash)
|
393
|
+
attributes.each do |key, value|
|
394
|
+
self.send("#{key}=".to_sym, value) if self.respond_to?("#{key}=".to_sym)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
save
|
398
|
+
end
|
399
|
+
|
400
|
+
def update_attribute(attribute_name, attribute_value)
|
401
|
+
self.send("#{attribute_name}=".to_sym, attribute_value) if self.respond_to?("#{attribute_name}=".to_sym)
|
402
|
+
save
|
403
|
+
end
|
404
|
+
|
405
|
+
def destroy
|
406
|
+
@@callbacks[model_name][:before_destroy].each do |callback|
|
407
|
+
self.send(callback)
|
408
|
+
end
|
409
|
+
|
410
|
+
@@properties[model_name].each do |prop|
|
411
|
+
$redis.hdel("#{model_name}:#{@id}", prop.to_s)
|
412
|
+
end
|
413
|
+
|
414
|
+
$redis.zrem "#{model_name}:ids", @id
|
415
|
+
|
416
|
+
# also we need to delete *links* to associated records
|
417
|
+
if !@@associations[model_name].empty?
|
418
|
+
@@associations[model_name].each do |assoc|
|
419
|
+
|
420
|
+
foreign_model = ""
|
421
|
+
records = []
|
422
|
+
|
423
|
+
case assoc[:type]
|
424
|
+
when :belongs_to
|
425
|
+
foreign_model = assoc[:foreign_model].to_s
|
426
|
+
foreign_model_name = assoc[:options][:as] ? assoc[:options][:as] : assoc[:foreign_model]
|
427
|
+
records << self.send(foreign_model_name)
|
428
|
+
|
429
|
+
$redis.del "#{model_name}:#{@id}:#{assoc[:foreign_model]}"
|
430
|
+
when :has_one
|
431
|
+
foreign_model = assoc[:foreign_model].to_s
|
432
|
+
foreign_model_name = assoc[:options][:as] ? assoc[:options][:as] : assoc[:foreign_model]
|
433
|
+
records << self.send(foreign_model_name)
|
434
|
+
|
435
|
+
$redis.del "#{model_name}:#{@id}:#{assoc[:foreign_model]}"
|
436
|
+
when :has_many
|
437
|
+
foreign_model = assoc[:foreign_models].to_s.singularize
|
438
|
+
foreign_models_name = assoc[:options][:as] ? assoc[:options][:as] : assoc[:foreign_models]
|
439
|
+
records += self.send(foreign_models_name)
|
440
|
+
|
441
|
+
# delete all members
|
442
|
+
$redis.zremrangebyscore "#{model_name}:#{@id}:#{assoc[:foreign_models]}", 0, Time.now.to_f
|
443
|
+
end
|
444
|
+
|
445
|
+
# check whether foreign_model also has an assoc to the destroying record
|
446
|
+
# and remove an id of destroing record from each of associated sets
|
447
|
+
if !records.compact.empty?
|
448
|
+
records.compact.each do |record|
|
449
|
+
# we make 3 different checks rather then 1 with elsif to ensure that all associations will be processed
|
450
|
+
# it's covered in test/option_test in "should delete link to associated record when record was deleted" scenario
|
451
|
+
# for if class Album; has_one :photo, :as => :front_photo; has_many :photos; end
|
452
|
+
# end some photo from the album will be deleted w/o these checks only first has_one will be triggered
|
453
|
+
if @@associations[foreign_model].detect{|h| h[:type] == :belongs_to && h[:foreign_model] == model_name.to_sym}
|
454
|
+
#puts 'from destr :belongs_to - ' + "#{foreign_model}:#{record.id}:#{model_name}"
|
455
|
+
$redis.del "#{foreign_model}:#{record.id}:#{model_name}"
|
456
|
+
end
|
457
|
+
|
458
|
+
if @@associations[foreign_model].detect{|h| h[:type] == :has_one && h[:foreign_model] == model_name.to_sym}
|
459
|
+
#puts 'from destr :has_one - ' + "#{foreign_model}:#{record.id}:#{model_name}"
|
460
|
+
$redis.del "#{foreign_model}:#{record.id}:#{model_name}"
|
461
|
+
end
|
462
|
+
|
463
|
+
if @@associations[foreign_model].detect{|h| h[:type] == :has_many && h[:foreign_models] == model_name.pluralize.to_sym}
|
464
|
+
#puts "from destr :has_many - " + "#{foreign_model}:#{record.id}:#{model_name.pluralize}"
|
465
|
+
$redis.zrem "#{foreign_model}:#{record.id}:#{model_name.pluralize}", @id
|
466
|
+
end
|
467
|
+
end
|
468
|
+
end
|
469
|
+
|
470
|
+
if assoc[:options][:dependant] == :destroy
|
471
|
+
records.each do |r|
|
472
|
+
r.destroy
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
# we need to ensure that smembers are correct after removal of the record
|
479
|
+
@@indices[model_name].each do |index|
|
480
|
+
prepared_index = if index[:name].is_a?(Array) # TODO sort alphabetically
|
481
|
+
index[:name].inject([model_name]) do |sum, index_part|
|
482
|
+
sum += [index_part, self.instance_variable_get(:"@#{index_part}")]
|
483
|
+
end.join(':')
|
484
|
+
else
|
485
|
+
[model_name, index[:name], self.instance_variable_get(:"@#{index[:name]}")].join(':')
|
486
|
+
end
|
487
|
+
|
488
|
+
if index[:options][:unique]
|
489
|
+
$redis.del(prepared_index)
|
490
|
+
else
|
491
|
+
$redis.zremrangebyscore(prepared_index, 0, Time.now.to_f)
|
492
|
+
end
|
493
|
+
end
|
494
|
+
|
495
|
+
@@callbacks[model_name][:after_destroy].each do |callback|
|
496
|
+
self.send(callback)
|
497
|
+
end
|
498
|
+
|
499
|
+
true # if there were no errors just return true, so *if* conditions would work
|
500
|
+
end
|
501
|
+
end
|
502
|
+
end
|