Class: Sohm::Model

Inherits:
Object
  • Object
show all
Defined in:
lib/sohm.rb,
lib/sohm/json.rb

Overview

The base class for all your models. In order to better understand it, here is a semi-realtime explanation of the details involved when creating a User instance.

Example:

class User < Sohm::Model
  attribute :name
  index :name

  attribute :email
  unique :email

  counter :points

  set :posts, :Post
end

u = User.create(:name => "John", :email => "[email protected]")
u.incr :points
u.posts.add(Post.create)

When you execute ‘User.create(…)`, you run the following Redis commands:

# Generate an ID
INCR User:id

# Add the newly generated ID, (let's assume the ID is 1).
SADD User:all 1

# Store the unique index
HSET User:uniques:email foo@bar.com 1

# Store the name index
SADD User:indices:name:John 1

# Store the HASH
HMSET User:1 name John email foo@bar.com

Next we increment points:

HINCR User:1:_counters points 1

And then we add a Post to the posts set. (For brevity, let’s assume the Post created has an ID of 1).

SADD User:1:posts 1

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(atts = {}) ⇒ Model

Initialize a model using a dictionary of attributes.

Example:

u = User.new(:name => "John")


1094
1095
1096
1097
1098
1099
# File 'lib/sohm.rb', line 1094

def initialize(atts = {})
  @attributes = {}
  @serial_attributes = {}
  @serial_attributes_changed = false
  update_attributes(atts)
end

Instance Attribute Details

#cas_tokenObject

Returns the value of attribute cas_token.



1121
1122
1123
# File 'lib/sohm.rb', line 1121

def cas_token
  @cas_token
end

#idObject

Access the ID used to store this model. The ID is used together with the name of the class in order to form the Redis key.

Example:

class User < Sohm::Model; end

u = User.create
u.id
# => 1

u.key
# => User:1

Raises:



1115
1116
1117
1118
# File 'lib/sohm.rb', line 1115

def id
  raise MissingID if not defined?(@id)
  @id
end

Class Method Details

.[](id) ⇒ Object

Retrieve a record by ID.

Example:

u = User.create
u == User[u.id]
# =>  true


731
732
733
# File 'lib/sohm.rb', line 731

def self.[](id)
  new(:id => id).load! if id && exists?(id)
end

.attribute(name, cast = nil) ⇒ Object

The bread and butter macro of all models. Basically declares persisted attributes. All attributes are stored on the Redis hash.

class User < Sohm::Model
  attribute :name
end

user = User.new(name: "John")
user.name
# => "John"

user.name = "Jane"
user.name
# => "Jane"

A lambda can be passed as a second parameter to add typecasting support to the attribute.

class User < Sohm::Model
  attribute :age, ->(x) { x.to_i }
end

user = User.new(age: 100)

user.age
# => 100

user.age.kind_of?(Integer)
# => true

Check rubydoc.info/github/cyx/ohm-contrib#Ohm__DataTypes to see more examples about the typecasting feature.



989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
# File 'lib/sohm.rb', line 989

def self.attribute(name, cast = nil)
  if serial_attributes.include?(name)
    raise ArgumentError,
          "#{name} is already used as a serial attribute."
  end
  attributes << name unless attributes.include?(name)

  if cast
    define_method(name) do
      cast[@attributes[name]]
    end
  else
    define_method(name) do
      @attributes[name]
    end
  end

  define_method(:"#{name}=") do |value|
    @attributes[name] = value
  end
end

.attributesObject



1384
1385
1386
# File 'lib/sohm.rb', line 1384

def self.attributes
  @attributes
end

.collection(name, model, reference = to_reference) ⇒ Object

A macro for defining a method which basically does a find.

Example:

class Post < Sohm::Model
  reference :user, :User
end

class User < Sohm::Model
  collection :posts, :Post
end

# is the same as

class User < Sohm::Model
  def posts
    Post.find(:user_id => self.id)
  end
end


894
895
896
897
898
899
# File 'lib/sohm.rb', line 894

def self.collection(name, model, reference = to_reference)
  define_method name do
    model = Utils.const(self.class, model)
    model.find(:"#{reference}_id" => id)
  end
end

.counter(name) ⇒ Object

Declare a counter. All the counters are internally stored in a different Redis hash, independent from the one that stores the model attributes. Counters are updated with the incr and decr methods, which interact directly with Redis. Their value can’t be assigned as with regular attributes.

Example:

class User < Sohm::Model
  counter :points
end

u = User.create
u.incr :points

u.points
# => 1

Note: You can’t use counters until you save the model. If you try to do it, you’ll receive an Sohm::MissingID error.



1061
1062
1063
1064
1065
1066
1067
1068
1069
# File 'lib/sohm.rb', line 1061

def self.counter(name)
  counters << name unless counters.include?(name)

  define_method(name) do
    return 0 if new?

    redis.call("HGET", key[:_counters], name).to_i
  end
end

.countersObject



1376
1377
1378
# File 'lib/sohm.rb', line 1376

def self.counters
  @counters
end

.create(atts = {}) ⇒ Object

Create a new model, notice that under Sohm’s circumstances, this is no longer a syntactic sugar for Model.new(atts).save



1078
1079
1080
# File 'lib/sohm.rb', line 1078

def self.create(atts = {})
  new(atts).save
end

.exists?(id) ⇒ Boolean

Check if the ID exists within <Model>:all.



752
753
754
# File 'lib/sohm.rb', line 752

def self.exists?(id)
  redis.call("EXISTS", key[id]) == 1
end

.fetch(ids) ⇒ Object

Retrieve a set of models given an array of IDs.

Example:

User.fetch([1, 2, 3])


810
811
812
# File 'lib/sohm.rb', line 810

def self.fetch(ids)
  all.fetch(ids)
end

.filters(dict) ⇒ Object



1392
1393
1394
1395
1396
1397
1398
1399
1400
# File 'lib/sohm.rb', line 1392

def self.filters(dict)
  unless dict.kind_of?(Hash)
    raise ArgumentError,
      "You need to supply a hash with filters. " +
      "If you want to find by ID, use #{self}[id] instead."
  end

  dict.map { |k, v| to_indices(k, v) }.flatten
end

.find(dict) ⇒ Object

Find values in indexed fields.

Example:

class User < Sohm::Model
  attribute :email

  attribute :name
  index :name

  attribute :status
  index :status

  index :provider
  index :tag

  def provider
    email[/@(.*?).com/, 1]
  end

  def tag
    ["ruby", "python"]
  end
end

u = User.create(name: "John", status: "pending", email: "[email protected]")
User.find(provider: "me", name: "John", status: "pending").include?(u)
# => true

User.find(:tag => "ruby").include?(u)
# => true

User.find(:tag => "python").include?(u)
# => true

User.find(:tag => ["ruby", "python"]).include?(u)
# => true


794
795
796
797
798
799
800
801
802
# File 'lib/sohm.rb', line 794

def self.find(dict)
  keys = filters(dict)

  if keys.size == 1
    Sohm::Set.new(keys.first, key, self)
  else
    Sohm::MultiSet.new(key, self, Command.new(:sinterstore, *keys))
  end
end

.index(attribute) ⇒ Object

Index any method on your model. Once you index a method, you can use it in find statements.



816
817
818
# File 'lib/sohm.rb', line 816

def self.index(attribute)
  indices << attribute unless indices.include?(attribute)
end

.indicesObject



1372
1373
1374
# File 'lib/sohm.rb', line 1372

def self.indices
  @indices
end

.inherited(subclass) ⇒ Object

Workaround to JRuby’s concurrency problem



1364
1365
1366
1367
1368
1369
1370
# File 'lib/sohm.rb', line 1364

def self.inherited(subclass)
  subclass.instance_variable_set(:@indices, [])
  subclass.instance_variable_set(:@counters, [])
  subclass.instance_variable_set(:@tracked, [])
  subclass.instance_variable_set(:@attributes, [])
  subclass.instance_variable_set(:@serial_attributes, [])
end

.keyObject

Returns the namespace for all the keys generated using this model.

Example:

class User < Sohm::Model
end

User.key == "User"
User.key.kind_of?(String)
# => true

User.key.kind_of?(Nido)
# => true

To find out more about Nido, see:

http://github.com/soveran/nido


719
720
721
# File 'lib/sohm.rb', line 719

def self.key
  Nido.new(self.name)
end

.list(name, model) ⇒ Object

Declare an Sohm::List with the given name.

Example:

class Comment < Sohm::Model
end

class Post < Sohm::Model
  list :comments, :Comment
end

p = Post.create
p.comments.push(Comment.create)
p.comments.unshift(Comment.create)
p.comments.size == 2
# => true

Note: You can’t use the list until you save the model. If you try to do it, you’ll receive an Sohm::MissingID error.



865
866
867
868
869
870
871
872
873
# File 'lib/sohm.rb', line 865

def self.list(name, model)
  track(name)

  define_method name do
    model = Utils.const(self.class, model)

    Sohm::List.new(key[name], model.key, model)
  end
end

.mutexObject



694
695
696
# File 'lib/sohm.rb', line 694

def self.mutex
  Sohm.mutex
end

.redisObject



690
691
692
# File 'lib/sohm.rb', line 690

def self.redis
  defined?(@redis) ? @redis : Sohm.redis
end

.redis=(redis) ⇒ Object



686
687
688
# File 'lib/sohm.rb', line 686

def self.redis=(redis)
  @redis = redis
end

.reference(name, model) ⇒ Object

A macro for defining an attribute, an index, and an accessor for a given model.

Example:

class Post < Sohm::Model
  reference :user, :User
end

# It's the same as:

class Post < Sohm::Model
  attribute :user_id
  index :user_id

  def user
    User[user_id]
  end

  def user=(user)
    self.user_id = user.id
  end

  def user_id=(user_id)
    self.user_id = user_id
  end
end


929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
# File 'lib/sohm.rb', line 929

def self.reference(name, model)
  reader = :"#{name}_id"
  writer = :"#{name}_id="

  attributes << reader unless attributes.include?(reader)

  index reader

  define_method(reader) do
    @attributes[reader]
  end

  define_method(writer) do |value|
    @attributes[reader] = value
  end

  define_method(:"#{name}=") do |value|
    send(writer, value ? value.id : nil)
  end

  define_method(name) do
    model = Utils.const(self.class, model)
    model[send(reader)]
  end
end

.serial_attribute(name, cast = nil) ⇒ Object

Attributes that require CAS property



1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
# File 'lib/sohm.rb', line 1012

def self.serial_attribute(name, cast = nil)
  if attributes.include?(name)
    raise ArgumentError,
          "#{name} is already used as a normal attribute."
  end
  serial_attributes << name unless serial_attributes.include?(name)

  if cast
    define_method(name) do
      # NOTE: This is a temporary solution, since we might use
      # composite objects (such as arrays), which won't always
      # do a reset
      @serial_attributes_changed = true
      cast[@serial_attributes[name]]
    end
  else
    define_method(name) do
      @serial_attributes_changed = true
      @serial_attributes[name]
    end
  end

  define_method(:"#{name}=") do |value|
    @serial_attributes_changed = true
    @serial_attributes[name] = value
  end
end

.serial_attributesObject



1388
1389
1390
# File 'lib/sohm.rb', line 1388

def self.serial_attributes
  @serial_attributes
end

.set(name, model) ⇒ Object

Declare an Sohm::Set with the given name.

Example:

class User < Sohm::Model
  set :posts, :Post
end

u = User.create
u.posts.empty?
# => true

Note: You can’t use the set until you save the model. If you try to do it, you’ll receive an Sohm::MissingID error.



835
836
837
838
839
840
841
842
843
# File 'lib/sohm.rb', line 835

def self.set(name, model)
  track(name)

  define_method name do
    model = Utils.const(self.class, model)

    Sohm::MutableSet.new(key[name], model.key, model)
  end
end

.synchronize(&block) ⇒ Object



698
699
700
# File 'lib/sohm.rb', line 698

def self.synchronize(&block)
  mutex.synchronize(&block)
end

.to_indices(att, val) ⇒ Object

Raises:



1402
1403
1404
1405
1406
1407
1408
1409
1410
# File 'lib/sohm.rb', line 1402

def self.to_indices(att, val)
  raise IndexNotFound unless indices.include?(att)

  if val.kind_of?(Enumerable)
    val.map { |v| key[:_indices][att][v] }
  else
    [key[:_indices][att][val]]
  end
end

.to_procObject

Retrieve a set of models given an array of IDs.

Example:

ids = [1, 2, 3]
ids.map(&User)

Note: The use of this should be a last resort for your actual application runtime, or for simply debugging in your console. If you care about performance, you should pipeline your reads. For more information checkout the implementation of Sohm::List#fetch.



747
748
749
# File 'lib/sohm.rb', line 747

def self.to_proc
  lambda { |id| self[id] }
end

.to_referenceObject



1356
1357
1358
1359
1360
1361
# File 'lib/sohm.rb', line 1356

def self.to_reference
  name.to_s.
    match(/^(?:.*::)*(.*)$/)[1].
    gsub(/([a-z\d])([A-Z])/, '\1_\2').
    downcase.to_sym
end

.track(name) ⇒ Object

Keep track of key[name] and remove when deleting the object.



1072
1073
1074
# File 'lib/sohm.rb', line 1072

def self.track(name)
  tracked << name unless tracked.include?(name)
end

.trackedObject



1380
1381
1382
# File 'lib/sohm.rb', line 1380

def self.tracked
  @tracked
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?

Check for equality by doing the following assertions:

  1. That the passed model is of the same type.

  2. That they represent the same Redis key.



1128
1129
1130
1131
1132
# File 'lib/sohm.rb', line 1128

def ==(other)
  other.kind_of?(model) && other.key == key
rescue MissingID
  false
end

#attributesObject

Returns a hash of the attributes with their names as keys and the values of the attributes as values. It doesn’t include the ID of the model.

Example:

class User < Sohm::Model
  attribute :name
end

u = User.create(:name => "John")
u.attributes
# => { :name => "John" }


1202
1203
1204
# File 'lib/sohm.rb', line 1202

def attributes
  @attributes
end

#decr(att, count = 1) ⇒ Object

Decrement a counter atomically. Internally uses HINCRBY.



1167
1168
1169
# File 'lib/sohm.rb', line 1167

def decr(att, count = 1)
  incr(att, -count)
end

#deleteObject

Delete the model, including all the following keys:

  • <Model>:<id>

  • <Model>:<id>:_counters

  • <Model>:<id>:<set name>

If the model has uniques or indices, they’re also cleaned up.



1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
# File 'lib/sohm.rb', line 1293

def delete
  memo_key = key["_indices"]
  commands = [["DEL", key], ["DEL", memo_key], ["DEL", key["_counters"]]]
  index_list = redis.call("SMEMBERS", memo_key)
  index_list.each do |index_key|
    commands << ["SREM", index_key, id]
  end
  model.tracked.each do |tracked_key|
    commands << ["DEL", key[tracked_key]]
  end

  model.synchronize do
    commands.each do |command|
      redis.queue(*command)
    end
    redis.commit
  end

  return self
end

#hashObject

Return a value that allows the use of models as hash keys.

Example:

h = {}

u = User.new

h[:u] = u
h[:u] == u
# => true


1183
1184
1185
# File 'lib/sohm.rb', line 1183

def hash
  new? ? super : key.hash
end

#incr(att, count = 1) ⇒ Object

Increment a counter atomically. Internally uses HINCRBY.



1162
1163
1164
# File 'lib/sohm.rb', line 1162

def incr(att, count = 1)
  redis.call("HINCRBY", key[:_counters], att, count)
end

#keyObject

Returns the namespace for the keys generated using this model. Check Sohm::Model.key documentation for more details.



1084
1085
1086
# File 'lib/sohm.rb', line 1084

def key
  model.key[id]
end

#load!Object

Preload all the attributes of this model from Redis. Used internally by Model::[].



1136
1137
1138
1139
1140
# File 'lib/sohm.rb', line 1136

def load!
  update_attributes(Utils.dict(redis.call("HGETALL", key))) if id
  @serial_attributes_changed = false
  return self
end

#new?Boolean

Returns true if the model is not persisted. Otherwise, returns false.

Example:

class User < Sohm::Model
  attribute :name
end

u = User.new(:name => "John")
u.new?
# => true

u.save
u.new?
# => false


1157
1158
1159
# File 'lib/sohm.rb', line 1157

def new?
  !(defined?(@id) && model.exists?(id))
end

#saveObject

Persist the model attributes and update indices and unique indices. The ‘counter`s and `set`s are not touched during save.

