module Mongoo module Persistence class RawUpdate attr_accessor :criteria, :updates, :opts def initialize(doc) @doc = doc @criteria ||= {} @updates ||= {} @opts ||= {} end def will_change(&block) @will_change_block = block end def run @criteria.stringify_keys! @criteria["_id"] = @doc.id ret = @doc.collection.update(self.criteria, self.updates, self.opts) if !ret.is_a?(Hash) || (ret["updatedExisting"] && ret["n"] == 1) if @will_change_block @will_change_block.call(@doc, ret) end end ret end end def self.included(base) base.extend(ClassMethods) end module ClassMethods def collection_name(val=nil) if val @collection_name = val else @collection_name ||= self.model_name.tableize end end def conn=(conn_lambda) @conn_lambda = conn_lambda @_conn = nil @_db = nil @collection = nil @conn_lambda end def db=(db_name) @db_name = db_name @_db = nil @collection = nil @db_name end def conn @_conn ||= ((@conn_lambda && @conn_lambda.call) || Mongoo.conn) end def db @_db ||= begin if db_name = (@db_name || (@conn_lambda && Mongoo.db.name)) conn.db(db_name) else Mongoo.db end end end def collection @collection ||= db.collection(collection_name) end # Atomically update and return a document using MongoDB's findAndModify command. (MongoDB > 1.3.0) # # @option opts [Hash] :query ({}) a query selector document for matching the desired document. # @option opts [Hash] :update (nil) the update operation to perform on the matched document. # @option opts [Array, String, OrderedHash] :sort ({}) specify a sort option for the query using any # of the sort options available for Cursor#sort. Sort order is important if the query will be matching # multiple documents since only the first matching document will be updated and returned. # @option opts [Boolean] :remove (false) If true, removes the the returned document from the collection. # @option opts [Boolean] :new (false) If true, returns the updated document; otherwise, returns the document # prior to update. # # @return [Hash] the matched document. # # @core findandmodify find_and_modify-instance_method def find_and_modify(opts) if doc = collection.find_and_modify(opts) Mongoo::Cursor.new(self, nil).obj_from_doc(doc) end end def find(query={}, opts={}) Mongoo::Cursor.new(self, collection.find(query, opts)) end def find_one(query={}, opts={}) id_map_on = Mongoo::IdentityMap.on? is_simple_query = nil is_simple_query = Mongoo::IdentityMap.simple_query?(query, opts) if id_map_on if id_map_on && is_simple_query if doc = Mongoo::IdentityMap.read(query) return doc end end if doc = collection.find_one(query, opts) Mongoo::Cursor.new(self, nil).obj_from_doc(doc) end end def all find end def each find.each { |found| yield(found) } end def first find.limit(1).next_document end def empty? count == 0 end def count collection.count end def drop collection.drop end def index_meta return @index_meta if @index_meta @index_meta = {} end def index(spec, opts={}) self.index_meta[spec] = opts end def create_indexes self.index_meta.each do |spec, opts| opts[:background] = true if !opts.has_key?(:background) collection.create_index(spec, opts) end; true end end # ClassMethods def db self.class.db end def conn self.class.conn end def collection_name self.class.collection_name end def to_param persisted? ? get("_id").to_s : nil end def to_key get("_id") end def to_model self end def save!(*args) persisted? ? update!(*args) : insert!(*args) end def save(*args) persisted? ? update(*args) : insert(*args) end def persisted? @persisted == true #!get("_id").nil? end def collection self.class.collection end def insert(opts={}) ret = _run_insert_callbacks do if persisted? raise AlreadyInsertedError, "document has already been inserted" end unless valid? if opts[:safe] == true raise Mongoo::NotValidError, "document contains errors" else return false end end ret = self.collection.insert(mongohash, opts) unless ret.is_a?(BSON::ObjectId) raise InsertError, "not an object: #{ret.inspect}" end mongohash.delete(:_id) set("_id", ret) @persisted = true set_persisted_mongohash(mongohash) ret end Mongoo::IdentityMap.write(self) if Mongoo::IdentityMap.on? ret rescue Mongo::OperationFailure => e if e.message.to_s =~ /^11000\:/ raise Mongoo::DuplicateKeyError, e.message else raise e end end def insert!(opts={}) insert(opts.merge(:safe => true)) end def raw_update(&block) raw = RawUpdate.new(self) block.call(raw) raw.run end def update(opts={}) _run_update_callbacks do unless persisted? raise NotInsertedError, "document must be inserted before being updated" end unless valid? if opts[:safe] == true raise Mongoo::NotValidError, "document contains errors" else return false end end opts[:only_if_current] = true unless opts.has_key?(:only_if_current) opts[:safe] = true if !opts.has_key?(:safe) && opts[:only_if_current] == true update_hash = build_update_hash(self.changelog) return true if update_hash.empty? update_query_hash = build_update_query_hash(persisted_mongohash.to_key_value, self.changelog) if Mongoo.verbose_debug puts "\n* update_query_hash: #{update_query_hash.inspect}\n update_hash: #{update_hash.inspect}\n opts: #{opts.inspect}\n" end if opts.delete(:find_and_modify) == true ret = self.collection.find_and_modify(query: update_query_hash, update: update_hash, new: true) reload(ret) else ret = self.collection.update(update_query_hash, update_hash, opts) if !ret.is_a?(Hash) || (ret["updatedExisting"] && ret["n"] == 1) reset_persisted_mongohash true else if opts[:only_if_current] raise StaleUpdateError, ret.inspect else raise UpdateError, ret.inspect end end end # if opts.delete(:find_and_modify) end rescue Mongo::OperationFailure => e if e.message.to_s =~ /^11000\:/ raise Mongoo::DuplicateKeyError, e.message else raise e end end def update!(opts={}) update(opts.merge(:safe => true)) end def destroyed? @destroyed != nil end def new_record? !persisted? end def remove(opts={}) _run_remove_callbacks do unless persisted? raise NotInsertedError, "document must be inserted before it can be removed" end ret = self.collection.remove({"_id" => get("_id")}, opts) if !ret.is_a?(Hash) || (ret["err"] == nil && ret["n"] == 1) @destroyed = true @persisted = false true else raise RemoveError, ret.inspect end end end def remove!(opts={}) remove(opts.merge(:safe => true)) end def reload(new_doc=nil) new_doc ||= collection.find_one(get("_id")) init_from_hash(new_doc) @persisted = true set_persisted_mongohash(mongohash) true end def build_update_hash(changelog) update_hash = {} changelog.each do |op, k, v| update_hash["$#{op}"] ||= {} update_hash["$#{op}"][k] = v end update_hash end protected :build_update_hash def build_update_query_hash(persisted_mongohash_kv, changelog) update_query_hash = { "_id" => get("_id") } changelog.each do |op, k, v| if persisted_val = persisted_mongohash_kv[k] if persisted_val == [] # work around a bug where mongo won't find a doc # using an empty array [] if an index is defined # on that field. persisted_val = { "$size" => 0 } end update_query_hash[k] = persisted_val end end update_query_hash end protected :build_update_query_hash end end