class Sohm::Model

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 => "foo@bar.com")
u.incr :points
u.posts.add(Post.create)

Attributes

cas_token[RW]
id[W]
serial_attributes_changed[R]

Public Class Methods

[](id) click to toggle source

Retrieve a record by ID.

Example:

u = User.create
u == User[u.id]
# =>  true
# File lib/sohm.rb, line 553
def self.[](id)
  new(:id => id).load! if id && exists?(id)
end
attribute(name, cast = nil) click to toggle source

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.

# File lib/sohm.rb, line 811
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
collection(name, model, reference = to_reference) click to toggle source

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
# File lib/sohm.rb, line 716
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) click to toggle source

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.

# File lib/sohm.rb, line 883
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
create(atts = {}) click to toggle source

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

# File lib/sohm.rb, line 900
def self.create(atts = {})
  new(atts).save
end
exists?(id) click to toggle source

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

# File lib/sohm.rb, line 574
def self.exists?(id)
  redis.call("EXISTS", key[id]) == 1
end
fetch(ids) click to toggle source

Retrieve a set of models given an array of IDs.

Example:

User.fetch([1, 2, 3])
# File lib/sohm.rb, line 632
def self.fetch(ids)
  all.fetch(ids)
end
find(dict) click to toggle source

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: "foo@me.com")
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

Due to restrictions in Codis, we only support single-index query. If you want to query based on multiple fields, you can make an index based on all the fields.

# File lib/sohm.rb, line 616
def self.find(dict)
  keys = filters(dict)

  if keys.size == 1
    Sohm::Set.new(keys.first, key, self)
  else
    raise NotSupported
  end
end
index(attribute) click to toggle source

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

# File lib/sohm.rb, line 638
def self.index(attribute)
  indices << attribute unless indices.include?(attribute)
end
key() click to toggle source

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
# File lib/sohm.rb, line 541
def self.key
  Nido.new(self.name)
end
list(name, model) click to toggle source

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.

# File lib/sohm.rb, line 687
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
mutex() click to toggle source
# File lib/sohm.rb, line 516
def self.mutex
  Sohm.mutex
end
new(atts = {}) click to toggle source

Initialize a model using a dictionary of attributes.

Example:

u = User.new(:name => "John")
# File lib/sohm.rb, line 916
def initialize(atts = {})
  @attributes = {}
  @serial_attributes = {}
  @serial_attributes_changed = false
  update_attributes(atts)
end
redis() click to toggle source
# File lib/sohm.rb, line 502
def self.redis
  defined?(@redis) ? @redis : Sohm.redis
end
redis=(redis) click to toggle source
# File lib/sohm.rb, line 498
def self.redis=(redis)
  @redis = redis
end
reference(name, model) click to toggle source

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
# File lib/sohm.rb, line 751
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
refresh_indices_inline() click to toggle source
# File lib/sohm.rb, line 510
def self.refresh_indices_inline
  defined?(@refresh_indices_inline) ?
    @refresh_indices_inline :
    Sohm.refresh_indices_inline
end
refresh_indices_inline=(refresh_indices_inline) click to toggle source
# File lib/sohm.rb, line 506
def self.refresh_indices_inline=(refresh_indices_inline)
  @refresh_indices_inline = refresh_indices_inline
end
serial_attribute(name, cast = nil) click to toggle source

Attributes that require CAS property

# File lib/sohm.rb, line 834
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
set(name, model) click to toggle source

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.

# File lib/sohm.rb, line 657
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) click to toggle source
# File lib/sohm.rb, line 520
def self.synchronize(&block)
  mutex.synchronize(&block)
end
to_proc() click to toggle source

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.

# File lib/sohm.rb, line 569
def self.to_proc
  lambda { |id| self[id] }
end
track(name) click to toggle source

Keep track of `key` and remove when deleting the object.

# File lib/sohm.rb, line 894
def self.track(name)
  tracked << name unless tracked.include?(name)
end

Protected Class Methods

attributes() click to toggle source
# File lib/sohm.rb, line 1257
def self.attributes
  @attributes
end
counters() click to toggle source
# File lib/sohm.rb, line 1249
def self.counters
  @counters
end
filters(dict) click to toggle source
# File lib/sohm.rb, line 1265
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
indices() click to toggle source
# File lib/sohm.rb, line 1245
def self.indices
  @indices
end
inherited(subclass) click to toggle source

Workaround to JRuby's concurrency problem

# File lib/sohm.rb, line 1237
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
serial_attributes() click to toggle source
# File lib/sohm.rb, line 1261
def self.serial_attributes
  @serial_attributes
end
to_indices(att, val) click to toggle source
# File lib/sohm.rb, line 1275
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_reference() click to toggle source
# File lib/sohm.rb, line 1229
def self.to_reference
  name.to_s.
    match(/^(?:.*::)*(.*)$/)[1].
    gsub(/([a-z\d])([A-Z])/, '\1_\2').
    downcase.to_sym
end
tracked() click to toggle source
# File lib/sohm.rb, line 1253
def self.tracked
  @tracked
end

Public Instance Methods

==(other) click to toggle source

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.

# File lib/sohm.rb, line 942
def ==(other)
  other.kind_of?(model) && other.key == key
rescue MissingID
  false
end
Also aliased as: eql?
attributes() click to toggle source

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" }
# File lib/sohm.rb, line 1016
def attributes
  @attributes