Example:

class User < Sohm::Model
  attribute :name
end

u = User.new(:name => "John").save
u.kind_of?(User)
# => true


1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
# File 'lib/sohm.rb', line 1259

def save
  if serial_attributes_changed
    response = script(LUA_SAVE, 1, key,
      sanitize_attributes(serial_attributes).to_msgpack,
      cas_token)

    if response.is_a?(RuntimeError)
      if response.message =~ /cas_error/
        raise CasViolation
      else
        raise response
      end
    end

    @cas_token = response
    @serial_attributes_changed = false
  end

  redis.call("HSET", key, "_ndata",
             sanitize_attributes(attributes).to_msgpack)

  refresh_indices

  return self
end

#script(file, *args) ⇒ Object

Run lua scripts and cache the sha in order to improve successive calls.



1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
# File 'lib/sohm.rb', line 1316

def script(file, *args)
  response = nil

  if Sohm.enable_evalsha
    response = redis.call("EVALSHA", LUA_SAVE_DIGEST, *args)
    if response.is_a?(RuntimeError)
      if response.message =~ /NOSCRIPT/
        response = nil
      end
    end
  end

  response ? response : redis.call("EVAL", LUA_SAVE, *args)
end

#serial_attributesObject



1206
1207
1208
# File 'lib/sohm.rb', line 1206

