lib/ohm.rb in ohm-2.1.0 vs lib/ohm.rb in ohm-2.2.0

- old
+ new

@@ -1,12 +1,11 @@ # encoding: UTF-8 require "msgpack" require "nido" require "redic" -require "securerandom" -require_relative "ohm/command" +require "stal" module Ohm LUA_CACHE = Hash.new { |h, k| h[k] = Hash.new } LUA_SAVE = File.expand_path("../ohm/lua/save.lua", __FILE__) LUA_DELETE = File.expand_path("../ohm/lua/delete.lua", __FILE__) @@ -74,20 +73,20 @@ def self.dict(arr) Hash[*arr] end - def self.sort(redis, key, options) + def self.sort_options(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) + return args end end # Use this if you want to do quick ad hoc redis commands against the # defined Ohm connection. @@ -283,10 +282,11 @@ # # p.comments.size == 0 # # => true # def delete(model) + # LREM key 0 <id> means remove all elements matching <id> # @see http://redis.io/commands/lrem redis.call("LREM", key, 0, model.id) end @@ -319,33 +319,109 @@ def redis model.redis end end - # Defines most of the methods used by `Set` and `MultiSet`. - class BasicSet + class Set 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. + attr :key + attr :model + attr :namespace + + def initialize(model, namespace, key) + @model = model + @namespace = namespace + @key = key + end + + # 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 + + # Returns an array with all the ID's of the set. + # + # class Post < Ohm::Model + # end + # # class User < Ohm::Model # attribute :name + # index :name + # + # set :posts, :Post # 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]) + # User.create(name: "John") + # User.create(name: "Jane") # - # Note: This is slower compared to just doing `sort`, specifically - # because Redis has to read each individual hash in order to sort - # them. + # User.all.ids + # # => ["1", "2"] # - def sort_by(att, options = {}) - sort(options.merge(:by => to_key(att))) + # User.find(name: "John").union(name: "Jane").ids + # # => ["1", "2"] + # + def ids + if Array === key + Stal.solve(redis, key) + else + redis.call("SMEMBERS", key) + end end + # Returns the total size of the set using SCARD. + def size + Stal.solve(redis, ["SCARD", key]) + end + + # Returns +true+ if +id+ is included in the set. Otherwise, returns +false+. + # + # Example: + # + # class Post < Ohm::Model + # end + # + # class User < Ohm::Model + # set :posts, :Post + # end + # + # user = User.create + # post = Post.create + # user.posts.add(post) + # + # user.posts.exists?('nonexistent') # => false + # user.posts.exists?(post.id) # => true + # + def exists?(id) + Stal.solve(redis, ["SISMEMBER", key, id]) == 1 + 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 + # 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. # @@ -368,37 +444,36 @@ # # => 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) }) + Stal.solve(redis, ["SORT", key, *Utils.sort_options(options)]) + else + fetch(Stal.solve(redis, ["SORT", key, *Utils.sort_options(options)])) + end end - # Check if a model is included in this set. + # 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. # - # Example: + # class User < Ohm::Model + # attribute :name + # end # - # u = User.create + # 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]) # - # User.all.include?(u) - # # => true + # Note: This is slower compared to just doing `sort`, specifically + # because Redis has to read each individual hash in order to sort + # them. # - # 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) + def sort_by(att, options = {}) + sort(options.merge(:by => to_key(att))) end - # 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. # # Example: # @@ -417,102 +492,20 @@ else sort(opts).first end end - # Returns an array with all the ID's of the set. - # - # class Post < Ohm::Model - # end - # - # class User < Ohm::Model - # attribute :name - # index :name - # - # set :posts, :Post - # end - # - # User.create(name: "John") - # User.create(name: "Jane") - # - # User.all.ids - # # => ["1", "2"] - # - # User.find(name: "John").union(name: "Jane").ids - # # => ["1", "2"] - # - def ids - execute { |key| redis.call("SMEMBERS", key) } - end - - # 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 - - # Returns +true+ if +id+ is included in the set. Otherwise, returns +false+. - # - # Example: - # - # class Post < Ohm::Model - # end - # - # class User < Ohm::Model - # set :posts, :Post - # end - # - # user = User.create - # post = Post.create - # user.posts.add(post) - # - # user.posts.exists?('nonexistent') # => false - # 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 - attr :model - - def initialize(key, namespace, model) - @key = key - @namespace = namespace - @model = model - end - # Chain new fiters on an existing set. # # Example: # # set = User.find(:name => "John") # set.find(:age => 30) # def find(dict) - MultiSet.new( - namespace, model, Command[:sinterstore, key, *model.filters(dict)] + Ohm::Set.new( + model, namespace, [:SINTER, key, *model.filters(dict)] ) end # Reduce the set using any number of filters. # @@ -523,11 +516,13 @@ # # # You can also do it in one line. # User.find(:name => "John").except(:country => "US") # def except(dict) - MultiSet.new(namespace, model, key).except(dict) + Ohm::Set.new( + model, namespace, [:SDIFF, key, [:SUNION, *model.filters(dict)]] + ) end # Perform an intersection between the existent set and # the new set created by the union of the passed filters. # @@ -537,11 +532,13 @@ # set.combine(:name => ["John", "Jane"]) # # # The result will include all users with active status # # and with names "John" or "Jane". def combine(dict) - MultiSet.new(namespace, model, key).combine(dict) + Ohm::Set.new( + model, namespace, [:SINTER, key, [:SUNION, *model.filters(dict)]] + ) end # Do a union to the existing set using any number of filters. # # Example: @@ -551,24 +548,31 @@ # # # You can also do it in one line. # User.find(:name => "John").union(:name => "Jane") # def union(dict) - MultiSet.new(namespace, model, key).union(dict) + Ohm::Set.new( + model, namespace, [:SUNION, key, [:SINTER, *model.filters(dict)]] + ) end private - def execute - yield key + def to_key(att) + if model.counters.include?(att) + namespace["*:counters->%s" % att] + else + namespace["*->%s" % att] + end end def redis model.redis end end class MutableSet < Set + # Add a model directly to the set. # # Example: # # user = User.create @@ -622,132 +626,10 @@ redis.commit end end end - # Anytime you filter a set with more than one requirement, you - # internally use a `MultiSet`. `MultiSet` 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 < BasicSet - attr :namespace - attr :model - attr :command - - def initialize(namespace, model, command) - @namespace = namespace - @model = model - @command = command - end - - # Chain new fiters on an existing set. - # - # Example: - # - # set = User.find(:name => "John", :age => 30) - # set.find(:status => 'pending') - # - def find(dict) - MultiSet.new( - namespace, model, Command[:sinterstore, command, intersected(dict)] - ) - end - - # Reduce the set using any number of filters. - # - # Example: - # - # set = User.find(:name => "John") - # set.except(:country => "US") - # - # # You can also do it in one line. - # User.find(:name => "John").except(:country => "US") - # - def except(dict) - MultiSet.new( - namespace, model, Command[:sdiffstore, command, unioned(dict)] - ) - end - - # Perform an intersection between the existent set and - # the new set created by the union of the passed filters. - # - # Example: - # - # set = User.find(:status => "active") - # set.combine(:name => ["John", "Jane"]) - # - # # The result will include all users with active status - # # and with names "John" or "Jane". - def combine(dict) - MultiSet.new( - namespace, model, Command[:sinterstore, command, unioned(dict)] - ) - end - - # Do a union to the existing set using any number of filters. - # - # Example: - # - # set = User.find(:name => "John") - # set.union(:name => "Jane") - # - # # You can also do it in one line. - # User.find(:name => "John").union(:name => "Jane") - # - def union(dict) - MultiSet.new( - namespace, model, Command[:sunionstore, command, intersected(dict)] - ) - end - - private - def redis - model.redis - end - - def intersected(dict) - Command[:sinterstore, *model.filters(dict)] - end - - def unioned(dict) - Command[:sunionstore, *model.filters(dict)] - end - - def execute - # namespace[:tmp] is where all the temp keys should be stored in. - # redis will be where all the commands are executed against. - response = command.call(namespace[:tmp], redis) - - begin - - # At this point, we have the final aggregated set, which we yield - # to the caller. the caller can do all the normal set operations, - # i.e. SCARD, SMEMBERS, etc. - yield response - - ensure - - # We have to make sure we clean up the temporary keys to avoid - # memory leaks and the unintended explosion of memory usage. - command.clean - end - end - end - # 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: @@ -925,13 +807,13 @@ # def self.find(dict) keys = filters(dict) if keys.size == 1 - Ohm::Set.new(keys.first, key, self) + Ohm::Set.new(self, key, keys.first) else - Ohm::MultiSet.new(key, self, Command.new(:sinterstore, *keys)) + Ohm::Set.new(self, key, [:SINTER, *keys]) end end # Retrieve a set of models given an array of IDs. # @@ -978,11 +860,11 @@ track(name) define_method name do model = Utils.const(self.class, model) - Ohm::MutableSet.new(key[name], model.key, model) + Ohm::MutableSet.new(model, model.key, key[name]) end end # Declare an Ohm::List with the given name. # @@ -1188,10 +1070,10 @@ tracked << name unless tracked.include?(name) end # An Ohm::Set wrapper for Model.key[:all]. def self.all - Set.new(key[:all], key, self) + Ohm::Set.new(self, key, key[:all]) end # Syntactic sugar for Model.new(atts).save def self.create(atts = {}) new(atts).save