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.



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

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.



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

def attributes
  @attributes
end

#idObject



221
222
223
# File 'lib/redis_assist/base.rb', line 221

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



95
96
97
# File 'lib/redis_assist/base.rb', line 95

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

.hash_to_redis(obj) ⇒ Object



177
178
179
# File 'lib/redis_assist/base.rb', line 177

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

.hashesObject



105
106
107
# File 'lib/redis_assist/base.rb', line 105

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

.index_key_for(index_name) ⇒ Object



116
117
118
# File 'lib/redis_assist/base.rb', line 116

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



121
122
123
# File 'lib/redis_assist/base.rb', line 121

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

.key_prefix(val = nil) ⇒ Object



126
127
128
129
130
# File 'lib/redis_assist/base.rb', line 126

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



133
134
135
# File 'lib/redis_assist/base.rb', line 133

def key_prefix=(val)
  @key_prefix = val
end

.listsObject



100
101
102
# File 'lib/redis_assist/base.rb', line 100

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

.load_attributes(*ids) ⇒ Object



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
173
174
# File 'lib/redis_assist/base.rb', line 143

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



111
112
113
# File 'lib/redis_assist/base.rb', line 111

def persisted_attrs
  @persisted_attrs ||= {}
end

.redisObject



138
139
140
# File 'lib/redis_assist/base.rb', line 138

def redis
  RedisAssist::Config.redis_client
end

.transform(direction, attr, val) ⇒ Object



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

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

  if transformer
    transformer.transform(direction, value) if transformer
  else
    value
  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



405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
# File 'lib/redis_assist/base.rb', line 405

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)


400
401
402
403
# File 'lib/redis_assist/base.rb', line 400

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

#inspectObject



447
448
449
450
# File 'lib/redis_assist/base.rb', line 447

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



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

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

#new_record?Boolean

Returns:

  • (Boolean)


433
434
435
# File 'lib/redis_assist/base.rb', line 433

def new_record?
  !!new_record
end

#read_attribute(name) ⇒ Object

Transform and read a standard attribute



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

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



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

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



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

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



437
438
439
# File 'lib/redis_assist/base.rb', line 437

def redis
  self.class.redis
end

#saveObject



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
381
382
383
384
385
386
387
# File 'lib/redis_assist/base.rb', line 340

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



389
390
391
392
# File 'lib/redis_assist/base.rb', line 389

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

#saved?Boolean

Returns:

  • (Boolean)


306
307
308
# File 'lib/redis_assist/base.rb', line 306

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

#undeleteObject



424
425
426
427
428
429
430
431
# File 'lib/redis_assist/base.rb', line 424

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



311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/redis_assist/base.rb', line 311

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)


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

def valid?
  invoke_callback(:before_validation)
  super
end

#write_attribute(name, val) ⇒ Object

Transform and write a standard attribute value



285
286
287
288
289
290
291
292
# File 'lib/redis_assist/base.rb', line 285

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

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

#write_hash(name, val) ⇒ Object

Transform and write a hash attribute



301
302
303
304
# File 'lib/redis_assist/base.rb', line 301

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



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

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