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.
@@ -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