end
counters() click to toggle source
# File lib/sohm.rb, line 1024
def counters
  hash = {}
  self.class.counters.each do |name|
    hash[name] = 0
  end
  return hash if new?
  redis.call("HGETALL", key[:_counters]).each_slice(2).each do |pair|
    hash[pair[0].to_sym] = pair[1].to_i
  end
  hash
end
decr(att, count = 1) click to toggle source

Decrement a counter atomically. Internally uses HINCRBY.

# File lib/sohm.rb, line 981
def decr(att, count = 1)
  incr(att, -count)
end
delete() click to toggle source

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.

# File lib/sohm.rb, line 1166
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
eql?(other)
Alias for: ==
hash() click to toggle source

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

Example:

h = {}

u = User.new

h[:u] = u
h[:u] == u
# => true
Calls superclass method
# File lib/sohm.rb, line 997
def hash
  new? ? super : key.hash
end
id() click to toggle source

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.

Different from ohm, id must be provided by the user in sohm, if you want to use auto-generated id, you can include Sohm::AutoId module.

# File lib/sohm.rb, line 929
def id
  raise MissingID if not defined?(@id)
  @id
end
incr(att, count = 1) click to toggle source

Increment a counter atomically. Internally uses HINCRBY.

# File lib/sohm.rb, line 976
def incr(att, count = 1)
  redis.call("HINCRBY", key[:_counters], att, count)
end
key() click to toggle source

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

# File lib/sohm.rb, line 906
def key
  model.key[id]
end
load!() click to toggle source

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

# File lib/sohm.rb, line 950
def load!
  update_attributes(Utils.dict(redis.call("HGETALL", key))) if id
  @serial_attributes_changed = false
  return self
end
new?() click to toggle source

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
# File lib/sohm.rb, line 971
def new?
  !(defined?(@id) && model.exists?(id))
end
refresh_indices() click to toggle source

Refresh model indices

# File lib/sohm.rb, line 1115
def refresh_indices
  memo_key = key["_indices"]
  # Add new indices first
  commands = fetch_indices.each_pair.map do |field, vals|
    vals.map do |val|
      index_key = model.key["_indices"][field][val]
      [["SADD", memo_key, index_key], ["SADD", index_key, id]]
    end
  end.flatten(2)

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

  # Remove old indices
  index_set = ::Set.new(redis.call("SMEMBERS", memo_key))
  # Here we are fetching the latest model to avoid concurrency issue
  valid_list = model[id].send(:fetch_indices).each_pair.map do |field, vals|
    vals.map do |val|
      model.key["_indices"][field][val]
    end
  end.flatten(1)
  valid_set = ::Set.new(valid_list)
  diff_set = index_set - valid_set
  if diff_set.size > 0
    diff_list = diff_set.to_a
    commands = diff_list.map do |key|
      ["SREM", key, id]
    end + [["SREM", memo_key] + diff_list]

    model.synchronize do
      commands.each do |command|
        redis.queue(*command)
      end
      redis.commit
    end
  end
  true
end
save() click to toggle source

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
# File lib/sohm.rb, line 1085
def save
  if serial_attributes_changed
    response = script(LUA_SAVE, 1, key,
      sanitize_attributes(serial_attributes).to_msgpack,
      cas_token,
      sanitize_attributes(attributes).to_msgpack)

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

    @cas_token = response
    @serial_attributes_changed = false
  else
    redis.call("HSET", key, "_ndata",
               sanitize_attributes(attributes).to_msgpack)
  end

  if model.refresh_indices_inline
    refresh_indices
  end

  return self
end
script(file, *args) click to toggle source

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

# File lib/sohm.rb, line 1189
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_attributes() click to toggle source
# File lib/sohm.rb, line 1020
def serial_attributes
  @serial_attributes
end
to_hash() click to toggle source

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" }
# File lib/sohm.rb, line 1064
def to_hash
  attrs = {}
  attrs[:id] = id unless new?

  return attrs
end
to_json(*args) click to toggle source

Export a JSON representation of the model by encoding `to_hash`.

# File lib/sohm/json.rb, line 6
def to_json(*args)
  to_hash.to_json(*args)
end
update(attributes) click to toggle source

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
# File lib/sohm.rb, line 1216
def update(attributes)
  update_attributes(attributes)
  save
end
update_attributes(atts) click to toggle source

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

# File lib/sohm.rb, line 1222
def update_attributes(atts)
  unpack_attrs(atts).each { |att, val| send(:"#{att}=", val) }
end

Protected Instance Methods

fetch_indices() click to toggle source
# File lib/sohm.rb, line 1285
def fetch_indices
  indices = {}
  model.indices.each { |field| indices[field] = Array(send(field)) }
  indices
end
model() click to toggle source
# File lib/sohm.rb, line 1313
def model
  self.class
end
redis() click to toggle source
# File lib/sohm.rb, line 1317
def redis
  model.redis
end
sanitize_attributes(attributes) click to toggle source
# File lib/sohm.rb, line 1309
def sanitize_attributes(attributes)
  attributes.select { |key, val| val }
end
unpack_attrs(attrs) click to toggle source

Unpack hash returned by redis, which contains _cas, _sdata, _ndata columns

# File lib/sohm.rb, line 1293
def unpack_attrs(attrs)
  if ndata = attrs.delete("_ndata")
    attrs.merge!(MessagePack.unpack(ndata))
  end

  if sdata = attrs.delete("_sdata")
    attrs.merge!(MessagePack.unpack(sdata))
  end

  if cas_token = attrs.delete("_cas")
    attrs["cas_token"] = cas_token
  end

  attrs
end