Class: RedisAssist::Base

Inherits:
Object
  • Object
show all
Extended by:
Finders
Includes:
Associations, Callbacks, Validations
Defined in:
lib/redis_assist/base.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Finders

all, exists?, find, find_by_id, find_by_ids, find_in_batches, first, last

Methods included from Associations

included

Methods included from Validations

#add_error, #errors, included, #validate

Methods included from Callbacks

included, #invoke_callback

Constructor Details

#initialize(attrs = {}) ⇒ Base

Returns a new instance of Base.



223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/redis_assist/base.rb', line 223

def initialize(attrs={})
  self.attributes = {}
  self.lists      = {}
  self.hashes     = {}
  
  if attrs[:id]
    self.id = attrs[:id]
    load_attributes(attrs[:raw_attributes])
    return self if self.id 
  end
  
  self.new_record = true
  
  invoke_callback(:on_load)
  
  self.class.persisted_attrs.keys.each do |name|
    send("#{name}=", attrs[name]) if attrs[name]
    attrs.delete(name)
  end
  
  raise "RedisAssist: #{self.class.name} does not support attributes: #{attrs.keys.join(', ')}" if attrs.length > 0
end

Instance Attribute Details

#attributesObject

Returns the value of attribute attributes.



217
218
219
# File 'lib/redis_assist/base.rb', line 217

def attributes
  @attributes
end

#idObject



219
220
221
# File 'lib/redis_assist/base.rb', line 219

def id
  @id.to_i
end

Class Method Details

.attr_persist(name, opts = {}) ⇒ Object



22
23
24
25
26
27
28
29
30
31
32
# File 'lib/redis_assist/base.rb', line 22

def attr_persist(name, opts={})
  persisted_attrs[name] = opts

  if opts[:as].eql?(:list)
    define_list(name)
  elsif opts[:as].eql?(:hash)
    define_hash(name)
  else
    define_attribute(name)
  end
end

.countObject

Get count of records



36
37
38
# File 'lib/redis_assist/base.rb', line 36

def count
  redis.zcard(index_key_for(:id))
end

.create(attrs = {}) ⇒ Object



41
42
43
44
# File 'lib/redis_assist/base.rb', line 41

def create(attrs={})
  roll = new(attrs)
  roll.save ? roll : false
end

.fieldsObject



93
94
95
# File 'lib/redis_assist/base.rb', line 93

def fields
  persisted_attrs.select{|k,v| !(v[:as].eql?(:list) || v[:as].eql?(:hash)) }
end

.hash_to_redis(obj) ⇒ Object



175
176
177
# File 'lib/redis_assist/base.rb', line 175

def hash_to_redis(obj)
  obj.each_with_object([]) {|kv,args| args << kv[0] << kv[1] }
end

.hashesObject



103
104
105
# File 'lib/redis_assist/base.rb', line 103

def hashes 
  persisted_attrs.select{|k,v| v[:as].eql?(:hash) }
end

.index_key_for(index_name) ⇒ Object



114
115
116
# File 'lib/redis_assist/base.rb', line 114

def index_key_for(index_name)
  "#{key_prefix}:index:#{index_name}"
end

.inherited(base) ⇒ Object



12
13
14
15
16
# File 'lib/redis_assist/base.rb', line 12

def self.inherited(base)
  base.before_create {|record| record.send(:created_at=, Time.now.to_f) if record.respond_to?(:created_at) }
  base.before_update {|record| record.send(:updated_at=, Time.now.to_f) if record.respond_to?(:updated_at) }
  base.after_create  {|record| record.send(:new_record=, false) }
end

.key_for(id, attribute) ⇒ Object



119
120
121
# File 'lib/redis_assist/base.rb', line 119

def key_for(id, attribute)
  "#{key_prefix}:#{id}:#{attribute}"
end

.key_prefix(val = nil) ⇒ Object



124
125
126
127
128
# File 'lib/redis_assist/base.rb', line 124

def key_prefix(val=nil)
  return self.key_prefix = val if val
  return @key_prefix if @key_prefix
  return self.key_prefix = StringHelper.underscore(name)
end

.key_prefix=(val) ⇒ Object



131
132
133
# File 'lib/redis_assist/base.rb', line 131

def key_prefix=(val)
  @key_prefix = val
end

.listsObject



98
99
100
# File 'lib/redis_assist/base.rb', line 98

def lists 
  persisted_attrs.select{|k,v| v[:as].eql?(:list) }
end

.load_attributes(*ids) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/redis_assist/base.rb', line 141

