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
Public Class Methods
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
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
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
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 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
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
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 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 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
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
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
# File lib/sohm.rb, line 516 def self.mutex Sohm.mutex end
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
# File lib/sohm.rb, line 502 def self.redis defined?(@redis) ? @redis : Sohm.redis end
# File lib/sohm.rb, line 498 def self.redis=(redis) @redis = redis end
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
# File lib/sohm.rb, line 510 def self.refresh_indices_inline defined?(@refresh_indices_inline) ? @refresh_indices_inline : Sohm.refresh_indices_inline end
# File lib/sohm.rb, line 506 def self.refresh_indices_inline=(refresh_indices_inline) @refresh_indices_inline = refresh_indices_inline end
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
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
# File lib/sohm.rb, line 520 def self.synchronize(&block) mutex.synchronize(&block) end
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
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
# File lib/sohm.rb, line 1257 def self.attributes @attributes end
# File lib/sohm.rb, line 1249 def self.counters @counters end
# 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
# File lib/sohm.rb, line 1245 def self.indices @indices end
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
# File lib/sohm.rb, line 1261 def self.serial_attributes @serial_attributes end
# 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
# 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
# File lib/sohm.rb, line 1253 def self.tracked @tracked end
Public Instance Methods
Check for equality by doing the following assertions:
-
That the passed model is of the same type.
-
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
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
# 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
Decrement a counter atomically. Internally uses HINCRBY.
# File lib/sohm.rb, line 981 def decr(att, count = 1) incr(att, -count) end
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
Return a value that allows the use of models as hash keys.
Example:
h = {} u = User.new h[:u] = u h[:u] == u # => true
# File lib/sohm.rb, line 997 def hash new? ? super : key.hash end
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
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
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
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
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 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
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
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
# File lib/sohm.rb, line 1020 def serial_attributes @serial_attributes end
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
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 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
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
# File lib/sohm.rb, line 1285 def fetch_indices indices = {} model.indices.each { |field| indices[field] = Array(send(field)) } indices end
# File lib/sohm.rb, line 1313 def model self.class end
# File lib/sohm.rb, line 1317 def redis model.redis end
# File lib/sohm.rb, line 1309 def sanitize_attributes(attributes) attributes.select { |key, val| val } end
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