lib/ohm.rb in ohm-0.0.32 vs lib/ohm.rb in ohm-0.0.33
- old
+ new
@@ -2,22 +2,28 @@
require "base64"
require File.join(File.dirname(__FILE__), "ohm", "redis")
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")
+require File.join(File.dirname(__FILE__), "ohm", "collection")
module Ohm
# Provides access to the Redis database. This is shared accross all models and instances.
def redis
- Thread.current[:redis] ||= Ohm::Redis.new(*options)
+ threaded[:redis] ||= connection(*options)
end
def redis=(connection)
- Thread.current[:redis] = connection
+ threaded[:redis] = connection
end
+ def threaded
+ Thread.current[:ohm] ||= {}
+ end
+
# 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.
@@ -28,71 +34,79 @@
def connect(*options)
self.redis = nil
@options = options
end
+ # Return a connection to Redis.
+ #
+ # This is a wapper around Ohm::Redis.new(options)
+ def connection(*options)
+ Ohm::Redis.new(*options)
+ end
+
def options
- @options
+ @options || []
end
# Clear the database.
def flush
redis.flushdb
end
# Join the parameters with ":" to create a key.
def key(*args)
- args.join(":")
+ Key[*args]
end
- module_function :key, :connect, :flush, :redis, :redis=, :options
+ module_function :key, :connect, :connection, :flush, :redis, :redis=, :options, :threaded
- module Attributes
+ Error = Class.new(StandardError)
+
+ class Model
class Collection
include Enumerable
- attr_accessor :key, :db, :model
+ attr :raw
+ attr :model
- def initialize(db, key, model = nil)
- self.db = db
- self.key = key
- self.model = model
+ def initialize(key, model, db = model.db)
+ @raw = self.class::Raw.new(key, db)
+ @model = model
end
+ def <<(model)
+ raw << model.id
+ end
+
+ alias add <<
+
def each(&block)
- all.each(&block)
+ raw.each do |id|
+ block.call(model[id])
+ end
end
- # Return instances of model for all the ids contained in the collection.
- def all
- instantiate(raw)
+ def key
+ raw.key
end
- # Return the values as model instances, ordered by the options supplied.
- # Check redis documentation to see what values you can provide to each option.
- #
- # @param options [Hash] options to sort the collection.
- # @option options [#to_s] :by Model attribute to sort the instances by.
- # @option options [#to_s] :order (ASC) Sorting order, which can be ASC or DESC.
- # @option options [Integer] :limit (all) Number of items to return.
- # @option options [Integer] :start (0) An offset from where the limit will be applied.
- #
- # @example Get the first ten users sorted alphabetically by name:
- #
- # @event.attendees.sort(:by => :name, :order => "ALPHA", :limit => 10)
- #
- # @example Get five posts sorted by number of votes and starting from the number 5 (zero based):
- #
- # @blog.posts.sort(:by => :votes, :start => 5, :limit => 10")
- def sort(options = {})
- return [] if empty?
- options[:start] ||= 0
- options[:limit] = [options[:start], options[:limit]] if options[:limit]
- result = db.sort(key, options)
- options[:get] ? result : instantiate(result)
+ def first(options = {})
+ if options[:by]
+ sort_by(options.delete(:by), options.merge(:limit => 1)).first
+ else
+ model[raw.first(options)]
+ end
end
+ def [](index)
+ model[raw[index]]
+ end
+
+ def sort(*args)
+ raw.sort(*args).map(&model)
+ end
+
# Sort the model instances by the given attribute.
#
# @example Sorting elements by name:
#
# User.create :name => "B"
@@ -100,167 +114,62 @@
#
# user = User.all.sort_by(:name, :order => "ALPHA").first
# user.name == "A"
# # => true
def sort_by(att, options = {})
- sort(options.merge(:by => model.key("*", att)))
- end
+ options.merge!(:by => model.key("*", att))
- # Sort the model instances by id and return the first instance
- # found. If a :by option is provided with a valid attribute name, the
- # method sort_by is used instead and the option provided is passed as the
- # first parameter.
- #
- # @see #sort
- # @see #sort_by
- # @return [Ohm::Model, nil] Returns the first instance found or nil.
- def first(options = {})
- options = options.merge(:limit => 1)
- options[:by] ?
- sort_by(options.delete(:by), options).first :
- sort(options).first
+ if options[:get]
+ raw.sort(options.merge(:get => model.key("*", options[:get])))
+ else
+ sort(options)
+ end
end
- def to_ary
- all
+ def delete(model)
+ raw.delete(model.id)
+ model
end
- def ==(other)
- to_ary == other
+ def clear
+ raw.clear
end
- # @return [true, false] Returns whether or not the collection is empty.
- def empty?
- size.zero?
- end
-
- # Clears the values in the collection.
- def clear
- db.del(key)
+ def concat(models)
+ raw.concat(models.map { |model| model.id })
self
end
- # Appends the given values to the collection.
- def concat(values)
- values.each { |value| self << value }
+ def replace(models)
+ raw.replace(models.map { |model| model.id })
self
end
- # Replaces the collection with the passed values.
- def replace(values)
- clear
- concat(values)
+ def include?(model)
+ raw.include?(model.id)
end
- # @param value [Ohm::Model#id] Adds the id of the object if it's an Ohm::Model.
- def add(model)
- self << model.id
+ def empty?
+ raw.empty?
end
- private
-
- def instantiate(raw)
- model ? raw.collect { |id| model[id] } : raw
- end
- end
-
- # Represents a Redis list.
- #
- # @example Use a list attribute.
- #
- # class Event < Ohm::Model
- # attribute :name
- # list :participants
- # end
- #
- # event = Event.create :name => "Redis Meeting"
- # event.participants << "Albert"
- # event.participants << "Benoit"
- # event.participants.all #=> ["Albert", "Benoit"]
- class List < Collection
-
- # @param value [#to_s] Pushes value to the tail of the list.
- def << value
- db.rpush(key, value)
- end
-
- alias push <<
-
- # @return [String] Return and remove the last element of the list.
- def pop
- db.rpop(key)
- end
-
- # @return [String] Return and remove the first element of the list.
- def shift
- db.lpop(key)
- end
-
- # @param value [#to_s] Pushes value to the head of the list.
- def unshift(value)
- db.lpush(key, value)
- end
-
- # @return [Array] Elements of the list.
- def raw
- db.list(key)
- end
-
- # @return [Integer] Returns the number of elements in the list.
def size
- db.llen(key)
+ raw.size
end
- def include?(value)
- raw.include?(value)
+ def all
+ raw.to_a.map(&model)
end
- def inspect
- "#<List: #{raw.inspect}>"
- end
+ alias to_a all
end
- # Represents a Redis set.
- #
- # @example Use a set attribute.
- #
- # class Company < Ohm::Model
- # attribute :name
- # set :employees
- # end
- #
- # company = Company.create :name => "Redis Co."
- # company.employees << "Albert"
- # company.employees << "Benoit"
- # company.employees.all #=> ["Albert", "Benoit"]
- # company.include?("Albert") #=> true
class Set < Collection
+ Raw = Ohm::Set
- # @param value [#to_s] Adds value to the list.
- def << value
- db.sadd(key, value)
- end
-
- def delete(value)
- db.srem(key, value)
- end
-
- def include?(value)
- db.sismember(key, value)
- end
-
- def raw
- db.smembers(key)
- end
-
- # @return [Integer] Returns the number of elements in the set.
- def size
- db.scard(key)
- end
-
def inspect
- "#<Set: #{raw.inspect}>"
+ "#<Set (#{model}): #{all.inspect}>"
end
# Returns an intersection with the sets generated from the passed hash.
#
# @see Ohm::Model.find
@@ -283,40 +192,46 @@
private
# Apply a redis operation on a collection of sets.
def apply(operation, hash, glue)
- indices = keys(hash).unshift(key).uniq
- target = indices.join(glue)
- db.send(operation, target, *indices)
- self.class.new(db, target, model)
+ target = key.volatile.group(glue).append(*keys(hash))
+ model.db.send(operation, target, *target.parts)
+ Set.new(target, model)
end
# Transform a hash of attribute/values into an array of keys.
def keys(hash)
- hash.inject([]) do |acc, t|
- acc + Array(t[1]).map do |v|
- model.index_key_for(t[0], v)
+ [].tap do |keys|
+ hash.each do |key, values|
+ values = [values] unless values.kind_of?(Array) # Yes, Array() is different in 1.8.x.
+ values.each do |v|
+ keys << model.index_key_for(key, v)
+ end
end
end
end
end
- class Index < Set
+ class List < Collection
+ Raw = Ohm::List
+
def inspect
- "#<Index: #{raw.inspect}>"
+ "#<List (#{model}): #{all.inspect}>"
end
+ end
- def clear
- raise Ohm::Model::CannotDeleteIndex
+ class Index < Set
+ def apply(operation, hash, glue)
+ if hash.keys.size == 1
+ return Set.new(keys(hash).first, model)
+ else
+ super
+ end
end
end
- end
- Error = Class.new(StandardError)
-
- class Model
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.
@@ -337,16 +252,10 @@
def message
"You tried to perform an operation that needs the model ID, but it's not present."
end
end
- class CannotDeleteIndex < Error
- def message
- "You tried to delete an internal index used by Ohm."
- end
- end
-
class IndexNotFound < Error
def initialize(att)
@att = att
end
@@ -397,21 +306,21 @@
# Defines a list attribute for the model. It can be accessed only after the model instance
# is created.
#
# @param name [Symbol] Name of the list.
def self.list(name, model = nil)
- attr_list_reader(name, model)
+ attr_collection_reader(name, :List, 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.
#
# @param name [Symbol] Name of the set.
def self.set(name, model = nil)
- attr_set_reader(name, model)
+ attr_collection_reader(name, :Set, model)
collections << name unless collections.include?(name)
end
# Creates an index (a set) that will be used for finding instances.
#
@@ -430,21 +339,122 @@
# @param name [Symbol] Name of the attribute to be indexed.
def self.index(att)
indices << att unless indices.include?(att)
end
- def self.attr_list_reader(name, model = nil)
- define_method(name) do
- instance_variable_get("@#{name}") ||
- instance_variable_set("@#{name}", Attributes::List.new(db, key(name), model))
+ # Define a reference to another object.
+ #
+ # @example
+ # class Comment < Ohm::Model
+ # attribute :content
+ # reference :post, Post
+ # end
+ #
+ # @post = Post.create :content => "Interesting stuff"
+ #
+ # @comment = Comment.create(:content => "Indeed!", :post => @post)
+ #
+ # @comment.post.content
+ # # => "Interesting stuff"
+ #
+ # @comment.post = Post.create(:content => "Wonderful stuff")
+ #
+ # @comment.post.content
+ # # => "Wonderful stuff"
+ #
+ # @comment.post.update(:content => "Magnific stuff")
+ #
+ # @comment.post.content
+ # # => "Magnific stuff"
+ #
+ # @comment.post = nil
+ #
+ # @comment.post
+ # # => nil
+ #
+ # @see Ohm::Model::collection
+ def self.reference(name, model)
+ reader = :"#{name}_id"
+ writer = :"#{name}_id="
+
+ attribute reader
+ index reader
+
+ define_memoized_method(name) do
+ model[send(reader)]
end
+
+ define_method(:"#{name}=") do |value|
+ instance_variable_set("@#{name}", nil)
+ send(writer, value ? value.id : nil)
+ end
+
+ define_method(writer) do |value|
+ instance_variable_set("@#{name}", nil)
+ write_local(reader, value)
+ end
end
- def self.attr_set_reader(name, 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
+ #
+ # class Post < Ohm::Model
+ # attribute :content
+ # collection :comments, Comment
+ # reference :author, Person
+ # end
+ #
+ # class Person < Ohm::Model
+ # attribute :name
+ #
+ # # When the name of the reference cannot be inferred,
+ # # 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
+ # @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.
+ #
+ # @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.
+ def self.collection(name, model, reference = to_reference)
+ define_method(name) { model.find(:"#{reference}_id" => send(:id)) }
+ end
+
+ def self.to_reference
+ name.to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
+ end
+
+ def self.attr_collection_reader(name, type, model)
+ if model
+ define_memoized_method(name) { Ohm::Model::const_get(type).new(key(name), model, db) }
+ else
+ define_memoized_method(name) { Ohm::const_get(type).new(key(name), db) }
+ end
+ end
+
+ def self.define_memoized_method(name, &block)
define_method(name) do
instance_variable_get("@#{name}") ||
- instance_variable_set("@#{name}", Attributes::Set.new(db, key(name), model))
+ instance_variable_set("@#{name}", instance_eval(&block))
end
end
def self.[](id)
new(:id => id) if exists?(id)
@@ -453,11 +463,11 @@
def self.to_proc
Proc.new { |id| self[id] }
end
def self.all
- @all ||= Attributes::Index.new(db, key(:all), self)
+ @all ||= Ohm::Model::Index.new(key(:all), self)
end
def self.attributes
@@attributes[self]
end
@@ -586,12 +596,13 @@
# Lock the object before executing the block, and release it once the block is done.
def mutex
lock!
yield
- unlock!
self
+ ensure
+ unlock!
end
def inspect
everything = (attributes + collections + counters).map do |att|
value = begin
@@ -604,10 +615,29 @@
end
"#<#{self.class}:#{new? ? "?" : id} #{everything.map {|e| e.join("=") }.join(" ")}>"
end
+ # Makes the model connect to a different Redis instance.
+ #
+ # @example
+ #
+ # class Post < Ohm::Model
+ # connect :port => 6380, :db => 2
+ #
+ # attribute :body
+ # end
+ #
+ # # 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)
+ #
+ def self.connect(*options)
+ self.db = Ohm.connection(*options)
+ end
+
protected
def key(*args)
self.class.key(id, *args)
end
@@ -621,33 +651,39 @@
# Write attributes using SET
# This method will be removed once MSET becomes standard.
def write_with_set
attributes.each do |att|
- (value = send(att)) ?
+ value = send(att)
+ value.to_s.empty? ?
db.set(key(att), value) :
db.del(key(att))
end
end
# Write attributes using MSET
# This is the preferred method, and will be the only option
# available once MSET becomes standard.
def write_with_mset
unless attributes.empty?
- rems, adds = attributes.map { |a| [key(a), send(a)] }.partition { |t| t.last.nil? }
+ rems, adds = attributes.map { |a| [key(a), send(a)] }.partition { |t| t.last.to_s.empty? }
db.del(*rems.flatten.compact) unless rems.empty?
db.mset(adds.flatten) unless adds.empty?
end
end
private
+ # Provides access to the Redis database. This is shared accross all models and instances.
def self.db
- Ohm.redis
+ Ohm.threaded[self] || Ohm.redis
end
+ def self.db=(connection)
+ Ohm.threaded[self] = connection
+ end
+
def self.key(*args)
Ohm.key(*args.unshift(self))
end
def self.exists?(id)
@@ -657,10 +693,10 @@
def initialize_id
self.id = db.incr(self.class.key("id")).to_s
end
def db
- Ohm.redis
+ self.class.db
end
def delete_attributes(atts)
atts.each do |att|
db.del(key(att))