def serial_attributes
  @serial_attributes
end

#to_hashObject

Export the ID of the model. The approach of Ohm is to whitelist public attributes, as opposed to exporting each (possibly sensitive) attribute.

Example:

class User < Sohm::Model
  attribute :name
end

u = User.create(:name => "John")
u.to_hash
# => { :id => "1" }

In order to add additional attributes, you can override to_hash:

class User < Sohm::Model
  attribute :name

  def to_hash
    super.merge(:name => name)
  end
end

u = User.create(:name => "John")
u.to_hash
# => { :id => "1", :name => "John" }


1238
1239
1240
1241
1242
1243
# File 'lib/sohm.rb', line 1238

def to_hash
  attrs = {}
  attrs[:id] = id unless new?

  return attrs
end

#to_json(*args) ⇒ Object

Export a JSON representation of the model by encoding to_hash.



6
7
8
# File 'lib/sohm/json.rb', line 6

def to_json(*args)
  to_hash.to_json(*args)
end

#update(attributes) ⇒ Object

Update the model attributes and call save.

Example:

User[1].update(:name => "John")

# It's the same as:

u = User[1]
u.update_attributes(:name => "John")
u.save


1343
1344
1345
1346
# File 'lib/sohm.rb', line 1343

def update(attributes)
  update_attributes(attributes)
  save
end

#update_attributes(atts) ⇒ Object

Write the dictionary of key-value pairs to the model.



1349
1350
1351
# File 'lib/sohm.rb', line 1349

def update_attributes(atts)
  unpack_attrs(atts).each { |att, val| send(:"#{att}=", val) }
end