def load_attributes(*ids)
  future_attrs  = {}
  attrs         = {}

  # Load all the futures into an organized Hash
  redis.pipelined do |pipe|
    ids.each_with_object(future_attrs) do |id, futures|
      future_lists  = {}
      future_hashes = {}
      future_fields = nil
  
      lists.each do |name, opts|
        future_lists[name]  = pipe.lrange(key_for(id, name), 0, -1)
      end
  
      hashes.each do |name, opts|
        future_hashes[name] = pipe.hgetall(key_for(id, name))
      end
  
      future_fields = pipe.hmget(key_for(id, :attributes), fields.keys)

      futures[id] = { 
        lists:  future_lists, 
        hashes: future_hashes, 
        fields: future_fields, 
        exists: pipe.exists(key_for(id, :attributes))
      } 
    end
  end

  future_attrs
end

.persisted_attrsObject

TODO: Attribute class



109
110
111
# File 'lib/redis_assist/base.rb', line 109

def persisted_attrs
  @persisted_attrs ||= {}
end

.redisObject



136
137
138
# File 'lib/redis_assist/base.rb', line 136

def redis
  RedisAssist::Config.redis_client
end

.transform(direction, attr, val) ⇒ Object



82
83
84
85
86
87
88
89
90
# File 'lib/redis_assist/base.rb', line 82

def transform(direction, attr, val)
  transformer = RedisAssist.transforms[persisted_attrs[attr][:as]]

  if transformer
    transformer.transform(direction, val)
  else
    val || persisted_attrs[attr][:default]
  end
end

.update(id, params = {}, opts = {}) ⇒ Object

TODO: needs a refactor. Should this be an interface for skipping validations? Should we optimize and skip the find? Support an array of ids?



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/redis_assist/base.rb', line 49

def update(id, params={}, opts={})
  record = find(id)
  return false unless record

  record.send(:invoke_callback, :before_update)
  record.send(:invoke_callback, :before_save)

  redis.multi do
    params.each do |attr, val|
      if persisted_attrs.include?(attr)
        if fields.keys.include? attr
          transform(:to, attr, val)
          redis.hset(key_for(id, :attributes), attr, transform(:to, attr, val)) 
        end

        if lists.keys.include? attr
          redis.del(key_for(id, attr)) 
          redis.rpush(key_for(id, attr), val) unless val.empty?
        end

        if hashes.keys.include? attr
          redis.del(key_for(id, attr))
          redis.hmset(key_for(id, attr), *hash_to_redis(val))
        end
      end
    end
  end

  record.send(:invoke_callback, :after_save)
  record.send(:invoke_callback, :after_update)
end

Instance Method Details

#deleteObject



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# File 'lib/redis_assist/base.rb', line 398

def delete
  if respond_to?(:deleted_at)
    self.deleted_at = Time.now.to_f if respond_to?(:deleted_at)
    save
  else
    redis.multi do
      redis.del(key_for(:attributes))
      lists.merge(hashes).each do |name|
        redis.del(key_for(name))
      end
    end
  end

  remove_from_index(:id, id)

  invoke_callback(:after_delete)
  self
end

#deleted?Boolean

TODO: should this be a redis-assist feature?

Returns:

  • (Boolean)


393
394
395
396
# File 'lib/redis_assist/base.rb', line 393

def deleted?
  return false unless respond_to?(:deleted_at)
  deleted_at && deleted_at.is_a?(Time)
end

#inspectObject



440
441
442
443
# File 'lib/redis_assist/base.rb', line 440

def inspect
  attr_list = self.class.persisted_attrs.map{|key,val| "#{key}: #{send(key).to_s[0, 200]}" } * ", "
  "#<#{self.class.name} id: #{id}, #{attr_list}>"
end

#key_for(attribute) ⇒ Object



435
436
437
# File 'lib/redis_assist/base.rb', line 435

def key_for(attribute)
  self.class.key_for(id, attribute)
end

#new_record?Boolean

Returns:

  • (Boolean)


426
427
428
# File 'lib/redis_assist/base.rb', line 426

def new_record?
  !!new_record
end

#read_attribute(name) ⇒ Object

Transform and read a standard attribute



248
249
250
251
252
253
254
255
# File 'lib/redis_assist/base.rb', line 248

def read_attribute(name)
  if attributes.is_a?(Redis::Future)
    value = attributes.value 
    self.attributes = value ? Hash[*self.class.fields.keys.zip(value).flatten] : {}
  end

  self.class.transform(:from, name, attributes[name])
end

#read_hash(name) ⇒ Object

Transform and read a hash attribute



270
271
272
273
274
275
276
277
278
279
# File 'lib/redis_assist/base.rb', line 270

