lib/sohm.rb in sohm-0.0.1 vs lib/sohm.rb in sohm-0.9.0
- old
+ new
@@ -31,11 +31,11 @@
# UniqueIndexViolation:
#
# Raised when trying to save an object with a `unique` index for
# which the value already exists.
#
- # Solution: rescue `Ohm::UniqueIndexViolation` during save, but
+ # Solution: rescue `Sohm::UniqueIndexViolation` during save, but
# also, do some validations even before attempting to save.
#
class Error < StandardError; end
class MissingID < Error; end
class IndexNotFound < Error; end
@@ -51,18 +51,18 @@
# 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
+ # class Comment < Sohm::Model
# reference :user, User # NameError undefined constant User.
# end
#
# # Instead of relying on some clever `const_missing` hack, we can
# # simply use a symbol or a string.
#
- # class Comment < Ohm::Model
+ # class Comment < Sohm::Model
# reference :user, :User
# reference :post, "Post"
# end
#
def self.const(context, name)
@@ -74,40 +74,42 @@
end
def self.dict(arr)
Hash[*arr]
end
-
- def self.sort(redis, key, options)
- args = []
-
- args.concat(["BY", options[:by]]) if options[:by]
- args.concat(["GET", options[:get]]) if options[:get]
- args.concat(["LIMIT"] + options[:limit]) if options[:limit]
- args.concat(options[:order].split(" ")) if options[:order]
- args.concat(["STORE", options[:store]]) if options[:store]
-
- redis.call("SORT", key, *args)
- end
end
# Use this if you want to do quick ad hoc redis commands against the
# defined Ohm connection.
#
# Examples:
#
# Ohm.redis.call("SET", "foo", "bar")
# Ohm.redis.call("FLUSH")
#
+ @redis = Redic.new
def self.redis
- @redis ||= Redic.new
+ @redis
end
def self.redis=(redis)
@redis = redis
end
+ # If you are using a Redis pool to override @redis above, chances are
+ # you won't need a mutex(since your opertions will run on different
+ # redis instances), so you can use this to override the default mutex
+ # for better performance
+ @mutex = Mutex.new
+ def self.mutex=(mutex)
+ @mutex = mutex
+ end
+
+ def self.mutex
+ @mutex
+ end
+
# By default, EVALSHA is used
def self.enable_evalsha
defined?(@enable_evalsha) ? @enable_evalsha : true
end
@@ -135,11 +137,10 @@
def empty?
size == 0
end
- # TODO: fix this later
# Wraps the whole pipelining functionality.
def fetch(ids)
data = nil
model.synchronize do
@@ -152,11 +153,11 @@
return [] if data.nil?
[].tap do |result|
data.each_with_index do |atts, idx|
- unless attrs.empty?
+ unless atts.empty?
result << model.new(Utils.dict(atts).update(:id => ids[idx]))
end
end
end
end
@@ -193,14 +194,14 @@
# Returns an array of elements from the list using LRANGE.
# #range receives 2 integers, start and stop
#
# Example:
#
- # class Comment < Ohm::Model
+ # class Comment < Sohm::Model
# end
#
- # class Post < Ohm::Model
+ # class Post < Sohm::Model
# list :comments, :Comment
# end
#
# c1 = Comment.create
# c2 = Comment.create
@@ -244,14 +245,14 @@
# Note: If your list contains the model multiple times, this method
# will delete all instances of that model in one go.
#
# Example:
#
- # class Comment < Ohm::Model
+ # class Comment < Sohm::Model
# end
#
- # class Post < Ohm::Model
+ # class Post < Sohm::Model
# list :comments, :Comment
# end
#
# p = Post.create
# c = Comment.create
@@ -270,14 +271,14 @@
redis.call("LREM", key, 0, model.id)
end
# Returns an array with all the ID's of the list.
#
- # class Comment < Ohm::Model
+ # class Comment < Sohm::Model
# end
#
- # class Post < Ohm::Model
+ # class Post < Sohm::Model
# list :comments, :Comment
# end
#
# post = Post.create
# post.comments.push(Comment.create)
@@ -303,61 +304,10 @@
# Defines most of the methods used by `Set` and `MultiSet`.
class BasicSet
include Collection
- # Allows you to sort by any attribute in the hash, this doesn't include
- # the +id+. If you want to sort by ID, use #sort.
- #
- # 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 => to_key(att)))
- end
-
- # 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] = to_key(options[:get])
- return execute { |key| Utils.sort(redis, key, options) }
- end
-
- fetch(execute { |key| Utils.sort(redis, key, options) })
- end
-
# Check if a model is included in this set.
#
# Example:
#
# u = User.create
@@ -375,38 +325,32 @@
# Returns the total size of the set using SCARD.
def size
execute { |key| redis.call("SCARD", key) }
end
- # Syntactic sugar for `sort_by` or `sort` when you only need the
- # first element.
+ # SMEMBERS then choosing the first will take too much memory in case data
+ # grow big enough, which will be slow in this case.
+ # Providing +sample+ only gives a hint that we won't preserve any order
+ # for this, there will be 2 cases:
#
- # Example:
+ # 1. Anyone in the set will do, this is the original use case of +sample*
+ # 2. For some reasons(maybe due to filters), we only have 1 element left
+ # in this set, using +sample+ will do the trick
#
- # 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])
-
- if opts[:by]
- sort_by(opts.delete(:by), opts).first
- else
- sort(opts).first
- end
+ # For all the other cases, we won't be able to fetch a single element
+ # without fetching all elements first(in other words, doing this
+ # efficiently)
+ def sample
+ model[execute { |key| redis.call("SRANDMEMBER", key) }]
end
# Returns an array with all the ID's of the set.
#
- # class Post < Ohm::Model
+ # class Post < Sohm::Model
# end
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :name
# index :name
#
# set :posts, :Post
# end
@@ -440,14 +384,14 @@
# Returns +true+ if +id+ is included in the set. Otherwise, returns +false+.
#
# Example:
#
- # class Post < Ohm::Model
+ # class Post < Sohm::Model
# end
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# set :posts, :Post
# end
#
# user = User.create
# post = Post.create
@@ -457,19 +401,10 @@
# user.posts.exists?(post.id) # => true
#
def exists?(id)
execute { |key| redis.call("SISMEMBER", key, id) == 1 }
end
-
- private
- def to_key(att)
- if model.counters.include?(att)
- namespace["*:counters->%s" % att]
- else
- namespace["*->%s" % att]
- end
- end
end
class Set < BasicSet
attr :key
attr :namespace
@@ -581,17 +516,17 @@
# a `Set` because it has to `SINTERSTORE` all the keys prior to
# retrieving the members, size, etc.
#
# Example:
#
- # User.all.kind_of?(Ohm::Set)
+ # User.all.kind_of?(Sohm::Set)
# # => true
#
- # User.find(:name => "John").kind_of?(Ohm::Set)
+ # User.find(:name => "John").kind_of?(Sohm::Set)
# # => true
#
- # User.find(:name => "John", :age => 30).kind_of?(Ohm::MultiSet)
+ # User.find(:name => "John", :age => 30).kind_of?(Sohm::MultiSet)
# # => true
#
class MultiSet < BasicSet
attr :namespace
attr :model
@@ -702,11 +637,11 @@
# it, here is a semi-realtime explanation of the details involved
# when creating a User instance.
#
# Example:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :name
# index :name
#
# attribute :email
# unique :email
@@ -738,11 +673,11 @@
# # Store the HASH
# HMSET User:1 name John email foo@bar.com
#
# Next we increment points:
#
- # HINCR User:1:counters points 1
+ # 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
@@ -755,22 +690,22 @@
def self.redis
defined?(@redis) ? @redis : Sohm.redis
end
def self.mutex
- @@mutex ||= Mutex.new
+ Sohm.mutex
end
def self.synchronize(&block)
mutex.synchronize(&block)
end
# Returns the namespace for all the keys generated using this model.
#
# Example:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# end
#
# User.key == "User"
# User.key.kind_of?(String)
# # => true
@@ -780,11 +715,11 @@
#
# To find out more about Nido, see:
# http://github.com/soveran/nido
#
def self.key
- @key ||= Nido.new(self.name)
+ Nido.new(self.name)
end
# Retrieve a record by ID.
#
# Example:
@@ -805,11 +740,11 @@
# 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::List#fetch.
+ # more information checkout the implementation of Sohm::List#fetch.
#
def self.to_proc
lambda { |id| self[id] }
end
@@ -820,11 +755,11 @@
# Find values in indexed fields.
#
# Example:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :email
#
# attribute :name
# index :name
#
@@ -858,13 +793,13 @@
#
def self.find(dict)
keys = filters(dict)
if keys.size == 1
- Ohm::Set.new(keys.first, key, self)
+ Sohm::Set.new(keys.first, key, self)
else
- Ohm::MultiSet.new(key, self, Command.new(:sinterstore, *keys))
+ Sohm::MultiSet.new(key, self, Command.new(:sinterstore, *keys))
end
end
# Retrieve a set of models given an array of IDs.
#
@@ -880,79 +815,79 @@
# use it in `find` statements.
def self.index(attribute)
indices << attribute unless indices.include?(attribute)
end
- # Declare an Ohm::Set with the given name.
+ # Declare an Sohm::Set with the given name.
#
# Example:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# set :posts, :Post
# end
#
# u = User.create
# u.posts.empty?
# # => true
#
# Note: You can't use the set until you save the model. If you try
- # to do it, you'll receive an Ohm::MissingID error.
+ # to do it, you'll receive an Sohm::MissingID error.
#
def self.set(name, model)
track(name)
define_method name do
model = Utils.const(self.class, model)
- Ohm::MutableSet.new(key[name], model.key, model)
+ Sohm::MutableSet.new(key[name], model.key, model)
end
end
- # Declare an Ohm::List with the given name.
+ # Declare an Sohm::List with the given name.
#
# Example:
#
- # class Comment < Ohm::Model
+ # class Comment < Sohm::Model
# end
#
- # class Post < Ohm::Model
+ # class Post < Sohm::Model
# list :comments, :Comment
# end
#
# p = Post.create
# p.comments.push(Comment.create)
# p.comments.unshift(Comment.create)
# p.comments.size == 2
# # => true
#
# Note: You can't use the list until you save the model. If you try
- # to do it, you'll receive an Ohm::MissingID error.
+ # to do it, you'll receive an Sohm::MissingID error.
#
def self.list(name, model)
track(name)
define_method name do
model = Utils.const(self.class, model)
- Ohm::List.new(key[name], model.key, model)
+ Sohm::List.new(key[name], model.key, model)
end
end
# A macro for defining a method which basically does a find.
#
# Example:
- # class Post < Ohm::Model
+ # class Post < Sohm::Model
# reference :user, :User
# end
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# collection :posts, :Post
# end
#
# # is the same as
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# def posts
# Post.find(:user_id => self.id)
# end
# end
#
@@ -966,31 +901,29 @@
# A macro for defining an attribute, an index, and an accessor
# for a given model.
#
# Example:
#
- # class Post < Ohm::Model
+ # class Post < Sohm::Model
# reference :user, :User
# end
#
# # It's the same as:
#
- # class Post < Ohm::Model
+ # class Post < Sohm::Model
# attribute :user_id
# index :user_id
#
# def user
- # @_memo[:user] ||= User[user_id]
+ # User[user_id]
# end
#
# def user=(user)
# self.user_id = user.id
- # @_memo[:user] = user
# end
#
# def user_id=(user_id)
- # @_memo.delete(:user_id)
# self.user_id = user_id
# end
# end
#
def self.reference(name, model)
@@ -1004,32 +937,28 @@
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(name) do
- @_memo[name] ||= begin
- model = Utils.const(self.class, model)
- model[send(reader)]
- end
+ model = Utils.const(self.class, model)
+ model[send(reader)]
end
end
# The bread and butter macro of all models. Basically declares
# persisted attributes. All attributes are stored on the Redis
# hash.
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :name
# end
#
# user = User.new(name: "John")
# user.name
@@ -1040,11 +969,11 @@
# # => "Jane"
#
# A +lambda+ can be passed as a second parameter to add
# typecasting support to the attribute.
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :age, ->(x) { x.to_i }
# end
#
# user = User.new(age: 100)
#
@@ -1087,14 +1016,19 @@
end
serial_attributes << name unless serial_attributes.include?(name)
if cast
define_method(name) do
+ # NOTE: This is a temporary solution, since we might use
+ # composite objects (such as arrays), which won't always
+ # do a reset
+ @serial_attributes_changed = true
cast[@serial_attributes[name]]
end
else
define_method(name) do
+ @serial_attributes_changed = true
@serial_attributes[name]
end
end
define_method(:"#{name}=") do |value|
@@ -1109,30 +1043,30 @@
# `decr` methods, which interact directly with Redis. Their value
# can't be assigned as with regular attributes.
#
# Example:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# counter :points
# end
#
# u = User.create
# u.incr :points
#
# u.points
# # => 1
#
# Note: You can't use counters until you save the model. If you
- # try to do it, you'll receive an Ohm::MissingID error.
+ # try to do it, you'll receive an Sohm::MissingID error.
#
def self.counter(name)
counters << name unless counters.include?(name)
define_method(name) do
return 0 if new?
- redis.call("HGET", key[:counters], name).to_i
+ redis.call("HGET", key[:_counters], name).to_i
end
end
# Keep track of `key[name]` and remove when deleting the object.
def self.track(name)
@@ -1140,15 +1074,15 @@
end
# Create a new model, notice that under Sohm's circumstances,
# this is no longer a syntactic sugar for Model.new(atts).save
def self.create(atts = {})
- new(atts).save(create: true)
+ new(atts).save
end
# Returns the namespace for the keys generated using this model.
- # Check `Ohm::Model.key` documentation for more details.
+ # Check `Sohm::Model.key` documentation for more details.
def key
model.key[id]
end
# Initialize a model using a dictionary of attributes.
@@ -1158,21 +1092,20 @@
# u = User.new(:name => "John")
#
def initialize(atts = {})
@attributes = {}
@serial_attributes = {}
- @_memo = {}
@serial_attributes_changed = false
update_attributes(atts)
end
# Access the ID used to store this model. The ID is used together
# with the name of the class in order to form the Redis key.
#
# Example:
#
- # class User < Ohm::Model; end
+ # class User < Sohm::Model; end
#
# u = User.create
# u.id
# # => 1
#
@@ -1199,56 +1132,20 @@
end
# Preload all the attributes of this model from Redis. Used
# internally by `Model::[]`.
def load!
- update_attributes(Utils.dict(redis.call("HGETALL", key))) unless new?
+ update_attributes(Utils.dict(redis.call("HGETALL", key))) if id
@serial_attributes_changed = false
return self
end
- # Read an attribute remotely from Redis. Useful if you want to get
- # the most recent value of the attribute and not rely on locally
- # cached value.
- #
- # Example:
- #
- # User.create(:name => "A")
- #
- # Session 1 | Session 2
- # --------------|------------------------
- # u = User[1] | u = User[1]
- # u.name = "B" |
- # u.save |
- # | u.name == "A"
- # | u.get(:name) == "B"
- #
- def get(att)
- @attributes[att] = redis.call("HGET", key, att)
- end
-
- # Update an attribute value atomically. The best usecase for this
- # is when you simply want to update one value.
- #
- # Note: This method is dangerous because it doesn't update indices
- # and uniques. Use it wisely. The safe equivalent is `update`.
- #
- def set(att, val)
- if val.to_s.empty?
- redis.call("HDEL", key, att)
- else
- redis.call("HSET", key, att, val)
- end
-
- @attributes[att] = val
- end
-
# Returns +true+ if the model is not persisted. Otherwise, returns +false+.
#
# Example:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :name
# end
#
# u = User.new(:name => "John")
# u.new?
@@ -1256,16 +1153,16 @@
#
# u.save
# u.new?
# # => false
def new?
- !model.exists?(id)
+ !(defined?(@id) && model.exists?(id))
end
# Increment a counter atomically. Internally uses HINCRBY.
def incr(att, count = 1)
- redis.call("HINCRBY", key[:counters], att, count)
+ redis.call("HINCRBY", key[:_counters], att, count)
end
# Decrement a counter atomically. Internally uses HINCRBY.
def decr(att, count = 1)
incr(att, -count)
@@ -1292,11 +1189,11 @@
# and the values of the attributes as values. It doesn't
# include the ID of the model.
#
# Example:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :name
# end
#
# u = User.create(:name => "John")
# u.attributes
@@ -1314,21 +1211,21 @@
# whitelist public attributes, as opposed to exporting each
# (possibly sensitive) attribute.
#
# Example:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :name
# end
#
# u = User.create(:name => "John")
# u.to_hash
# # => { :id => "1" }
#
# In order to add additional attributes, you can override `to_hash`:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :name
#
# def to_hash
# super.merge(:name => name)
# end
@@ -1349,22 +1246,22 @@
# Persist the model attributes and update indices and unique
# indices. The `counter`s and `set`s are not touched during save.
#
# Example:
#
- # class User < Ohm::Model
+ # class User < Sohm::Model
# attribute :name
# end
#
# u = User.new(:name => "John").save
# u.kind_of?(User)
# # => true
#
def save
if serial_attributes_changed
response = script(LUA_SAVE, 1, key,
- serial_attributes.to_msgpack,
+ sanitize_attributes(serial_attributes).to_msgpack,
cas_token)
if response.is_a?(RuntimeError)
if response.message =~ /cas_error/
raise CasViolation
@@ -1375,32 +1272,36 @@
@cas_token = response
@serial_attributes_changed = false
end
- redis.call("HSET", key, "_ndata", attributes.to_msgpack)
+ redis.call("HSET", key, "_ndata",
+ sanitize_attributes(attributes).to_msgpack)
refresh_indices
return self
end
# Delete the model, including all the following keys:
#
# - <Model>:<id>
- # - <Model>:<id>:counters
+ # - <Model>:<id>:_counters
# - <Model>:<id>:<set name>
#
# If the model has uniques or indices, they're also cleaned up.
#
def delete
memo_key = key["_indices"]
- commands = [["DEL", key], ["DEL", memo_key]]
+ commands = [["DEL", key], ["DEL", memo_key], ["DEL", key["_counters"]]]
index_list = redis.call("SMEMBERS", memo_key)
index_list.each do |index_key|
commands << ["SREM", index_key, id]
end
+ model.tracked.each do |tracked_key|
+ commands << ["DEL", key[tracked_key]]
+ end
model.synchronize do
commands.each do |command|
redis.queue(*command)
end
@@ -1457,28 +1358,37 @@
match(/^(?:.*::)*(.*)$/)[1].
gsub(/([a-z\d])([A-Z])/, '\1_\2').
downcase.to_sym
end
+ # Workaround to JRuby's concurrency problem
+ def self.inherited(subclass)
+ subclass.instance_variable_set(:@indices, [])
+ subclass.instance_variable_set(:@counters, [])
+ subclass.instance_variable_set(:@tracked, [])
+ subclass.instance_variable_set(:@attributes, [])
+ subclass.instance_variable_set(:@serial_attributes, [])
+ end
+
def self.indices
- @indices ||= []
+ @indices
end
def self.counters
- @counters ||= []
+ @counters
end
def self.tracked
- @tracked ||= []
+ @tracked
end
def self.attributes
- @attributes ||= []
+ @attributes
end
def self.serial_attributes
- @serial_attributes ||= []
+ @serial_attributes
end
def self.filters(dict)
unless dict.kind_of?(Hash)
raise ArgumentError,
@@ -1491,13 +1401,13 @@
def self.to_indices(att, val)
raise IndexNotFound unless indices.include?(att)
if val.kind_of?(Enumerable)
- val.map { |v| key[:indices][att][v] }
+ val.map { |v| key[:_indices][att][v] }
else
- [key[:indices][att][val]]
+ [key[:_indices][att][val]]
end
end
def fetch_indices
indices = {}
@@ -1509,11 +1419,11 @@
def refresh_indices
memo_key = key["_indices"]
# Add new indices first
commands = fetch_indices.each_pair.map do |field, vals|
vals.map do |val|
- index_key = key["_indices"][field][val]
+ index_key = model.key["_indices"][field][val]
[["SADD", memo_key, index_key], ["SADD", index_key, id]]
end
end.flatten(2)
# TODO: Think about switching to a redis pool later
@@ -1527,11 +1437,11 @@
# Remove old indices
# TODO: we can do this asynchronously, or maybe in a background queue
index_set = ::Set.new(redis.call("SMEMBERS", memo_key))
valid_list = model[id].send(:fetch_indices).each_pair.map do |field, vals|
vals.map do |val|
- key["_indices"][field][val]
+ model.key["_indices"][field][val]
end
end.flatten(1)
valid_set = ::Set.new(valid_list)
diff_set = index_set - valid_set
diff_list = diff_set.to_a
@@ -1561,9 +1471,13 @@
if cas_token = attrs.delete("_cas")
attrs["cas_token"] = cas_token
end
attrs
+ end
+
+ def sanitize_attributes(attributes)
+ attributes.select { |key, val| val }
end
def model
self.class
end