lib/ohm.rb in ohm-0.1.5 vs lib/ohm.rb in ohm-1.0.0.alpha1
- old
+ new
@@ -1,2000 +1,1231 @@
# encoding: UTF-8
-require "base64"
-require "redis"
require "nest"
+require "redis"
+require "securerandom"
+require "scrivener"
+require "ohm/transaction"
-require File.join(File.dirname(__FILE__), "ohm", "pattern")
-require File.join(File.dirname(__FILE__), "ohm", "validations")
-require File.join(File.dirname(__FILE__), "ohm", "compat-1.8.6")
-require File.join(File.dirname(__FILE__), "ohm", "key")
-
module Ohm
- # Provides access to the _Redis_ database. It is highly recommended that you
- # use this sparingly, and only if you really know what you're doing.
+ # All of the known errors in Ohm can be traced back to one of these
+ # exceptions.
#
- # The better way to access the _Redis_ database and do raw _Redis_
- # commands would be one of the following:
+ # MissingID:
#
- # 1. Use {Ohm::Model.key} or {Ohm::Model#key}. So if the name of your
- # model is *Post*, it would be *Post.key* or the protected method
- # *#key* which should be used within your *Post* model.
+ # Comment.new.id # => Error
+ # Comment.new.key # => Error
#
- # 2. Use {Ohm::Model.db} or {Ohm::Model#db}. Although this is also
- # accessible, it is much cleaner and terse to use {Ohm::Model.key}.
+ # Solution: you need to save your model first.
#
- # @example
+ # IndexNotFound:
#
- # class Post < Ohm::Model
- # def comment_ids
- # key[:comments].zrange(0, -1)
- # end
+ # Comment.find(foo: "Bar") # => Error
#
- # def add_comment_id(id)
- # key[:comments].zadd(Time.now.to_i, id)
- # end
+ # Solution: add an index with `Comment.index :foo`.
#
- # def remove_comment_id(id)
- # # Let's use the db style here just to demonstrate.
- # db.zrem key[:comments], id
- # end
- # end
+ # UniqueIndexViolation:
#
- # Post.key[:latest].sadd(1)
- # Post.key[:latest].smembers == ["1"]
- # # => true
+ # Raised when trying to save an object with a `unique` index for
+ # which the value already exists.
#
- # Post.key[:latest] == "Post:latest"
- # # => true
+ # Solution: rescue `Ohm::UniqueIndexViolation` during save, but
+ # also, do some validations even before attempting to save.
#
- # p = Post.create
- # p.comment_ids == []
- # # => true
- #
- # p.add_comment_id(101)
- # p.comment_ids == ["101"]
- # # => true
- #
- # p.remove_comment_id(101)
- # p.comment_ids == []
- # # => true
- def self.redis
- threaded[:redis] ||= connection(*options)
+ class Error < StandardError; end
+ class MissingID < Error; end
+ class IndexNotFound < Error; end
+ class UniqueIndexViolation < Error; end
+
+ # Instead of monkey patching Kernel or trying to be clever, it's
+ # best to confine all the helper methods in a Utils module.
+ module Utils
+
+ # Used by: `attribute`, `counter`, `set`, `reference`,
+ # `collection`.
+ #
+ # Employed as a solution to avoid `NameError` problems when trying
+ # to load models referring to other models not yet loaded.
+ #
+ # Example:
+ #
+ # class Comment < Ohm::Model
+ # reference :user, User # NameError undefined constant User.
+ # end
+ #
+ # Instead of relying on some clever `const_missing` hack, we can
+ # simply use a Symbol.
+ #
+ # class Comment < Ohm::Model
+ # reference :user, :User
+ # end
+ #
+ def self.const(context, name)
+ case name
+ when Symbol then context.const_get(name)
+ else name
+ end
+ end
end
- # Assign a new _Redis_ connection. Internally used by {Ohm.connect}
- # to clear the cached _Redis_ instance.
- #
- # If you're looking to change the connection or reconnect with different
- # parameters, try {Ohm.connect} or {Ohm::Model.connect}.
- # @see connect
- # @see Model.connect
- # @param connection [Redis] an instance created using `Redis.new`.
- def self.redis=(connection)
- threaded[:redis] = connection
+ class Connection
+ attr_accessor :context
+ attr_accessor :options
+
+ def initialize(context = :main, options = {})
+ @context = context
+ @options = options
+ end
+
+ def reset!
+ threaded[context] = nil
+ end
+
+ def start(options = {})
+ self.options = options
+ self.reset!
+ end
+
+ def redis
+ threaded[context] ||= Redis.connect(options)
+ end
+
+ def threaded
+ Thread.current[:ohm] ||= {}
+ end
end
- # @private Used internally by Ohm for thread safety.
- def self.threaded
- Thread.current[:ohm] ||= {}
+ def self.conn
+ @conn ||= Connection.new
end
- # Connect to a _Redis_ database.
+ # Stores the connection options for the Redis instance.
#
- # It is also worth mentioning that you can pass in a *URI* e.g.
+ # Examples:
#
- # Ohm.connect :url => "redis://127.0.0.1:6379/0"
+ # Ohm.connect(port: 6380, db: 1, host: "10.0.1.1")
+ # Ohm.connect(url: "redis://10.0.1.1:6380/1")
#
- # Note that the value *0* refers to the database number for the given
- # _Redis_ instance.
+ # All of the options are simply passed on to `Redis.connect`.
#
- # Also you can use {Ohm.connect} without any arguments. The behavior will
- # be as follows:
+ def self.connect(options = {})
+ conn.start(options)
+ end
+
+ # Use this if you want to do quick ad hoc redis commands against the
+ # defined Ohm connection.
#
- # # Connect to redis://127.0.0.1:6379/0
- # Ohm.connect
+ # Examples:
#
- # # Connect to redis://10.0.0.100:22222/5
- # ENV["REDIS_URL"] = "redis://10.0.0.100:22222/5"
- # Ohm.connect
+ # Ohm.redis.keys("User:*")
+ # Ohm.redis.set("foo", "bar")
#
- # @param options [{Symbol => #to_s}] An options hash.
- # @see file:README.html#connecting Ohm.connect options documentation.
- #
- # @example Connect to a database in port 6380.
- # Ohm.connect(:port => 6380)
- def self.connect(*options)
- self.redis = nil
- @options = options
+ def self.redis
+ conn.redis
end
- # @private Return a connection to Redis.
- #
- # This is a wrapper around Redis.connect(options)
- def self.connection(*options)
- Redis.connect(*options)
- end
-
- # @private Stores the connection options for Ohm.redis.
- def self.options
- @options = [] unless defined? @options
- @options
- end
-
- # Clear the database. You typically use this only during testing,
- # or when you seed your site.
- #
- # @see http://code.google.com/p/redis/wiki/FlushdbCommand FLUSHDB in the
- # Redis Command Reference.
+ # Wrapper for Ohm.redis.flushdb.
def self.flush
redis.flushdb
end
- # The base class of all *Ohm* errors. Can be used as a catch all for
- # Ohm related errors.
- class Error < StandardError; end
+ # Defines most of the methods used by `Set` and `MultiSet`.
+ module Collection
+ include Enumerable
- # This is the class that you need to extend in order to define your
- # own models.
- #
- # Probably the most magic happening within {Ohm::Model} is the catching
- # of {Ohm::Model.const_missing} exceptions to allow the use of constants
- # even before they are defined.
- #
- # @example
- #
- # class Post < Ohm::Model
- # reference :author, User # no User definition yet!
- # end
- #
- # class User < Ohm::Model
- # end
- #
- # @see Model.const_missing
- class Model
+ # Fetch the data from Redis in one go.
+ def to_a
+ fetch(ids)
+ end
- # Wraps a model name for lazy evaluation.
- class Wrapper < BasicObject
+ def each
+ to_a.each { |e| yield e }
+ end
- # Allows you to use a constant even before it is defined. This solves
- # the issue of having to require inter-project dependencies in a very
- # simple and "magic-free" manner.
- #
- # Example of how it was done before Wrapper existed:
- #
- # require "./app/models/user"
- # require "./app/models/comment"
- #
- # class Post < Ohm::Model
- # reference :author, User
- # list :comments, Comment
- # end
- #
- # Now, you can simply do the following:
- # class Post < Ohm::Model
- # reference :author, User
- # list :comments, Comment
- # end
- #
- # @example
- #
- # module Commenting
- # def self.included(base)
- # base.list :comments, Ohm::Model::Wrapper.new(:Comment) {
- # Object.const_get(:Comment)
- # }
- # end
- # end
- #
- # # In your classes:
- # class Post < Ohm::Model
- # include Commenting
- # end
- #
- # class Comment < Ohm::Model
- # end
- #
- # p = Post.create
- # p.comments.empty?
- # # => true
- #
- # p.comments.push(Comment.create)
- # p.comments.size == 1
- # # => true
- #
- # @param [Symbol, String] name Canonical name of wrapped class.
- # @param [#to_proc] block Closure for getting the name of the constant.
- def initialize(name, &block)
- @name = name
- @caller = ::Kernel.caller[2]
- @block = block
+ def empty?
+ size == 0
+ end
- class << self
- def method_missing(method_id, *args)
- ::Kernel.raise(
- ::NoMethodError,
- "You tried to call %s#%s, but %s is not defined on %s" % [
- @name, method_id, @name, @caller
- ]
- )
- end
- end
- end
+ # Allows you to sort by any field in your model.
+ #
+ # Example:
+ #
+ # class User < Ohm::Model
+ # attribute :name
+ # end
+ #
+ # User.all.sort_by(:name, order: "ALPHA")
+ # User.all.sort_by(:name, order: "ALPHA DESC")
+ # User.all.sort_by(:name, order: "ALPHA DESC", limit: [0, 10])
+ #
+ # Note: This is slower compared to just doing `sort`, specifically
+ # because Redis has to read each individual hash in order to sort
+ # them.
+ #
+ def sort_by(att, options = {})
+ sort(options.merge(by: namespace["*->%s" % att]))
+ end
- # Used as a convenience for wrapping an existing constant into a
- # {Ohm::Model::Wrapper wrapper object}.
- #
- # This is used extensively within the library for points where a user
- # defined class (e.g. _Post_, _User_, _Comment_) is expected.
- #
- # You can also use this if you need to do uncommon things, such as
- # creating your own {Ohm::Model::Set Set}, {Ohm::Model::List List}, etc.
- #
- # (*NOTE:* Keep in mind that the following code is given only as an
- # educational example, and is in no way prescribed as good design.)
- #
- # class User < Ohm::Model
- # end
- #
- # User.create(:id => "1001")
- #
- # Ohm.redis.sadd("myset", 1001)
- #
- # key = Ohm::Key.new("myset", Ohm.redis)
- # set = Ohm::Model::Set.new(key, Ohm::Model::Wrapper.wrap(User))
- #
- # [User[1001]] == set.all.to_a
- # # => true
- #
- # @see http://ohm.keyvalue.org/tutorials/chaining Chaining Ohm Sets
- def self.wrap(object)
- object.class == self ? object : new(object.inspect) { object }
+ # Allows you to sort your models using their IDs. This is much
+ # faster than `sort_by`. If you simply want to get records in
+ # ascending or descending order, then this is the best method to
+ # do that.
+ #
+ # Example:
+ #
+ # class User < Ohm::Model
+ # attribute :name
+ # end
+ #
+ # User.create(name: "John")
+ # User.create(name: "Jane")
+ #
+ # User.all.sort.map(&:id) == ["1", "2"]
+ # # => true
+ #
+ # User.all.sort(order: "ASC").map(&:id) == ["1", "2"]
+ # # => true
+ #
+ # User.all.sort(order: "DESC").map(&:id) == ["2", "1"]
+ # # => true
+ #
+ def sort(options = {})
+ if options.has_key?(:get)
+ options[:get] = namespace["*->%s" % options[:get]]
+ return execute { |key| key.sort(options) }
end
- # Evaluates the passed block in {Ohm::Model::Wrapper#initialize}.
- #
- # @return [Class] The wrapped class.
- def unwrap
- @block.call
- end
+ fetch(execute { |key| key.sort(options) })
+ end
- # Since {Ohm::Model::Wrapper} is a subclass of _BasicObject_ we have
- # to manually declare this.
- #
- # @return [Wrapper]
- def class
- Wrapper
- end
+ # Check if a model is included in this set.
+ #
+ # Example:
+ #
+ # u = User.create
+ #
+ # User.all.include?(u)
+ # # => true
+ #
+ # Note: Ohm simply checks that the model's ID is included in the
+ # set. It doesn't do any form of type checking.
+ #
+ def include?(model)
+ exists?(model.id)
+ end
- # @return [String] A string describing this lazy object.
- def inspect
- "<Wrapper for #{@name} (in #{@caller})>"
- end
+ # Returns the total size of the set using SCARD.
+ def size
+ execute { |key| key.scard }
end
- # Defines the base implementation for all enumerable types in Ohm,
- # which includes {Ohm::Model::Set Sets}, {Ohm::Model::List Lists} and
- # {Ohm::Model::Index Indices}.
- class Collection
- include Enumerable
+ # Syntactic sugar for `sort_by` or `sort` when you only need the
+ # first element.
+ #
+ # Example:
+ #
+ # User.all.first ==
+ # User.all.sort(limit: [0, 1]).first
+ #
+ # User.all.first(by: :name, "ALPHA") ==
+ # User.all.sort_by(:name, order: "ALPHA", limit: [0, 1]).first
+ #
+ def first(options = {})
+ opts = options.dup
+ opts.merge!(limit: [0, 1])
- # An instance of {Ohm::Key}.
- attr :key
-
- # A subclass of {Ohm::Model}.
- attr :model
-
- # @param [Key] key A key which includes a _Redis_ connection.
- # @param [Ohm::Model::Wrapper] model A wrapped subclass of {Ohm::Model}.
- def initialize(key, model)
- @key = key
- @model = model.unwrap
+ if opts[:by]
+ sort_by(opts.delete(:by), opts).first
+ else
+ sort(opts).first
end
+ end
- # Adds an instance of {Ohm::Model} to this collection.
- #
- # @param [#id] model A model with an ID.
- def add(model)
- self << model
- end
+ # Grab all the elements of this set using SMEMBERS.
+ def ids
+ execute { |key| key.smembers }
+ end
- # Sort this collection using the ID by default, or an attribute defined
- # in the elements of this collection.
- #
- # *NOTE:* It is worth mentioning that if you want to sort by a specific
- # attribute instead of an ID, you would probably want to use
- # {Ohm::Model::Collection#sort_by sort_by} instead.
- #
- # @example
- # class Post < Ohm::Model
- # attribute :title
- # end
- #
- # p1 = Post.create(:title => "Alpha")
- # p2 = Post.create(:title => "Beta")
- # p3 = Post.create(:title => "Gamma")
- #
- # [p1, p2, p3] == Post.all.sort.to_a
- # # => true
- #
- # [p3, p2, p1] == Post.all.sort(:order => "DESC").to_a
- # # => true
- #
- # [p1, p2, p3] == Post.all.sort(:by => "Post:*->title",
- # :order => "ASC ALPHA").to_a
- # # => true
- #
- # [p3, p2, p1] == Post.all.sort(:by => "Post:*->title",
- # :order => "DESC ALPHA").to_a
- # # => true
- #
- # @see file:README.html#sorting Sorting in the README.
- # @see http://code.google.com/p/redis/wiki/SortCommand SORT in the
- # Redis Command Reference.
- def sort(options = {})
- return [] unless key.exists
+ # Retrieve a specific element using an ID from this set.
+ #
+ # Example:
+ #
+ # # Let's say we got the ID 1 from a request parameter.
+ # id = 1
+ #
+ # # Retrieve the post if it's included in the user's posts.
+ # post = user.posts[id]
+ #
+ def [](id)
+ model[id] if exists?(id)
+ end
- opts = options.dup
- opts[:start] ||= 0
- opts[:limit] = [opts[:start], opts[:limit]] if opts[:limit]
+ private
+ def exists?(id)
+ execute { |key| key.sismember(id) }
+ end
- key.sort(opts).map(&model)
+ def fetch(ids)
+ arr = model.db.pipelined do
+ ids.each { |id| namespace[id].hgetall }
end
- # Sort the model instances by the given attribute.
- #
- # @example Sorting elements by name:
- #
- # User.create :name => "B"
- # User.create :name => "A"
- #
- # user = User.all.sort_by(:name, :order => "ALPHA").first
- # user.name == "A"
- # # => true
- #
- # @see file:README.html#sorting Sorting in the README.
- def sort_by(att, options = {})
- return [] unless key.exists
+ return [] if arr.nil?
- opts = options.dup
- opts.merge!(:by => model.key["*->#{att}"])
-
- if opts[:get]
- key.sort(opts.merge(:get => model.key["*->#{opts[:get]}"]))
- else
- sort(opts)
- end
+ arr.map.with_index do |atts, idx|
+ model.new(atts.update(id: ids[idx]))
end
+ end
+ end
- # Delete this collection.
- #
- # @example
- #
- # class Post < Ohm::Model
- # list :comments, Comment
- # end
- #
- # class Comment < Ohm::Model
- # end
- #
- # post = Post.create
- # post.comments << Comment.create
- #
- # post.comments.size == 1
- # # => true
- #
- # post.comments.clear
- # post.comments.size == 0
- # # => true
- # @see http://code.google.com/p/redis/wiki/DelCommand DEL in the Redis
- # Command Reference.
- def clear
- key.del
- end
+ class Set < Struct.new(:key, :namespace, :model)
+ include Collection
- # Simultaneously clear and add all models. This wraps all operations
- # in a MULTI EXEC block to make the whole operation atomic.
- #
- # @example
- #
- # class Post < Ohm::Model
- # list :comments, Comment
- # end
- #
- # class Comment < Ohm::Model
- # end
- #
- # post = Post.create
- # post.comments << Comment.create(:id => 100)
- #
- # post.comments.map(&:id) == ["100"]
- # # => true
- #
- # comments = (101..103).to_a.map { |i| Comment.create(:id => i) }
- #
- # post.comments.replace(comments)
- # post.comments.map(&:id) == ["101", "102", "103"]
- # # => true
- #
- # @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC
- # in the Redis Command Reference.
- def replace(models)
- model.db.multi do
- clear
- models.each { |model| add(model) }
- end
- end
+ # Add a model directly to the set.
+ #
+ # Example:
+ #
+ # user = User.create
+ # post = Post.create
+ #
+ # user.posts.add(post)
+ #
+ def add(model)
+ key.sadd(model.id)
+ end
- # @return [true, false] Whether or not this collection is empty.
- def empty?
- !key.exists
- end
+ # Chain new fiters on an existing set.
+ #
+ # Example:
+ #
+ # set = User.find(name: "John")
+ # set.find(age: 30)
+ #
+ def find(dict)
+ keys = model.filters(dict)
+ keys.push(key)
- # @return [Array] Array representation of this collection.
- def to_a
- all
- end
+ MultiSet.new(keys, namespace, model)
end
- # Provides a Ruby-esque interface to a _Redis_ *SET*. The *SET* is assumed
- # to be composed of ids which maps to {#model}.
- class Set < Collection
- # An implementation which relies on *SMEMBERS* and yields an instance
- # of {#model}.
- #
- # @example
- #
- # class Author < Ohm::Model
- # set :poems, Poem
- # end
- #
- # class Poem < Ohm::Model
- # end
- #
- # neruda = Author.create
- # neruda.poems.add(Poem.create)
- #
- # neruda.poems.each do |poem|
- # # do something with the poem
- # end
- #
- # # if you look at the source, you'll quickly see that this can
- # # easily be achieved by doing the following:
- #
- # neruda.poems.key.smembers.each do |id|
- # poem = Poem[id]
- # # do something with the poem
- # end
- #
- # @see http://code.google.com/p/redis/wiki/SmembersCommand SMEMBERS
- # in Redis Command Reference.
- def each(&block)
- key.smembers.each { |id| block.call(model.to_proc[id]) }
- end
+ # Replace all the existing elements of a set with a different
+ # collection of models. This happens atomically in a MULTI-EXEC
+ # block.
+ #
+ # Example:
+ #
+ # user = User.create
+ # p1 = Post.create
+ # user.posts.add(p1)
+ #
+ # p2, p3 = Post.create, Post.create
+ # user.posts.replace([p2, p3])
+ #
+ # user.posts.include?(p1)
+ # # => false
+ #
+ def replace(models)
+ ids = models.map { |model| model.id }
- # Convenient way to scope access to a predefined set, useful for access
- # control.
- #
- # @example
- #
- # class User < Ohm::Model
- # set :photos, Photo
- # end
- #
- # class Photo < Ohm::Model
- # end
- #
- # @user = User.create
- # @user.photos.add(Photo.create(:id => "101"))
- # @user.photos.add(Photo.create(:id => "102"))
- #
- # Photo.create(:id => "500")
- #
- # @user.photos[101] == Photo[101]
- # # => true
- #
- # @user.photos[500] == nil
- # # => true
- #
- # @param [#to_s] id Any id existing within this set.
- # @return [Ohm::Model, nil] The model if it exists.
- def [](id)
- model[id] if key.sismember(id)
+ key.redis.multi do
+ key.del
+ ids.each { |id| key.sadd(id) }
end
+ end
- # Adds a model to this set.
- #
- # @param [#id] model Typically an instance of an {Ohm::Model} subclass.
- #
- # @see http://code.google.com/p/redis/wiki/SaddCommand SADD in Redis
- # Command Reference.
- def <<(model)
- key.sadd(model.id)
- end
- alias add <<
+ private
+ def execute
+ yield key
+ end
+ end
- # Thin Ruby interface wrapper for *SCARD*.
- #
- # @return [Fixnum] The total number of members for this set.
- # @see http://code.google.com/p/redis/wiki/ScardCommand SCARD in Redis
- # Command Reference.
- def size
- key.scard
- end
+ # Anytime you filter a set with more than one requirement, you
+ # internally use a `MultiSet`. `MutiSet` is a bit slower than just
+ # a `Set` because it has to `SINTERSTORE` all the keys prior to
+ # retrieving the members, size, etc.
+ #
+ # Example:
+ #
+ # User.all.kind_of?(Ohm::Set)
+ # # => true
+ #
+ # User.find(name: "John").kind_of?(Ohm::Set)
+ # # => true
+ #
+ # User.find(name: "John", age: 30).kind_of?(Ohm::MultiSet)
+ # # => true
+ #
+ class MultiSet < Struct.new(:keys, :namespace, :model)
+ include Collection
- # Thin Ruby interface wrapper for *SREM*.
- #
- # @param [#id] member a member of this set.
- # @see http://code.google.com/p/redis/wiki/SremCommand SREM in Redis
- # Command Reference.
- def delete(member)
- key.srem(member.id)
- end
+ # Chain new fiters on an existing set.
+ #
+ # Example:
+ #
+ # set = User.find(name: "John", age: 30)
+ # set.find(status: 'pending')
+ #
+ def find(dict)
+ keys = model.filters(dict)
+ keys.push(*self.keys)
- # Array representation of this set.
- #
- # @example
- #
- # class Author < Ohm::Model
- # set :posts, Post
- # end
- #
- # class Post < Ohm::Model
- # end
- #
- # author = Author.create
- # author.posts.add(Author.create(:id => "101"))
- # author.posts.add(Author.create(:id => "102"))
- #
- # author.posts.all.is_a?(Array)
- # # => true
- #
- # author.posts.all.include?(Author[101])
- # # => true
- #
- # author.posts.all.include?(Author[102])
- # # => true
- #
- # @return [Array<Ohm::Model>] All members of this set.
- def all
- key.smembers.map(&model)
- end
+ MultiSet.new(keys, namespace, model)
+ end
- # Allows you to find members of this set which fits the given criteria.
- #
- # @example
- #
- # class Post < Ohm::Model
- # attribute :title
- # attribute :tags
- #
- # index :title
- # index :tag
- #
- # def tag
- # tags.split(/\s+/)
- # end
- # end
- #
- # post = Post.create(:title => "Ohm", :tags => "ruby ohm redis")
- # Post.all.is_a?(Ohm::Model::Set)
- # # => true
- #
- # Post.all.find(:tag => "ruby").include?(post)
- # # => true
- #
- # # Post.find is actually just a wrapper around Post.all.find
- # Post.find(:tag => "ohm", :title => "Ohm").include?(post)
- # # => true
- #
- # Post.find(:tag => ["ruby", "python"]).empty?
- # # => true
- #
- # # Alternatively, you may choose to chain them later on.
- # ruby = Post.find(:tag => "ruby")
- # ruby.find(:title => "Ohm").include?(post)
- # # => true
- #
- # @param [Hash] options A hash of key value pairs.
- # @return [Ohm::Model::Set] A set satisfying the filter passed.
- def find(options)
- source = keys(options)
- target = source.inject(key.volatile) { |chain, other| chain + other }
- apply(:sinterstore, key, source, target)
- end
+ private
+ def execute
+ key = namespace[:temp][SecureRandom.uuid]
+ key.sinterstore(*keys)
- # Similar to find except that it negates the criteria.
- #
- # @example
- # class Post < Ohm::Model
- # attribute :title
- # end
- #
- # ohm = Post.create(:title => "Ohm")
- # ruby = Post.create(:title => "Ruby")
- #
- # Post.except(:title => "Ohm").include?(ruby)
- # # => true
- #
- # Post.except(:title => "Ohm").size == 1
- # # => true
- #
- # @param [Hash] options A hash of key value pairs.
- # @return [Ohm::Model::Set] A set satisfying the filter passed.
- def except(options)
- source = keys(options)
- target = source.inject(key.volatile) { |chain, other| chain - other }
- apply(:sdiffstore, key, source, target)
+ begin
+ yield key
+ ensure
+ key.del
end
+ end
+ end
- # Returns by default the lowest id value for this set. You may also
- # pass in options similar to {#sort}.
- #
- # @example
- #
- # class Post < Ohm::Model
- # attribute :title
- # end
- #
- # p1 = Post.create(:id => "101", :title => "Alpha")
- # p2 = Post.create(:id => "100", :title => "Beta")
- # p3 = Post.create(:id => "99", :title => "Gamma")
- #
- # Post.all.is_a?(Ohm::Model::Set)
- # # => true
- #
- # p3 == Post.all.first
- # # => true
- #
- # p1 == Post.all.first(:order => "DESC")
- # # => true
- #
- # p1 == Post.all.first(:by => :title, :order => "ASC ALPHA")
- # # => true
- #
- # # just ALPHA also means ASC ALPHA, for brevity.
- # p1 == Post.all.first(:by => :title, :order => "ALPHA")
- # # => true
- #
- # p3 == Post.all.first(:by => :title, :order => "DESC ALPHA")
- # # => true
- #
- # @param [Hash] options Sort options hash.
- # @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if this
- # set is empty.
- #
- # @see file:README.html#sorting Sorting in the README.
- def first(options = {})
- opts = options.dup
- opts.merge!(:limit => 1)
+ # 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 < Ohm::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)
+ #
+ # 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
+ #
+ class Model
+ include Scrivener::Validations
- if opts[:by]
- sort_by(opts.delete(:by), opts).first
- else
- sort(opts).first
- end
- end
+ def self.conn
+ @conn ||= Connection.new(name, Ohm.conn.options)
+ end
- # Ruby-like interface wrapper around *SISMEMBER*.
- #
- # @param [#id] model Typically an {Ohm::Model} instance.
- #
- # @return [true, false] Whether or not the {Ohm::Model model} instance
- # is a member of this set.
- #
- # @see http://code.google.com/p/redis/wiki/SismemberCommand SISMEMBER
- # in Redis Command Reference.
- def include?(model)
- key.sismember(model.id)
- end
+ def self.connect(options)
+ @key = nil
+ @lua = nil
+ conn.start(options)
+ end
- def inspect
- "#<Set (#{model}): #{key.smembers.inspect}>"
- end
+ def self.db
+ conn.redis
+ end
- protected
- # @private
- def apply(operation, key, source, target)
- target.send(operation, key, *source)
- Set.new(target, Wrapper.wrap(model))
- end
+ def self.lua
+ @lua ||= Lua.new(File.join(Dir.pwd, "lua"), db)
+ end
- # @private
- #
- # Transform a hash of attribute/values into an array of keys.
- def keys(hash)
- [].tap do |keys|
- hash.each do |key, values|
- values = [values] unless values.kind_of?(Array)
- values.each do |v|
- keys << model.index_key_for(key, v)
- end
- end
- end
- end
+ # The namespace for all the keys generated using this model.
+ #
+ # Example:
+ #
+ # class User < Ohm::Model
+ #
+ # User.key == "User"
+ # User.key.kind_of?(String)
+ # # => true
+ #
+ # User.key.kind_of?(Nest)
+ # # => true
+ #
+ # To find out more about Nest, see:
+ # http://github.com/soveran/nest
+ #
+ def self.key
+ @key ||= Nest.new(self.name, db)
end
- class Index < Set
- # This method is here primarily as an optimization. Let's say you have
- # the following model:
- #
- # class Post < Ohm::Model
- # attribute :title
- # index :title
- # end
- #
- # ruby = Post.create(:title => "ruby")
- # redis = Post.create(:title => "redis")
- #
- # Post.key[:all].smembers == [ruby.id, redis.id]
- # # => true
- #
- # Post.index_key_for(:title, "ruby").smembers == [ruby.id]
- # # => true
- #
- # Post.index_key_for(:title, "redis").smembers == [redis.id]
- # # => true
- #
- # If we want to search for example all `Posts` entitled "ruby" or
- # "redis", then it doesn't make sense to do an INTERSECTION with
- # `Post.key[:all]` since it would be redundant.
- #
- # The implementation of {Ohm::Model::Index#find} avoids this redundancy.
- #
- # @see Ohm::Model::Set#find find in Ohm::Model::Set.
- # @see Ohm::Model.find find in Ohm::Model.
- def find(options)
- keys = keys(options)
- return super(options) if keys.size > 1
-
- Set.new(keys.first, Wrapper.wrap(model))
- end
+ # Retrieve a record by ID.
+ #
+ # Example:
+ #
+ # u = User.create
+ # u == User[u.id]
+ # # => true
+ #
+ def self.[](id)
+ new(id: id).load! if id && exists?(id)
end
- # Provides a Ruby-esque interface to a _Redis_ *LIST*. The *LIST* is
- # assumed to be composed of ids which maps to {#model}.
- class List < Collection
- # An implementation which relies on *LRANGE* and yields an instance
- # of {#model}.
- #
- # @example
- #
- # class Post < Ohm::Model
- # list :comments, Comment
- # end
- #
- # class Comment < Ohm::Model
- # end
- #
- # post = Post.create
- # post.comments.add(Comment.create)
- # post.comments.add(Comment.create)
- #
- # post.comments.each do |comment|
- # # do something with the comment
- # end
- #
- # # reading the source reveals that this is achieved by doing:
- # post.comments.key.lrange(0, -1).each do |id|
- # comment = Comment[id]
- # # do something with the comment
- # end
- #
- # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
- # in Redis Command Reference.
- def each(&block)
- key.lrange(0, -1).each { |id| block.call(model.to_proc[id]) }
- end
-
- # Thin wrapper around *RPUSH*.
- #
- # @example
- #
- # class Post < Ohm::Model
- # list :comments, Comment
- # end
- #
- # class Comment < Ohm::Model
- # end
- #
- # p = Post.create
- # p.comments << Comment.create
- #
- # @param [#id] model Typically an {Ohm::Model} instance.
- #
- # @see http://code.google.com/p/redis/wiki/RpushCommand RPUSH
- # in Redis Command Reference.
- def <<(model)
- key.rpush(model.id)
- end
- alias push <<
-
- # Returns the element at index, or returns a subarray starting at
- # `start` and continuing for `length` elements, or returns a subarray
- # specified by `range`. Negative indices count backward from the end
- # of the array (-1 is the last element). Returns nil if the index
- # (or starting index) are out of range.
- #
- # @example
- # class Post < Ohm::Model
- # list :comments, Comment
- # end
- #
- # class Comment < Ohm::Model
- # end
- #
- # post = Post.create
- #
- # 10.times { post.comments << Comment.create }
- #
- # post.comments[0] == Comment[1]
- # # => true
- #
- # post.comments[0, 4] == (1..5).map { |i| Comment[i] }
- # # => true
- #
- # post.comments[0, 4] == post.comments[0..4]
- # # => true
- #
- # post.comments.all == post.comments[0, -1]
- # # => true
- #
- # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
- # in Redis Command Reference.
- def [](index, limit = nil)
- case [index, limit]
- when Pattern[Fixnum, Fixnum] then
- key.lrange(index, limit).collect { |id| model.to_proc[id] }
- when Pattern[Range, nil] then
- key.lrange(index.first, index.last).collect { |id| model.to_proc[id] }
- when Pattern[Fixnum, nil] then
- model[key.lindex(index)]
- end
- end
-
- # Convience method for doing list[0], similar to Ruby's Array#first
- # method.
- #
- # @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list
- # is empty.
- def first
- self[0]
- end
-
- # Returns the model at the tail of this list, while simultaneously
- # removing it from the list.
- #
- # @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if the list
- # is empty.
- #
- # @see http://code.google.com/p/redis/wiki/LpopCommand RPOP
- # in Redis Command Reference.
- def pop
- model[key.rpop]
- end
-
- # Returns the model at the head of this list, while simultaneously
- # removing it from the list.
- #
- # @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list
- # is empty.
- #
- # @see http://code.google.com/p/redis/wiki/LpopCommand LPOP
- # in Redis Command Reference.
- def shift
- model[key.lpop]
- end
-
- # Prepends an {Ohm::Model} instance at the beginning of this list.
- #
- # @param [#id] model Typically an {Ohm::Model} instance.
- #
- # @see http://code.google.com/p/redis/wiki/RpushCommand LPUSH
- # in Redis Command Reference.
- def unshift(model)
- key.lpush(model.id)
- end
-
- # Returns an array representation of this list, with elements of the
- # array being an instance of {#model}.
- #
- # @return [Array<Ohm::Model>] Instances of {Ohm::Model}.
- def all
- key.lrange(0, -1).map(&model)
- end
-
- # Thin Ruby interface wrapper for *LLEN*.
- #
- # @return [Fixnum] The total number of elements for this list.
- #
- # @see http://code.google.com/p/redis/wiki/LlenCommand LLEN in Redis
- # Command Reference.
- def size
- key.llen
- end
-
- # Ruby-like interface wrapper around *LRANGE*.
- #
- # @param [#id] model Typically an {Ohm::Model} instance.
- #
- # @return [true, false] Whether or not the {Ohm::Model} instance is
- # an element of this list.
- #
- # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE
- # in Redis Command Reference.
- def include?(model)
- key.lrange(0, -1).include?(model.id)
- end
-
- def inspect
- "#<List (#{model}): #{key.lrange(0, -1).inspect}>"
- 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 Ohm::Set#fetch.
+ #
+ def self.to_proc
+ lambda { |id| self[id] }
end
- # All validations that need access to the _Redis_ database go here.
- # As of this writing, {Ohm::Model::Validations#assert_unique} is the only
- # assertion contained within this module.
- module Validations
- include Ohm::Validations
-
- # Validates that the attribute or array of attributes are unique. For
- # this, an index of the same kind must exist.
- #
- # @overload assert_unique :name
- # Validates that the name attribute is unique.
- # @overload assert_unique [:street, :city]
- # Validates that the :street and :city pair is unique.
- def assert_unique(atts, error = [atts, :not_unique])
- indices = Array(atts).map { |att| index_key_for(att, send(att)) }
- result = db.sinter(*indices)
-
- assert result.empty? || !new? && result.include?(id.to_s), error
- end
+ # Check if the ID exists within <Model>:all.
+ def self.exists?(id)
+ key[:all].sismember(id)
end
- include Validations
-
- # Raised when you try and get the *id* of an {Ohm::Model} without an id.
+ # Find values in `unique` indices.
#
- # class Post < Ohm::Model
- # list :comments, Comment
- # end
+ # Example:
#
- # class Comment < Ohm::Model
+ # class User < Ohm::Model
+ # unique :email
# end
#
- # ex = nil
- # begin
- # Post.new.id
- # rescue Exception => e
- # ex = e
- # end
- #
- # ex.kind_of?(Ohm::Model::MissingID)
+ # u = User.create(email: "foo@bar.com")
+ # u == User.with(:email, "foo@bar.com")
# # => true
#
- # This is also one of the most common errors you'll be faced with when
- # you're new to {Ohm} coming from an ActiveRecord background, where you
- # are used to just assigning associations even before the base model is
- # persisted.
+ def self.with(att, val)
+ id = key[:uniques][att].hget(val)
+ id && self[id]
+ end
+
+ # Find values in indexed fields.
#
- # # following from the example above:
- # post = Post.new
+ # Example:
#
- # ex = nil
- # begin
- # post.comments << Comment.new
- # rescue Exception => e
- # ex = e
- # end
+ # class User < Ohm::Model
+ # attribute :email
#
- # ex.kind_of?(Ohm::Model::MissingID)
- # # => true
+ # attribute :name
+ # index :name
#
- # # Correct way:
- # post = Post.new
+ # attribute :status
+ # index :status
#
- # if post.save
- # post.comments << Comment.create
- # end
- class MissingID < Error
- def message
- "You tried to perform an operation that needs the model ID, " +
- "but it's not present."
- end
- end
-
- # Raised when you try and do an {Ohm::Model::Set#find} operation and use
- # a key which you did not define as an index.
+ # index :provider
+ # index :tag
#
- # class Post < Ohm::Model
- # attribute :title
+ # def provider
+ # email[/@(.*?).com/, 1]
+ # end
+ #
+ # def tag
+ # ["ruby", "python"]
+ # end
# end
#
- # post = Post.create(:title => "Ohm")
+ # u = User.create(name: "John", status: "pending", email: "foo@me.com")
+ # User.find(provider: "me", name: "John", status: "pending").include?(u)
+ # # => true
#
- # ex = nil
- # begin
- # Post.find(:title => "Ohm")
- # rescue Exception => e
- # ex = e
- # end
+ # User.find(tag: "ruby").include?(u)
+ # # => true
#
- # ex.kind_of?(Ohm::Model::IndexNotFound)
+ # User.find(tag: "python").include?(u)
# # => true
#
- # To correct this problem, simply define a _:title_ *index* in your class.
+ # User.find(tag: ["ruby", "python"]).include?(u)
+ # # => true
#
- # class Post < Ohm::Model
- # attribute :title
- # index :title
- # end
- class IndexNotFound < Error
- def initialize(att)
- @att = att
- end
+ def self.find(dict)
+ keys = filters(dict)
- def message
- "Index #{@att.inspect} not found."
+ if keys.size == 1
+ Ohm::Set.new(keys.first, key, self)
+ else
+ Ohm::MultiSet.new(keys, key, self)
end
end
- @@attributes = Hash.new { |hash, key| hash[key] = [] }
- @@collections = Hash.new { |hash, key| hash[key] = [] }
- @@counters = Hash.new { |hash, key| hash[key] = [] }
- @@indices = Hash.new { |hash, key| hash[key] = [] }
-
- def id
- @id or raise MissingID
+ # Index any method on your model. Once you index a method, you can
+ # use it in `find` statements.
+ def self.index(attribute)
+ indices << attribute unless indices.include?(attribute)
end
- # Defines a string attribute for the model. This attribute will be
- # persisted by _Redis_ as a string. Any value stored here will be
- # retrieved in its string representation.
+ # Create a unique index for any method on your model. Once you add
+ # a unique index, you can use it in `with` statements.
#
- # If you're looking to have typecasting built in, you may want to look at
- # Ohm::Typecast in Ohm::Contrib.
+ # Note: if there is a conflict while saving, an
+ # `Ohm::UniqueIndexViolation` violation is raised.
#
- # @param name [Symbol] Name of the attribute.
- # @see http://cyx.github.com/ohm-contrib/doc/Ohm/Typecast.html
- def self.attribute(name)
- define_method(name) do
- read_local(name)
- end
-
- define_method(:"#{name}=") do |value|
- write_local(name, value)
- end
-
- attributes << name unless attributes.include?(name)
+ def self.unique(attribute)
+ uniques << attribute unless uniques.include?(attribute)
end
- # Defines a counter attribute for the model. This attribute can't be
- # assigned, only incremented or decremented. It will be zero by default.
+ # Declare an Ohm::Set with the given name.
#
- # @param [Symbol] name Name of the counter.
- def self.counter(name)
- define_method(name) do
- read_local(name).to_i
- end
-
- counters << name unless counters.include?(name)
- end
-
- # Defines a list attribute for the model. It can be accessed only after
- # the model instance is created, or if you assign an :id during object
- # construction.
+ # Example:
#
- # @example
- #
- # class Post < Ohm::Model
- # list :comments, Comment
+ # class User < Ohm::Model
+ # set :posts, :Post
# end
#
- # class Comment < Ohm::Model
- # end
+ # u = User.create
+ # u.posts.empty?
+ # # => true
#
- # # WRONG!!!
- # post = Post.new
- # post.comments << Comment.create
+ # Note: You can't use the set until you save the model. If you try
+ # to do it, you'll receive an Ohm::MissingID error.
#
- # # Right :-)
- # post = Post.create
- # post.comments << Comment.create
- #
- # # Alternative way if you want to have custom ids.
- # post = Post.new(:id => "my-id")
- # post.comments << Comment.create
- # post.create
- #
- # @param [Symbol] name Name of the list.
- def self.list(name, model)
- define_memoized_method(name) { List.new(key[name], Wrapper.wrap(model)) }
- collections << name unless collections.include?(name)
- end
-
- # Defines a set attribute for the model. It can be accessed only after
- # the model instance is created. Sets are recommended when insertion and
- # retreival order is irrelevant, and operations like union, join, and
- # membership checks are important.
- #
- # @param [Symbol] name Name of the set.
def self.set(name, model)
- define_memoized_method(name) { Set.new(key[name], Wrapper.wrap(model)) }
collections << name unless collections.include?(name)
+
+ define_method name do
+ model = Utils.const(self.class, model)
+
+ Ohm::Set.new(key[name], model.key, model)
+ end
end
- # Creates an index (a set) that will be used for finding instances.
+ # A macro for defining a method which basically does a find.
#
- # If you want to find a model instance by some attribute value, then an
- # index for that attribute must exist.
+ # Example:
+ # class Post < Ohm::Model
+ # reference :user, :User
+ # end
#
- # @example
- #
# class User < Ohm::Model
- # attribute :email
- # index :email
+ # collection :posts, :Post
# end
#
- # # Now this is possible:
- # User.find :email => "ohm@example.com"
+ # # is the same as
#
- # @param [Symbol] name Name of the attribute to be indexed.
- def self.index(att)
- indices << att unless indices.include?(att)
+ # class User < Ohm::Model
+ # def posts
+ # Post.find(user_id: self.id)
+ # end
+ # end
+ #
+ 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
- # Define a reference to another object.
+ # A macro for defining an attribute, an index, and an accessor
+ # for a given model.
#
- # @example
+ # Example:
#
- # class Comment < Ohm::Model
- # attribute :content
- # reference :post, Post
+ # class Post < Ohm::Model
+ # reference :user, :User
# end
#
- # @post = Post.create :content => "Interesting stuff"
+ # # It's the same as:
#
- # @comment = Comment.create(:content => "Indeed!", :post => @post)
+ # class Post < Ohm::Model
+ # attribute :user_id
+ # index :user_id
#
- # @comment.post.content
- # # => "Interesting stuff"
+ # def user
+ # @_memo[:user] ||= User[user_id]
+ # end
#
- # @comment.post = Post.create(:content => "Wonderful stuff")
+ # def user=(user)
+ # self.user_id = user.id
+ # @_memo[:user] = user
+ # end
#
- # @comment.post.content
- # # => "Wonderful stuff"
+ # def user_id=(user_id)
+ # @_memo.delete(:user_id)
+ # self.user_id = user_id
+ # end
+ # end
#
- # @comment.post.update(:content => "Magnific stuff")
- #
- # @comment.post.content
- # # => "Magnific stuff"
- #
- # @comment.post = nil
- #
- # @comment.post
- # # => nil
- #
- # @see file:README.html#references References Explained.
- # @see Ohm::Model.collection
def self.reference(name, model)
- model = Wrapper.wrap(model)
-
reader = :"#{name}_id"
writer = :"#{name}_id="
- attributes << reader unless attributes.include?(reader)
-
index reader
- define_memoized_method(name) do
- model.unwrap[send(reader)]
+ define_method(reader) do
+ @attributes[reader]
end
+ define_method(writer) do |value|
+ @_memo.delete(name)
+ @attributes[reader] = value
+ end
+
define_method(:"#{name}=") do |value|
@_memo.delete(name)
send(writer, value ? value.id : nil)
end
- define_method(reader) do
- read_local(reader)
+ define_method(name) do
+ @_memo[name] ||= begin
+ model = Utils.const(self.class, model)
+ model[send(reader)]
+ end
end
-
- define_method(writer) do |value|
- @_memo.delete(name)
- write_local(reader, value)
- end
end
- # Define a collection of objects which have a
- # {Ohm::Model.reference reference} to this model.
+ # The bread and butter macro of all models. Basically declares
+ # persisted attributes. All attributes are stored on the Redis
+ # hash.
#
- # class Comment < Ohm::Model
- # attribute :content
- # reference :post, Post
+ # Example:
+ # class User < Ohm::Model
+ # attribute :name
# end
#
- # class Post < Ohm::Model
- # attribute :content
- # collection :comments, Comment
- # reference :author, Person
- # end
+ # # It's the same as:
#
- # class Person < Ohm::Model
- # attribute :name
+ # class User < Ohm::Model
+ # def name
+ # @attributes[:name]
+ # end
#
- # # When the name of the reference cannot be inferred,
- # # you need to specify it in the third param.
- # collection :posts, Post, :author
+ # def name=(name)
+ # @attributes[:name] = name
+ # end
# end
#
- # @person = Person.create :name => "Albert"
- # @post = Post.create :content => "Interesting stuff",
- # :author => @person
- # @comment = Comment.create :content => "Indeed!", :post => @post
+ def self.attribute(name, cast = nil)
+ 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
+
+ # 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.
#
- # @post.comments.first.content
- # # => "Indeed!"
+ # Example:
#
- # @post.author.name
- # # => "Albert"
+ # class User < Ohm::Model
+ # counter :points
+ # end
#
- # *Important*: Please note that even though a collection is a
- # {Ohm::Model::Set set}, you should not add or remove objects from this
- # collection directly.
+ # u = User.create
+ # u.incr :points
#
- # @see Ohm::Model.reference
- # @param name [Symbol] Name of the collection.
- # @param model [Constant] Model where the reference is defined.
- # @param reference [Symbol] Reference as defined in the associated
- # model.
+ # Ohm.redis.hget "User:1:counters", "points"
+ # # => 1
#
- # @see file:README.html#collections Collections Explained.
- def self.collection(name, model, reference = to_reference)
- model = Wrapper.wrap(model)
- define_method(name) {
- model.unwrap.find(:"#{reference}_id" => send(:id))
- }
- end
-
- # Used by {Ohm::Model.collection} to infer the reference.
+ # Note: You can't use counters until you save the model. If you
+ # try to do it, you'll receive an Ohm::MissingID error.
#
- # @return [Symbol] Representation of this class in an all-lowercase
- # format, separated by underscores and demodulized.
- def self.to_reference
- name.to_s.
- match(/^(?:.*::)*(.*)$/)[1].
- gsub(/([a-z\d])([A-Z])/, '\1_\2').
- downcase.to_sym
- end
-
- # @private
- def self.define_memoized_method(name, &block)
+ def self.counter(name)
define_method(name) do
- @_memo[name] ||= instance_eval(&block)
+ return 0 if new?
+
+ key[:counters].hget(name).to_i
end
end
- # Allows you to find an {Ohm::Model} instance by its *id*.
- #
- # @param [#to_s] id The id of the model you want to find.
- # @return [Ohm::Model, nil] The instance of Ohm::Model or nil of it does
- # not exist.
- def self.[](id)
- new(:id => id) if id && exists?(id)
+ # An Ohm::Set wrapper for Model.key[:all].
+ def self.all
+ Set.new(key[:all], key, self)
end
- # @private Used for conveniently doing [1, 2].map(&Post) for example.
- def self.to_proc
- lambda { |id| new(:id => id) }
+ # Syntactic sugar for Model.new(atts).save
+ def self.create(atts = {})
+ new(atts).save
end
- # Returns a {Ohm::Model::Set set} containing all the members of a given
- # class.
+ # Manipulate the Redis hash of attributes directly.
#
- # @example
+ # Example:
#
- # class Post < Ohm::Model
+ # class User < Ohm::Model
+ # attribute :name
# end
#
- # post = Post.create
+ # u = User.create(name: "John")
+ # u.key.hget(:name)
+ # # => John
#
- # Post.all.include?(post)
- # # => true
+ # For more details see
+ # http://github.com/soveran/nest
#
- # post.delete
- #
- # Post.all.include?(post)
- # # => false
- def self.all
- Ohm::Model::Index.new(key[:all], Wrapper.wrap(self))
+ def key
+ model.key[id]
end
- # All the defined attributes within a class.
- # @see Ohm::Model.attribute
- def self.attributes
- @@attributes[self]
- end
-
- # All the defined counters within a class.
- # @see Ohm::Model.counter
- def self.counters
- @@counters[self]
- end
-
- # All the defined collections within a class. This will be comprised of
- # all {Ohm::Model::Set sets} and {Ohm::Model::List lists} defined within
- # your class.
+ # Initialize a model using a dictionary of attributes.
#
- # @example
- # class Post < Ohm::Model
- # set :authors, Author
- # list :comments, Comment
- # end
+ # Example:
#
- # Post.collections == [:authors, :comments]
- # # => true
+ # u = User.new(name: "John")
#
- # @see Ohm::Model.list
- # @see Ohm::Model.set
- def self.collections
- @@collections[self]
+ def initialize(atts = {})
+ @attributes = {}
+ @_memo = {}
+ update_attributes(atts)
end
- # All the defined indices within a class.
- # @see Ohm::Model.index
- def self.indices
- @@indices[self]
- end
-
- # Convenience method to create and return the newly created object.
+ # 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
+ # Example:
#
- # class Post < Ohm::Model
- # attribute :title
- # end
+ # class User < Ohm::Model; end
#
- # post = Post.create(:title => "A new post")
+ # u = User.create
+ # u.id
+ # # => 1
#
- # @param [Hash] args attribute-value pairs for the object.
- # @return [Ohm::Model] an instance of the class you're trying to create.
- def self.create(*args)
- model = new(*args)
- model.create
- model
+ # u.key
+ # # => User:1
+ #
+ def id
+ raise MissingID if not defined?(@id)
+ @id
end
- # Search across multiple indices and return the intersection of the sets.
+ # Check for equality by doing the following assertions:
#
- # @example Finds all the user events for the supplied days
- # event1 = Event.create day: "2009-09-09", author: "Albert"
- # event2 = Event.create day: "2009-09-09", author: "Benoit"
- # event3 = Event.create day: "2009-09-10", author: "Albert"
+ # 1. That the passed model is of the same type.
+ # 2. That they represent the same Redis key.
#
- # [event1] == Event.find(author: "Albert", day: "2009-09-09").to_a
- # # => true
- def self.find(hash)
- unless hash.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
-
- all.find(hash)
+ def ==(other)
+ other.kind_of?(model) && other.key == key
+ rescue MissingID
+ false
end
- # Encode a value, making it safe to use as a key. Internally used by
- # {Ohm::Model.index_key_for} to canonicalize the indexed values.
- #
- # @param [#to_s] value Any object you want to be able to use as a key.
- # @return [String] A string which is safe to use as a key.
- # @see Ohm::Model.index_key_for
- def self.encode(value)
- Base64.encode64(value.to_s).gsub("\n", "")
+ # Preload all the attributes of this model from Redis. Used
+ # internally by `Model::[]`.
+ def load!
+ update_attributes(key.hgetall) unless new?
+ return self
end
- # Constructor for all subclasses of {Ohm::Model}, which optionally
- # takes a Hash of attribute value pairs.
+ # Read an attribute remotly from Redis. Useful if you want to get
+ # the most recent value of the attribute and not rely on locally
+ # cached value.
#
- # Starting with Ohm 0.1.0, you can use custom ids instead of being forced
- # to use auto incrementing numeric ids, but keep in mind that you have
- # to pass in the preferred id during object initialization.
+ # Example:
#
- # @example
+ # User.create(name: "A")
#
- # class User < Ohm::Model
- # end
+ # Session 1 | Session 2
+ # --------------|------------------------
+ # u = User[1] | u = User[1]
+ # u.name = "B" |
+ # u.save |
+ # | u.name == "A"
+ # | u.get(:name) == "B"
#
- # class Post < Ohm::Model
- # attribute :title
- # reference :user, User
- # end
+ def get(att)
+ @attributes[att] = key.hget(att)
+ end
+
+ # Update an attribute value atomically. The best usecase for this
+ # is when you simply want to update one value.
#
- # user = User.create
- # p1 = Post.new(:title => "Redis", :user_id => user.id)
- # p1.save
+ # Note: This method is dangerous because it doesn't update indices
+ # and uniques. Use it wisely. The safe equivalent is `update`.
#
- # p1.user_id == user.id
- # # => true
- #
- # p1.user == user
- # # => true
- #
- # # You can also just pass the actual User object, which is the better
- # # way to do it:
- # Post.new(:title => "Different way", :user => user).user == user
- # # => true
- #
- # # Let's try and generate custom ids
- # p2 = Post.new(:id => "ohm-redis-library", :title => "Lib")
- # p2 == Post["ohm-redis-library"]
- # # => true
- #
- # @param [Hash] attrs Attribute value pairs.
- def initialize(attrs = {})
- @id = nil
- @_memo = {}
- @_attributes = Hash.new { |hash, key| hash[key] = read_remote(key) }
- update_attributes(attrs)
+ def set(att, val)
+ val.to_s.empty? ? key.hdel(att) : key.hset(att, val)
+ @attributes[att] = val
end
- # @return [true, false] Whether or not this object has an id.
def new?
- !@id
+ !defined?(@id)
end
- # Create this model if it passes all validations.
- #
- # @return [Ohm::Model, nil] The newly created object or nil if it fails
- # validation.
- def create
- return unless valid?
- initialize_id
-
- mutex do
- create_model_membership
- write
- add_to_indices
- end
+ # Increment a counter atomically. Internally uses HINCRBY.
+ def incr(att, count = 1)
+ key[:counters].hincrby(att, count)
end
- # Create or update this object based on the state of #new?.
- #
- # @return [Ohm::Model, nil] The saved object or nil if it fails
- # validation.
- def save
- return create if new?
- return unless valid?
-
- mutex do
- write
- update_indices
- end
+ # Decrement a counter atomically. Internally uses HINCRBY.
+ def decr(att, count = 1)
+ incr(att, -count)
end
- # Update this object, optionally accepting new attributes.
+ # Return a value that allows the use of models as hash keys.
#
- # @param [Hash] attrs Attribute value pairs to use for the updated
- # version
- # @return [Ohm::Model, nil] The updated object or nil if it fails
- # validation.
- def update(attrs)
- update_attributes(attrs)
- save
- end
-
- # Locally update all attributes without persisting the changes.
- # Internally used by {Ohm::Model#initialize} and {Ohm::Model#update}
- # to set attribute value pairs.
+ # Example:
#
- # @param [Hash] attrs Attribute value pairs.
- def update_attributes(attrs)
- attrs.each do |key, value|
- send(:"#{key}=", value)
- end
- end
-
- # Delete this object from the _Redis_ datastore, ensuring that all
- # indices, attributes, collections, etc are also deleted with it.
+ # h = {}
#
- # @return [Ohm::Model] Returns a reference of itself.
- def delete
- delete_from_indices
- delete_attributes(collections) unless collections.empty?
- delete_model_membership
- self
- end
-
- # Increment the counter denoted by :att.
+ # u = User.new
#
- # @param [Symbol] att Attribute to increment.
- # @param [Fixnum] count An optional increment step to use.
- def incr(att, count = 1)
- unless counters.include?(att)
- raise ArgumentError, "#{att.inspect} is not a counter."
- end
-
- write_local(att, key.hincrby(att, count))
+ # h[:u] = u
+ # h[:u] == u
+ # # => true
+ #
+ def hash
+ new? ? super : key.hash
end
+ alias :eql? :==
- # Decrement the counter denoted by :att.
- #
- # @param [Symbol] att Attribute to decrement.
- # @param [Fixnum] count An optional decrement step to use.
- def decr(att, count = 1)
- incr(att, -count)
+ def attributes
+ @attributes
end
- # Export the id and errors of the object. The `to_hash` takes the opposite
- # approach of providing all the attributes and instead favors a white
- # listed approach.
+ # Export the ID and the errors of the model. The approach of Ohm
+ # is to whitelist public attributes, as opposed to exporting each
+ # (possibly sensitive) attribute.
#
- # @example
+ # Example:
#
- # person = Person.create(:name => "John Doe")
- # person.to_hash == { :id => '1' }
- # # => true
+ # class User < Ohm::Model
+ # attribute :name
+ # end
#
- # # if the person asserts presence of name, the errors will be included
- # person = Person.create(:name => "John Doe")
- # person.name = nil
- # person.valid?
- # # => false
+ # u = User.create(name: "John")
+ # u.to_hash
+ # # => { id: "1" }
#
- # person.to_hash == { :id => '1', :errors => [[:name, :not_present]] }
- # # => true
+ # In order to add additional attributes, you can override `to_hash`:
#
- # # for cases where you want to provide white listed attributes just do:
+ # class User < Ohm::Model
+ # attribute :name
#
- # class Person < Ohm::Model
# def to_hash
- # super.merge(:name => name)
+ # super.merge(name: name)
# end
# end
#
- # # now we have the name when doing a to_hash
- # person = Person.create(:name => "John Doe")
- # person.to_hash == { :id => '1', :name => "John Doe" }
- # # => true
+ # u = User.create(name: "John")
+ # u.to_hash
+ # # => { id: "1", name: "John" }
+ #
def to_hash
attrs = {}
attrs[:id] = id unless new?
- attrs[:errors] = errors unless errors.empty?
- attrs
+ attrs[:errors] = errors if errors.any?
+
+ return attrs
end
- # Returns the JSON representation of the {#to_hash} for this object.
- # Defining a custom {#to_hash} method will also affect this and return
- # a corresponding JSON representation of whatever you have in your
- # {#to_hash}.
+ # Export a JSON representation of the model by encoding `to_hash`.
+ def to_json(*args)
+ to_hash.to_json(*args)
+ end
+
+ # Persist the model attributes and update indices and unique
+ # indices. The `counter`s and `set`s are not touched during save.
#
- # @example
- # require "json"
+ # If the model is not valid, nil is returned. Otherwise, the
+ # persisted model is returned.
#
- # class Post < Ohm::Model
- # attribute :title
+ # Example:
#
- # def to_hash
- # super.merge(:title => title)
+ # class User < Ohm::Model
+ # attribute :name
+ #
+ # def validate
+ # assert_present :name
# end
# end
#
- # p1 = Post.create(:title => "Delta Force")
- # p1.to_hash == { :id => "1", :title => "Delta Force" }
- # # => true
+ # User.new(name: nil).save
+ # # => nil
#
- # p1.to_json == "{\"id\":\"1\",\"title\":\"Delta Force\"}"
+ # u = User.new(name: "John").save
+ # u.kind_of?(User)
# # => true
#
- # @return [String] The JSON representation of this object defined in
- # terms of {#to_hash}.
- def to_json(*args)
- to_hash.to_json(*args)
+ def save(&block)
+ return if not valid?
+ save!(&block)
end
- # Convenience wrapper for {Ohm::Model.attributes}.
- def attributes
- self.class.attributes
- end
+ # Saves the model without checking for validity. Refer to
+ # `Model#save` for more details.
+ def save!
+ transaction do |t|
+ t.watch(*_unique_keys)
+ t.watch(key) if not new?
- # Convenience wrapper for {Ohm::Model.counters}.
- def counters
- self.class.counters
- end
+ t.before do
+ _initialize_id if new?
+ end
- # Convenience wrapper for {Ohm::Model.collections}.
- def collections
- self.class.collections
- end
+ t.read do |store|
+ _verify_uniques
+ store.existing = key.hgetall
+ store.uniques = _read_index_type(:uniques)
+ store.indices = _read_index_type(:indices)
+ end
- # Convenience wrapper for {Ohm::Model.indices}.
- def indices
- self.class.indices
- end
+ t.write do |store|
+ model.key[:all].sadd(id)
+ _delete_uniques(store.existing)
+ _delete_indices(store.existing)
+ _save
+ _save_indices(store.indices)
+ _save_uniques(store.uniques)
+ end
- # Implementation of equality checking. Equality is defined by two simple
- # rules:
- #
- # 1. They have the same class.
- # 2. They have the same key (_Redis_ key e.g. Post:1 == Post:1).
- #
- # @return [true, false] Whether or not the passed object is equal.
- def ==(other)
- other.kind_of?(self.class) && other.key == key
- rescue MissingID
- false
- end
- alias :eql? :==
+ yield t if block_given?
+ end
- # Allows you to safely use an instance of {Ohm::Model} as a key in a
- # Ruby hash without running into weird scenarios.
- #
- # @example
- #
- # class Post < Ohm::Model
- # end
- #
- # h = {}
- # p1 = Post.new
- # h[p1] = "Ruby"
- # h[p1] == "Ruby"
- # # => true
- #
- # p1.save
- # h[p1] == "Ruby"
- # # => false
- #
- # @return [Fixnum] An integer representing this object to be used
- # as the index for hashes in Ruby.
- def hash
- new? ? super : key.hash
+ return self
end
- # Lock the object before executing the block, and release it once the
- # block is done.
+ # Delete the model, including all the following keys:
#
- # This is used during {#create} and {#save} to ensure that no race
- # conditions occur.
+ # - <Model>:<id>
+ # - <Model>:<id>:counters
+ # - <Model>:<id>:<set name>
#
- # @see http://code.google.com/p/redis/wiki/SetnxCommand SETNX in the
- # Redis Command Reference.
- def mutex
- lock!
- yield
- self
- ensure
- unlock!
- end
-
- # Returns everything, including {Ohm::Model.attributes attributes},
- # {Ohm::Model.collections collections}, {Ohm::Model.counters counters},
- # and the id of this object.
+ # If the model has uniques or indices, they're also cleaned up.
#
- # Useful for debugging and for doing irb work.
- def inspect
- everything = (attributes + collections + counters).map do |att|
- value = begin
- send(att)
- rescue MissingID
- nil
- end
+ def delete
+ transaction do |t|
+ t.read do |store|
+ store.existing = key.hgetall
+ end
- [att, value.inspect]
- end
+ t.write do |store|
+ _delete_uniques(store.existing)
+ _delete_indices(store.existing)
+ model.collections.each { |e| key[e].del }
+ model.key[:all].srem(id)
+ key[:counters].del
+ key.del
+ end
- sprintf("#<%s:%s %s>",
- self.class,
- new? ? "?" : id,
- everything.map {|e| e.join("=") }.join(" ")
- )
+ yield t if block_given?
+ end
end
- # Makes the model connect to a different Redis instance. This is useful
- # for scaling a large application, where one model can be stored in a
- # different Redis instance, and some other groups of models can be
- # in another Redis instance.
+ # Update the model attributes and call save.
#
- # This approach of splitting models is a lot simpler than doing a
- # distributed *Redis* solution and may well be the right solution for
- # certain cases.
+ # Example:
#
- # @example
+ # User[1].update(name: "John")
#
- # class Post < Ohm::Model
- # connect :port => 6380, :db => 2
+ # # It's the same as:
#
- # attribute :body
- # end
+ # u = User[1]
+ # u.update_attributes(name: "John")
+ # u.save
#
- # # Since these settings are usually environment-specific,
- # # you may want to call this method from outside of the class
- # # definition:
- # Post.connect(:port => 6380, :db => 2)
- #
- # @see file:README.html#connecting Ohm.connect options documentation.
- def self.connect(options = {})
- Ohm.threaded[self] = nil
- @options = options
+ def update(attributes)
+ update_attributes(attributes)
+ save
end
- # @return [Ohm::Key] A key scoped to the model which uses this object's
- # id.
- #
- # @see http://github.com/soveran/nest The Nest library.
- def key
- self.class.key[id]
+ # Write the dictionary of key-value pairs to the model.
+ def update_attributes(atts)
+ atts.each { |att, val| send(:"#{att}=", val) }
end
protected
- attr_writer :id
+ def self.to_reference
+ name.to_s.
+ match(/^(?:.*::)*(.*)$/)[1].
+ gsub(/([a-z\d])([A-Z])/, '\1_\2').
+ downcase.to_sym
+ end
+ def self.indices
+ @indices ||= []
+ end
- # Write all the attributes and counters of this object. The operation
- # is actually a 2-step process:
- #
- # 1. Delete the current key, e.g. Post:2.
- # 2. Set all of the new attributes (using HMSET).
- #
- # The DEL and HMSET operations are wrapped in a MULTI EXEC block to ensure
- # the atomicity of the write operation.
- #
- # @see http://code.google.com/p/redis/wiki/DelCommand DEL in the
- # Redis Command Reference.
- # @see http://code.google.com/p/redis/wiki/HmsetCommand HMSET in the
- # Redis Command Reference.
- # @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC
- # in the Redis Command Reference.
- def write
- unless (attributes + counters).empty?
- atts = (attributes + counters).inject([]) { |ret, att|
- value = send(att).to_s
+ def self.uniques
+ @uniques ||= []
+ end
- ret.push(att, value) if not value.empty?
- ret
- }
+ def self.collections
+ @collections ||= []
+ end
- db.multi do
- key.del
- key.hmset(*atts.flatten) if atts.any?
- end
+ 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| toindices(k, v) }.flatten
end
- # Write a single attribute both locally and remotely. It's very important
- # to know that this method skips validation checks, therefore you must
- # ensure data integrity and validity in your application code.
- #
- # @param [Symbol, String] att The name of the attribute to write.
- # @param [#to_s] value The value of the attribute to write.
- #
- # @see http://code.google.com/p/redis/wiki/HdelCommand HDEL in the
- # Redis Command Reference.
- # @see http://code.google.com/p/redis/wiki/HsetCommand HSET in the
- # Redis Command Reference.
- def write_remote(att, value)
- write_local(att, value)
+ def self.toindices(att, val)
+ raise IndexNotFound unless indices.include?(att)
- if value.to_s.empty?
- key.hdel(att)
+ if val.kind_of?(Enumerable)
+ val.map { |v| key[:indices][att][v] }
else
- key.hset(att, value)
+ [key[:indices][att][val]]
end
end
- # Wraps any missing constants lazily in {Ohm::Model::Wrapper} delaying
- # the evaluation of constants until they are actually needed.
- #
- # @see Ohm::Model::Wrapper
- # @see http://en.wikipedia.org/wiki/Lazy_evaluation Lazy evaluation
- def self.const_missing(name)
- wrapper = Wrapper.new(name) { const_get(name) }
-
- # Allow others to hook to const_missing.
- begin
- super(name)
- rescue NameError
- end
-
- wrapper
+ def self.new_id
+ key[:id].incr
end
- private
+ attr_writer :id
- # Provides access to the Redis database. This is shared accross all models and instances.
- def self.db
- return Ohm.redis unless defined?(@options)
-
- Ohm.threaded[self] ||= Redis.connect(@options)
+ def transaction
+ txn = Transaction.new { |t| yield t }
+ txn.commit(db)
end
- # Allows you to do key manipulations scoped solely to your class.
- def self.key
- Key.new(self, db)
+ def model
+ self.class
end
- def self.exists?(id)
- key[:all].sismember(id)
+ def db
+ model.db
end
- # The meat of the ID generation code for Ohm. For cases where you want to
- # customize ID generation (i.e. use GUIDs or Base62 ids) then you simply
- # override this method in your model.
- #
- # @example
- #
- # module UUID
- # def self.new
- # `uuidgen`.strip
- # end
- # end
- #
- # class Post < Ohm::Model
- #
- # private
- # def initialize_id
- # @id ||= UUID.new
- # end
- # end
- #
- def initialize_id
- @id ||= self.class.key[:id].incr.to_s
+ def _initialize_id
+ @id = model.new_id.to_s
end
- def db
- self.class.db
+ def _skip_empty(atts)
+ {}.tap do |ret|
+ atts.each do |att, val|
+ ret[att] = send(att).to_s unless val.to_s.empty?
+ end
+ end
end
- def delete_attributes(atts)
- db.del(*atts.map { |att| key[att] })
+ def _unique_keys
+ model.uniques.map { |att| model.key[:uniques][att] }
end
- def create_model_membership
- self.class.all << self
- end
-
- def delete_model_membership
+ def _save
key.del
- self.class.all.delete(self)
+ key.hmset(*_skip_empty(attributes).flatten)
end
- def update_indices
- delete_from_indices
- add_to_indices
+ def _verify_uniques
+ if att = _detect_duplicate
+ raise UniqueIndexViolation, "#{att} is not unique."
+ end
end
- def add_to_indices
- indices.each do |att|
- next add_to_index(att) unless collection?(send(att))
- send(att).each { |value| add_to_index(att, value) }
+ def _detect_duplicate
+ model.uniques.detect do |att|
+ id = model.key[:uniques][att].hget(send(att))
+ id && id != self.id.to_s
end
end
- def collection?(value)
- self.class.collection?(value)
+ def _read_index_type(type)
+ {}.tap do |ret|
+ model.send(type).each do |att|
+ ret[att] = send(att)
+ end
+ end
end
- def self.collection?(value)
- value.kind_of?(Enumerable) &&
- value.kind_of?(String) == false
+ def _save_uniques(uniques)
+ uniques.each do |att, val|
+ model.key[:uniques][att].hset(val, id)
+ end
end
- def add_to_index(att, value = send(att))
- index = index_key_for(att, value)
- index.sadd(id)
- key[:_indices].sadd(index)
- end
-
- def delete_from_indices
- key[:_indices].smembers.each do |index|
- db.srem(index, id)
+ def _delete_uniques(atts)
+ model.uniques.each do |att|
+ model.key[:uniques][att].hdel(atts[att.to_s])
end
-
- key[:_indices].del
end
- # Get the value of a specific attribute. An important fact about
- # attributes in Ohm is that they are all loaded lazily.
- #
- # @param [Symbol] att The attribute you you want to get.
- # @return [String] The value of att.
- def read_local(att)
- @_attributes[att]
- end
+ def _delete_indices(atts)
+ model.indices.each do |att|
+ val = atts[att.to_s]
- # Write the value of an attribute locally, without persisting it.
- #
- # @param [Symbol] att The attribute you want to set.
- # @param [#to_s] value The value of the attribute you want to set.
- def write_local(att, value)
- @_attributes[att] = value
- end
-
- # Used internally be the @_attributes hash to lazily load attributes
- # when you need them. You may also use this in your code if you know what
- # you are doing.
- #
- # @param [Symbol] att The attribute you you want to get.
- # @return [String] The value of att.
- def read_remote(att)
- unless new?
- value = key.hget(att)
- value.respond_to?(:force_encoding) ?
- value.force_encoding("UTF-8") :
- value
+ if val
+ model.key[:indices][att][val].srem(id)
+ end
end
end
- # Read attributes en masse locally.
- def read_locals(attrs)
- attrs.map do |att|
- send(att)
+ def _save_indices(indices)
+ indices.each do |att, val|
+ model.toindices(att, val).each do |index|
+ index.sadd(id)
+ end
end
end
+ end
- # Read attributes en masse remotely.
- def read_remotes(attrs)
- attrs.map do |att|
- read_remote(att)
- end
- end
+ class Lua
+ attr :dir
+ attr :redis
+ attr :files
+ attr :scripts
- # Get the index name for a specific index and value pair. The return value
- # is an instance of {Ohm::Key}, which you can readily do Redis operations
- # on.
- #
- # @example
- #
- # class Post < Ohm::Model
- # attribute :title
- # index :title
- # end
- #
- # post = Post.create(:title => "Foo")
- # key = Post.index_key_for(:title, "Foo")
- # key == "Post:title:Rm9v"
- # key.scard == 1
- # key.smembers == [post.id]
- # # => true
- #
- # @param [Symbol] name The name of the index.
- # @param [#to_s] value The value for the index.
- # @return [Ohm::Key] A {Ohm::Key key} which you can treat as a string,
- # but also do Redis operations on.
- def self.index_key_for(name, value)
- raise IndexNotFound, name unless indices.include?(name)
- key[name][encode(value)]
+ def initialize(dir, redis)
+ @dir = dir
+ @redis = redis
+ @files = Hash.new { |h, cmd| h[cmd] = read(cmd) }
+ @scripts = {}
end
- # Thin wrapper around {Ohm::Model.index_key_for}.
- def index_key_for(att, value)
- self.class.index_key_for(att, value)
+ def run_file(file, options)
+ run(files[file], options)
end
- # Lock the object so no other instances can modify it.
- # This method implements the design pattern for locks
- # described at: http://code.google.com/p/redis/wiki/SetnxCommand
- #
- # @see Model#mutex
- def lock!
- until key[:_lock].setnx(Time.now.to_f + 0.5)
- next unless timestamp = key[:_lock].get
- sleep(0.1) and next unless lock_expired?(timestamp)
+ def run(script, options)
+ keys = options[:keys]
+ argv = options[:argv]
- break unless timestamp = key[:_lock].getset(Time.now.to_f + 0.5)
- break if lock_expired?(timestamp)
+ begin
+ redis.evalsha(sha(script), keys.size, *keys, *argv)
+ rescue RuntimeError
+ redis.eval(script, keys.size, *keys, *argv)
end
end
- # Release the lock.
- # @see Model#mutex
- def unlock!
- key[:_lock].del
+ private
+ def read(file)
+ File.read("%s/%s.lua" % [dir, file])
end
- def lock_expired?(timestamp)
- timestamp.to_f < Time.now.to_f
+ def sha(script)
+ Digest::SHA1.hexdigest(script)
end
end
end