lib/ohm.rb in ohm-0.1.0.rc6 vs lib/ohm.rb in ohm-0.1.0
- old
+ new
@@ -9,112 +9,339 @@
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. This is shared accross all models and instances.
+ # 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.
+ #
+ # The better way to access the _Redis_ database and do raw _Redis_
+ # commands would be one of the following:
+ #
+ # 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.
+ #
+ # 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}.
+ #
+ # @example
+ #
+ # class Post < Ohm::Model
+ # def comment_ids
+ # key[:comments].zrange(0, -1)
+ # end
+ #
+ # def add_comment_id(id)
+ # key[:comments].zadd(Time.now.to_i, id)
+ # end
+ #
+ # def remove_comment_id(id)
+ # # Let's use the db style here just to demonstrate.
+ # db.zrem key[:comments], id
+ # end
+ # end
+ #
+ # Post.key[:latest].sadd(1)
+ # Post.key[:latest].smembers == ["1"]
+ # # => true
+ #
+ # Post.key[:latest] == "Post:latest"
+ # # => true
+ #
+ # 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)
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
end
+ # @private Used internally by Ohm for thread safety.
def self.threaded
Thread.current[:ohm] ||= {}
end
- # Connect to a redis database.
+ # Connect to a _Redis_ database.
#
- # @param options [Hash] options to create a message with.
- # @option options [#to_s] :host ('127.0.0.1') Host of the redis database.
- # @option options [#to_s] :port (6379) Port number.
- # @option options [#to_s] :db (0) Database number.
- # @option options [#to_s] :timeout (0) Database timeout in seconds.
+ # It is also worth mentioning that you can pass in a *URI* e.g.
+ #
+ # Ohm.connect :url => "redis://127.0.0.1:6379/0"
+ #
+ # Note that the value *0* refers to the database number for the given
+ # _Redis_ instance.
+ #
+ # Also you can use {Ohm.connect} without any arguments. The behavior will
+ # be as follows:
+ #
+ # # Connect to redis://127.0.0.1:6379/0
+ # Ohm.connect
+ #
+ # # Connect to redis://10.0.0.100:22222/5
+ # ENV["REDIS_URL"] = "redis://10.0.0.100:22222/5"
+ # Ohm.connect
+ #
+ # @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
end
- # Return a connection to Redis.
+ # @private Return a connection to Redis.
#
- # This is a wapper around Redis.connect(options)
+ # 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.
+ # 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.
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
+ # 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
# Wraps a model name for lazy evaluation.
class Wrapper < BasicObject
+
+ # 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
class << self
def method_missing(method_id, *args)
- ::Kernel.raise ::NoMethodError, "You tried to call #{@name}##{method_id}, but #{@name} is not defined on #{@caller}"
+ ::Kernel.raise(
+ ::NoMethodError,
+ "You tried to call %s#%s, but %s is not defined on %s" % [
+ @name, method_id, @name, @caller
+ ]
+ )
end
end
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 }
end
+ # Evaluates the passed block in {Ohm::Model::Wrapper#initialize}.
+ #
+ # @return [Class] The wrapped class.
def unwrap
@block.call
end
+ # Since {Ohm::Model::Wrapper} is a subclass of _BasicObject_ we have
+ # to manually declare this.
+ #
+ # @return [Wrapper]
def class
Wrapper
end
+ # @return [String] A string describing this lazy object.
def inspect
"<Wrapper for #{@name} (in #{@caller})>"
end
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
+ # 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
end
+ # Adds an instance of {Ohm::Model} to this collection.
+ #
+ # @param [#id] model A model with an ID.
def add(model)
self << model
end
- def sort(_options = {})
+ # 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
- options = _options.dup
- options[:start] ||= 0
- options[:limit] = [options[:start], options[:limit]] if options[:limit]
+ opts = options.dup
+ opts[:start] ||= 0
+ opts[:limit] = [opts[:start], opts[:limit]] if opts[:limit]
- key.sort(options).map(&model)
+ key.sort(opts).map(&model)
end
# Sort the model instances by the given attribute.
#
# @example Sorting elements by name:
@@ -123,146 +350,500 @@
# User.create :name => "A"
#
# user = User.all.sort_by(:name, :order => "ALPHA").first
# user.name == "A"
# # => true
- def sort_by(att, _options = {})
+ #
+ # @see file:README.html#sorting Sorting in the README.
+ def sort_by(att, options = {})
return [] unless key.exists
- options = _options.dup
- options.merge!(:by => model.key["*->#{att}"])
+ opts = options.dup
+ opts.merge!(:by => model.key["*->#{att}"])
- if options[:get]
- key.sort(options.merge(:get => model.key["*->#{options[:get]}"]))
+ if opts[:get]
+ key.sort(opts.merge(:get => model.key["*->#{opts[:get]}"]))
else
- sort(options)
+ sort(opts)
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
+ # 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
+ # @return [true, false] Whether or not this collection is empty.
def empty?
!key.exists
end
+ # @return [Array] Array representation of this collection.
def to_a
all
end
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[id]) }
end
+ # 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)
end
- def << model
+ # 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 <<
+ # 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
+ # 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
+ # 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
+ # 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
+ # 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)
end
- def first(_options = {})
- options = _options.dup
- options.merge!(:limit => 1)
+ # 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)
- if options[:by]
- sort_by(options.delete(:by), options).first
+ if opts[:by]
+ sort_by(opts.delete(:by), opts).first
else
- sort(options).first
+ sort(opts).first
end
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 inspect
"#<Set (#{model}): #{key.smembers.inspect}>"
end
protected
-
+ # @private
def apply(operation, key, source, target)
target.send(operation, key, *source)
Set.new(target, Wrapper.wrap(model))
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) # Yes, Array() is different in 1.8.x.
+ values = [values] unless values.kind_of?(Array)
values.each do |v|
keys << model.index_key_for(key, v)
end
end
end
end
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
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[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
+ # `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[id] }
when Pattern[Range, nil] then
@@ -270,92 +851,215 @@
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
end
+ # All validations which need to access the _Redis_ database goes 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.
+ # 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(attrs)
- result = db.sinter(*Array(attrs).map { |att| index_key_for(att, send(att)) })
- assert result.empty? || !new? && result.include?(id.to_s), [attrs, :not_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
end
include Validations
+ # Raised when you try and get the *id* of an {Ohm::Model} without an id.
+ #
+ # class Post < Ohm::Model
+ # list :comments, Comment
+ # end
+ #
+ # class Comment < Ohm::Model
+ # end
+ #
+ # ex = nil
+ # begin
+ # Post.new.id
+ # rescue Exception => e
+ # ex = e
+ # end
+ #
+ # ex.kind_of?(Ohm::Model::MissingID)
+ # # => 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.
+ #
+ # # following from the example above:
+ # post = Post.new
+ #
+ # ex = nil
+ # begin
+ # post.comments << Comment.new
+ # rescue Exception => e
+ # ex = e
+ # end
+ #
+ # ex.kind_of?(Ohm::Model::MissingID)
+ # # => true
+ #
+ # # Correct way:
+ # post = Post.new
+ #
+ # 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."
+ "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.
+ #
+ # class Post < Ohm::Model
+ # attribute :title
+ # end
+ #
+ # post = Post.create(:title => "Ohm")
+ #
+ # ex = nil
+ # begin
+ # Post.find(:title => "Ohm")
+ # rescue Exception => e
+ # ex = e
+ # end
+ #
+ # ex.kind_of?(Ohm::Model::IndexNotFound)
+ # # => true
+ #
+ # To correct this problem, simply define a _:title_ *index* in your class.
+ #
+ # class Post < Ohm::Model
+ # attribute :title
+ # index :title
+ # end
class IndexNotFound < Error
def initialize(att)
@att = att
end
def message
"Index #{@att.inspect} not found."
end
end
- @@attributes = Hash.new { |hash, key| hash[key] = [] }
+ @@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] = [] }
+ @@counters = Hash.new { |hash, key| hash[key] = [] }
+ @@indices = Hash.new { |hash, key| hash[key] = [] }
- attr_writer :id
-
def id
@id or raise MissingID
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.
+ # 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.
#
+ # If you're looking to have typecasting built in, you may want to look at
+ # Ohm::Typecast in Ohm::Contrib.
+ #
# @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
@@ -364,63 +1068,89 @@
end
attributes << name unless attributes.include?(name)
end
- # Defines a counter attribute for the model. This attribute can't be assigned, only incremented
- # or decremented. It will be zero by default.
+ # Defines a counter attribute for the model. This attribute can't be
+ # assigned, only incremented or decremented. It will be zero by default.
#
- # @param name [Symbol] Name of the counter.
+ # @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.
+ # 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.
#
- # @param name [Symbol] Name of the list.
+ # @example
+ #
+ # class Post < Ohm::Model
+ # list :comments, Comment
+ # end
+ #
+ # class Comment < Ohm::Model
+ # end
+ #
+ # # WRONG!!!
+ # post = Post.new
+ # post.comments << Comment.create
+ #
+ # # 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 retrival order is irrelevant, and
- # operations like union, join, and membership checks are important.
+ # 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 name [Symbol] Name of the set.
+ # @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)
end
# Creates an index (a set) that will be used for finding instances.
#
- # If you want to find a model instance by some attribute value, then an index for that
- # attribute must exist.
+ # If you want to find a model instance by some attribute value, then an
+ # index for that attribute must exist.
#
# @example
+ #
# class User < Ohm::Model
# attribute :email
# index :email
# end
#
# # Now this is possible:
- # User.find email: "ohm@example.com"
+ # User.find :email => "ohm@example.com"
#
- # @param name [Symbol] Name of the attribute to be indexed.
+ # @param [Symbol] name Name of the attribute to be indexed.
def self.index(att)
indices << att unless indices.include?(att)
end
# Define a reference to another object.
#
# @example
+ #
# class Comment < Ohm::Model
# attribute :content
# reference :post, Post
# end
#
@@ -444,11 +1174,12 @@
# @comment.post = nil
#
# @comment.post
# # => nil
#
- # @see Ohm::Model::collection
+ # @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="
@@ -474,12 +1205,12 @@
@_memo.delete(name)
write_local(reader, value)
end
end
- # Define a collection of objects which have a {Ohm::Model::reference reference}
- # to this model.
+ # Define a collection of objects which have a
+ # {Ohm::Model.reference reference} to this model.
#
# class Comment < Ohm::Model
# attribute :content
# reference :post, Post
# end
@@ -497,69 +1228,140 @@
# # you need to specify it in the third param.
# collection :posts, Post, :author
# end
#
# @person = Person.create :name => "Albert"
- # @post = Post.create :content => "Interesting stuff", :author => @person
+ # @post = Post.create :content => "Interesting stuff",
+ # :author => @person
# @comment = Comment.create :content => "Indeed!", :post => @post
#
# @post.comments.first.content
# # => "Indeed!"
#
# @post.author.name
# # => "Albert"
#
- # *Important*: please note that even though a collection is a {Ohm::Set Set},
- # you should not add or remove objects from this collection directly.
+ # *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.
#
- # @see Ohm::Model::reference
+ # @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.
+ # @param reference [Symbol] Reference as defined in the associated
+ # model.
+ #
+ # @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)) }
+ define_method(name) {
+ model.unwrap.find(:"#{reference}_id" => send(:id))
+ }
end
+ # Used by {Ohm::Model.collection} to infer the reference.
+ #
+ # @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
+ 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)
define_method(name) do
@_memo[name] ||= instance_eval(&block)
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)
end
+ # @private Used for conveniently doing [1, 2].map(&Post) for example.
def self.to_proc
Proc.new { |id| self[id] }
end
+ # Returns a {Ohm::Model::Set set} containing all the members of a given
+ # class.
+ #
+ # @example
+ #
+ # class Post < Ohm::Model
+ # end
+ #
+ # post = Post.create
+ #
+ # Post.all.include?(post)
+ # # => true
+ #
+ # post.delete
+ #
+ # Post.all.include?(post)
+ # # => false
def self.all
Ohm::Model::Index.new(key[:all], Wrapper.wrap(self))
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.
+ #
+ # @example
+ # class Post < Ohm::Model
+ # set :authors, Author
+ # list :comments, Comment
+ # end
+ #
+ # Post.collections == [:authors, :comments]
+ # # => true
+ #
+ # @see Ohm::Model.list
+ # @see Ohm::Model.set
def self.collections
@@collections[self]
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.
+ #
+ # @example
+ #
+ # class Post < Ohm::Model
+ # attribute :title
+ # end
+ #
+ # post = Post.create(:title => "A new post")
+ #
+ # @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
end
@@ -569,31 +1371,86 @@
# @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"
#
- # assert_equal [event1], Event.find(author: "Albert", day: "2009-09-09")
+ # [event1] == Event.find(author: "Albert", day: "2009-09-09").to_a
+ # # => true
def self.find(hash)
- raise ArgumentError, "You need to supply a hash with filters. If you want to find by ID, use #{self}[id] instead." unless hash.kind_of?(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)
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", "")
end
+ # Constructor for all subclasses of {Ohm::Model}, which optionally
+ # takes a Hash of attribute value pairs.
+ #
+ # 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
+ #
+ # class User < Ohm::Model
+ # end
+ #
+ # class Post < Ohm::Model
+ # attribute :title
+ # reference :user, User
+ # end
+ #
+ # user = User.create
+ # p1 = Post.new(:title => "Redis", :user_id => user.id)
+ # p1.save
+ #
+ # 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)
end
+ # @return [true, false] Whether or not this object has an id.
def new?
!@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
@@ -601,56 +1458,80 @@
write
add_to_indices
end
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
end
+ # Update this object, optionally accepting new attributes.
+ #
+ # @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.
+ #
+ # @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.
+ #
+ # @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.
#
- # @param att [Symbol] Attribute to increment.
+ # @param [Symbol] att Attribute to increment.
+ # @param [Fixnum] count An optional increment step to use.
def incr(att, count = 1)
- raise ArgumentError, "#{att.inspect} is not a counter." unless counters.include?(att)
+ unless counters.include?(att)
+ raise ArgumentError, "#{att.inspect} is not a counter."
+ end
+
write_local(att, key.hincrby(att, count))
end
# Decrement the counter denoted by :att.
#
- # @param att [Symbol] Attribute to decrement.
+ # @param [Symbol] att Attribute to decrement.
+ # @param [Fixnum] count An optional decrement step to use.
def decr(att, count = 1)
incr(att, -count)
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.
+ # approach of providing all the attributes and instead favors a white
+ # listed approach.
#
# @example
#
# person = Person.create(:name => "John Doe")
# person.to_hash == { :id => '1' }
@@ -682,50 +1563,118 @@
attrs[:id] = id unless new?
attrs[:errors] = errors unless errors.empty?
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}.
+ #
+ # @example
+ # require "json"
+ #
+ # class Post < Ohm::Model
+ # attribute :title
+ #
+ # def to_hash
+ # super.merge(:title => title)
+ # end
+ # end
+ #
+ # p1 = Post.create(:title => "Delta Force")
+ # p1.to_hash == { :id => "1", :title => "Delta Force" }
+ # # => true
+ #
+ # p1.to_json == "{\"id\":\"1\",\"title\":\"Delta Force\"}"
+ # # => true
+ #
+ # @return [String] The JSON representation of this object defined in
+ # terms of {#to_hash}.
def to_json(*args)
to_hash.to_json(*args)
end
+ # Convenience wrapper for {Ohm::Model.attributes}.
def attributes
self.class.attributes
end
+ # Convenience wrapper for {Ohm::Model.counters}.
def counters
self.class.counters
end
+ # Convenience wrapper for {Ohm::Model.collections}.
def collections
self.class.collections
end
+ # Convenience wrapper for {Ohm::Model.indices}.
def indices
self.class.indices
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? :==
+ # 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
end
- # Lock the object before executing the block, and release it once the block is done.
+ # Lock the object before executing the block, and release it once the
+ # block is done.
+ #
+ # This is used during {#create} and {#save} to ensure that no race
+ # conditions occur.
+ #
+ # @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.
+ #
+ # Useful for debugging and for doing irb work.
def inspect
everything = (attributes + collections + counters).map do |att|
value = begin
send(att)
rescue MissingID
@@ -733,15 +1682,26 @@
end
[att, value.inspect]
end
- "#<#{self.class}:#{new? ? "?" : id} #{everything.map {|e| e.join("=") }.join(" ")}>"
+ sprintf("#<%s:%s %s>",
+ self.class,
+ new? ? "?" : id,
+ everything.map {|e| e.join("=") }.join(" ")
+ )
end
- # Makes the model connect to a different Redis instance.
+ # 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.
#
+ # 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
#
# class Post < Ohm::Model
# connect :port => 6380, :db => 2
#
@@ -751,20 +1711,41 @@
# # 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)
self.db = Ohm.connection(*options)
end
protected
+ attr_writer :id
+ # @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]
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
@@ -777,20 +1758,36 @@
key.hmset(*atts.flatten) if atts.any?
end
end
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)
if value.to_s.empty?
key.hdel(att)
else
key.hset(att, value)
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
@@ -810,18 +1807,39 @@
def self.db=(connection)
Ohm.threaded[self] = connection
end
+ # Allows you to do key manipulations scoped solely to your class.
def self.key
Key.new(self, db)
end
def self.exists?(id)
key[:all].sismember(id)
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
end
def db
@@ -874,44 +1892,84 @@
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
+ # 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
end
end
+ # Read attributes en masse locally.
def read_locals(attrs)
attrs.map do |att|
send(att)
end
end
+ # Read attributes en masse remotely.
def read_remotes(attrs)
attrs.map do |att|
read_remote(att)
end
end
- def self.index_key_for(att, value)
- raise IndexNotFound, att unless indices.include?(att)
- key[att][encode(value)]
+ # 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)]
end
+ # Thin wrapper around {Ohm::Model.index_key_for}.
def index_key_for(att, value)
self.class.index_key_for(att, value)
end
# Lock the object so no other instances can modify it.
@@ -933,10 +1991,11 @@
# @see Model#mutex
def unlock!
key[:_lock].del
end
- def lock_expired? timestamp
+ def lock_expired?(timestamp)
timestamp.to_f < Time.now.to_f
end
end
end
+