def read_hash(name)
  opts = self.class.persisted_attrs[name]

  if !hashes[name] && opts[:default]
    opts[:default]
  else
    self.send("#{name}=", hashes[name].value) if hashes[name].is_a?(Redis::Future)
    hashes[name]
  end
end

#read_list(name) ⇒ Object

Transform and read a list attribute



258
259
260
261
262
263
264
265
266
267
# File 'lib/redis_assist/base.rb', line 258

def read_list(name)
  opts = self.class.persisted_attrs[name]

  if !lists[name] && opts[:default]
    opts[:default]
  else
    send("#{name}=", lists[name].value) if lists[name].is_a?(Redis::Future)
    lists[name]
  end
end

#redisObject



430
431
432
# File 'lib/redis_assist/base.rb', line 430

def redis
  self.class.redis
end

#saveObject



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/redis_assist/base.rb', line 333

def save
  return false unless valid? 
  
  invoke_callback(:before_update) unless new_record?
  invoke_callback(:before_create) if new_record?
  invoke_callback(:before_save)
  
  # Assing the record with the next available ID
  self.id = generate_id if new_record?

  redis.multi do
    # Add to the index
    insert_into_index(:id, id, id) if new_record? 

    # Remove soft-deleted record from index
    if deleted?
      remove_from_index(:id, id)
      insert_into_index(:deleted_at, deleted_at.to_i, id)
    end

    # build the arguments to pass to redis hmset
    # and insure the attributes are explicitely declared
    unless attributes.is_a?(Redis::Future)
      attribute_args = hash_to_redis(attributes)
      redis.hmset(key_for(:attributes), *attribute_args)
    end
  
    lists.each do |name, val|
      if val && !val.is_a?(Redis::Future) 
        redis.del(key_for(name))
        redis.rpush(key_for(name), val) unless val.empty?
      end
    end
  
    hashes.each do |name, val|
      unless val.is_a?(Redis::Future)
        hash_as_args = hash_to_redis(val)
        redis.hmset(key_for(name), *hash_as_args)
      end
    end
  end

  invoke_callback(:after_save)
  invoke_callback(:after_update) unless new_record?
  invoke_callback(:after_create) if new_record?
  
  self
end

#save!Object



382
383
384
385
# File 'lib/redis_assist/base.rb', line 382

def save!
  raise "RedisAssist: save! failed with errors" unless save
  self
end

#saved?Boolean

Returns:

  • (Boolean)


299
300
301
# File 'lib/redis_assist/base.rb', line 299

def saved?
  !!(new_record?.eql?(false) && id)
end

#undeleteObject



417
418
419
420
421
422
423
424
# File 'lib/redis_assist/base.rb', line 417

def undelete
  if deleted?
    remove_from_index(:deleted_at, id)
    insert_into_index(:id, id, id)
    self.deleted_at = nil
  end
  save
end

#update_columns(attrs) ⇒ Object

Update fields without hitting the callbacks



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/redis_assist/base.rb', line 304

def update_columns(attrs)
  redis.multi do
    attrs.each do |attr, value|
      if self.class.fields.has_key?(attr)
        write_attribute(attr, value)  
        redis.hset(key_for(:attributes), attr, self.class.transform(:to, attr, value)) unless new_record?
      end

      if self.class.lists.has_key?(attr)
        write_list(attr, value)       

        unless new_record?
          redis.del(key_for(attr))
          redis.rpush(key_for(attr), value) unless value.empty?
        end
      end

      if self.class.hashes.has_key?(attr)
        write_hash(attr, value)       

        unless new_record?
          hash_as_args = hash_to_redis(value)
          redis.hmset(key_for(attr), *hash_as_args)
        end
      end
    end
  end
end

#valid?Boolean

Returns:

  • (Boolean)


387
388
389
390
# File 'lib/redis_assist/base.rb', line 387

def valid?
  invoke_callback(:before_validation)
  super
end

#write_attribute(name, val) ⇒ Object

Transform and write a standard attribute value



283
284
285
# File 'lib/redis_assist/base.rb', line 283

def write_attribute(name, val)
  attributes[name] = self.class.transform(:to, name, val)
end

#write_hash(name, val) ⇒ Object

Transform and write a hash attribute



294
295
296
297
# File 'lib/redis_assist/base.rb', line 294

def write_hash(name, val)
  raise "RedisAssist: tried to store a #{val.class.name} as Hash" unless val.is_a?(Hash)
  hashes[name] = val
end

#write_list(name, val) ⇒ Object

Transform and write a list value



288
289
290
291
# File 'lib/redis_assist/base.rb', line 288

def write_list(name, val)
  raise "RedisAssist: tried to store a #{val.class.name} as Array" unless val.is_a?(Array)
  lists[name] = val
end