require 'set' module MongoMapper module Document def self.included(model) model.class_eval do include EmbeddedDocument include InstanceMethods include Observing include Callbacks include Dirty include RailsCompatibility::Document extend Validations::Macros extend ClassMethods extend Finders def self.per_page 25 end unless respond_to?(:per_page) end descendants << model end def self.descendants @descendants ||= Set.new end module ClassMethods def key(*args) key = super create_indexes_for(key) key end def ensure_index(name_or_array, options={}) keys_to_index = if name_or_array.is_a?(Array) name_or_array.map { |pair| [pair[0], pair[1]] } else name_or_array end MongoMapper.ensure_index(self, keys_to_index, options) end # @overload find(:first, options) # @see Document.first # # @overload find(:last, options) # @see Document.last # # @overload find(:all, options) # @see Document.all # # @overload find(ids, options) # # @raise DocumentNotFound raised when no ID or arguments are provided def find!(*args) options = args.extract_options! case args.first when :first then first(options) when :last then last(options) when :all then find_every(options) when Array then find_some(args, options) else case args.size when 0 raise DocumentNotFound, "Couldn't find without an ID" when 1 find_one!(options.merge({:_id => args[0]})) else find_some(args, options) end end end def find(*args) find!(*args) rescue DocumentNotFound nil end def paginate(options) per_page = options.delete(:per_page) || self.per_page page = options.delete(:page) total_entries = count(options) pagination = Pagination::PaginationProxy.new(total_entries, page, per_page) options.merge!(:limit => pagination.limit, :skip => pagination.skip) pagination.subject = find_every(options) pagination end # @param [Hash] options any conditions understood by # FinderOptions.to_mongo_criteria # # @return the first document in the ordered collection as described by # +options+ # # @see FinderOptions def first(options={}) find_one(options) end # @param [Hash] options any conditions understood by # FinderOptions.to_mongo_criteria # @option [String] :order this *mandatory* option describes how to # identify the ordering of the documents in your collection. Note that # the *last* document in this collection will be selected. # # @return the last document in the ordered collection as described by # +options+ # # @raise Exception when no :order option has been defined def last(options={}) raise ':order option must be provided when using last' if options[:order].blank? find_one(options.merge(:order => invert_order_clause(options[:order]))) end # @param [Hash] options any conditions understood by # FinderOptions.to_mongo_criteria # # @return [Array] all documents in your collection that match the # provided conditions # # @see FinderOptions def all(options={}) find_every(options) end def find_by_id(id) find_one(:_id => id) end def count(options={}) collection.find(to_criteria(options)).count end def exists?(options={}) !count(options).zero? end # @overload create(doc_attributes) # Create a single new document # @param [Hash] doc_attributes key/value pairs to create a new # document # # @overload create(docs_attributes) # Create many new documents # @param [Array] provide many Hashes of key/value pairs to create # multiple documents # # @example Creating a single document # MyModel.create({ :foo => "bar" }) # # @example Creating multiple documents # MyModel.create([{ :foo => "bar" }, { :foo => "baz" }) # # @return [Boolean] when a document is successfully created, +true+ will # be returned. If a document fails to create, +false+ will be returned. def create(*docs) initialize_each(*docs) { |doc| doc.save } end # @see Document.create # # @raise [DocumentNotValid] raised if a document fails to create def create!(*docs) initialize_each(*docs) { |doc| doc.save! } end # @overload update(id, attributes) # Update a single document # @param id the ID of the document you wish to update # @param [Hash] attributes the key to update on the document with a new # value # # @overload update(ids_and_attributes) # Update multiple documents # @param [Hash] ids_and_attributes each key is the ID of some document # you wish to update. The value each key points toward are those # applied to the target document # # @example Updating single document # Person.update(1, {:foo => 'bar'}) # # @example Updating multiple documents at once: # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}}) def update(*args) if args.length == 1 update_multiple(args[0]) else id, attributes = args update_single(id, attributes) end end # Removes ("deletes") one or many documents from the collection. Note # that this will bypass any +destroy+ hooks defined by your class. # # @param [Array] ids the ID or IDs of the records you wish to delete def delete(*ids) collection.remove(to_criteria(:_id => ids.flatten)) end def delete_all(options={}) collection.remove(to_criteria(options)) end # Iterates over each document found by the provided IDs and calls their # +destroy+ method. This has the advantage of processing your document's # +destroy+ call-backs. # # @overload destroy(id) # Destroy a single document by ID # @param id the ID of the document to destroy # # @overload destroy(ids) # Destroy many documents by their IDs # @param [Array] the IDs of each document you wish to destroy # # @example Destroying a single document # Person.destroy("34") # # @example Destroying multiple documents # Person.destroy("34", "45", ..., "54") # # # OR... # # Person.destroy(["34", "45", ..., "54"]) def destroy(*ids) find_some(ids.flatten).each(&:destroy) end def destroy_all(options={}) all(options).each(&:destroy) end # @overload connection() # @return [Mongo::Connection] the connection used by your document class # # @overload connection(mongo_connection) # @param [Mongo::Connection] mongo_connection a new connection for your # document class to use # @return [Mongo::Connection] a new Mongo::Connection for yoru document # class def connection(mongo_connection=nil) if mongo_connection.nil? @connection ||= MongoMapper.connection else @connection = mongo_connection end @connection end # Changes the database name from the default to whatever you want # # @param [#to_s] name the new database name to use. def set_database_name(name) @database_name = name end # Returns the database name # # @return [String] the database name def database_name @database_name end # Returns the database the document should use. Defaults to # MongoMapper.database if other database is not set. # # @return [Mongo::DB] the mongo database instance def database if database_name.nil? MongoMapper.database else connection.db(database_name) end end # Changes the collection name from the default to whatever you want # # @param [#to_s] name the new collection name to use. def set_collection_name(name) @collection_name = name end # Returns the collection name, if not set, defaults to class name tableized # # @return [String] the collection name, if not set, defaults to class # name tableized def collection_name @collection_name ||= self.to_s.tableize.gsub(/\//, '.') end # @return the Mongo Ruby driver +collection+ object def collection database.collection(collection_name) end # Defines a +created_at+ and +updated_at+ attribute (with a +Time+ # value) on your document. These attributes are updated by an # injected +update_timestamps+ +before_save+ hook. def timestamps! key :created_at, Time key :updated_at, Time class_eval { before_save :update_timestamps } end def single_collection_inherited? keys.has_key?('_type') && single_collection_inherited_superclass? end def single_collection_inherited_superclass? superclass.respond_to?(:keys) && superclass.keys.has_key?('_type') end protected def method_missing(method, *args) finder = DynamicFinder.new(method) if finder.found? meta_def(finder.method) { |*args| dynamic_find(finder, args) } send(finder.method, *args) else super end end private def create_indexes_for(key) ensure_index key.name if key.options[:index] end def initialize_each(*docs) instances = [] docs = [{}] if docs.blank? docs.flatten.each do |attrs| doc = initialize_doc(attrs) yield(doc) instances << doc end instances.size == 1 ? instances[0] : instances end def find_every(options) criteria, options = to_finder_options(options) collection.find(criteria, options).to_a.map do |doc| initialize_doc(doc) end end def find_some(ids, options={}) ids = ids.flatten.compact.uniq documents = find_every(options.merge(:_id => ids)) if ids.size == documents.size documents else raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}" end end def find_one(options={}) criteria, options = to_finder_options(options) if doc = collection.find_one(criteria, options) initialize_doc(doc) end end def find_one!(options={}) find_one(options) || raise(DocumentNotFound, "Document match #{options.inspect} does not exist in #{collection.name} collection") end def invert_order_clause(order) order.split(',').map do |order_segment| if order_segment =~ /\sasc/i order_segment.sub /\sasc/i, ' desc' elsif order_segment =~ /\sdesc/i order_segment.sub /\sdesc/i, ' asc' else "#{order_segment.strip} desc" end end.join(',') end def update_single(id, attrs) if id.blank? || attrs.blank? || !attrs.is_a?(Hash) raise ArgumentError, "Updating a single document requires an id and a hash of attributes" end doc = find(id) doc.update_attributes(attrs) doc end def update_multiple(docs) unless docs.is_a?(Hash) raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash" end instances = [] docs.each_pair { |id, attrs| instances << update(id, attrs) } instances end def to_criteria(options={}) FinderOptions.new(self, options).criteria end def to_finder_options(options={}) FinderOptions.new(self, options).to_a end end module InstanceMethods def collection self.class.collection end def new? read_attribute('_id').blank? || using_custom_id? end def save(perform_validations=true) !perform_validations || valid? ? create_or_update : false end def save! save || raise(DocumentNotValid.new(self)) end def destroy return false if frozen? self.class.delete(_id) unless new? freeze end def reload self.class.find(_id) end private def create_or_update result = new? ? create : update result != false end def create assign_id save_to_collection end def assign_id if read_attribute(:_id).blank? write_attribute :_id, Mongo::ObjectID.new end end def update save_to_collection end def save_to_collection clear_custom_id_flag collection.save(to_mongo) end def update_timestamps now = Time.now.utc write_attribute('created_at', now) if new? && read_attribute('created_at').blank? write_attribute('updated_at', now) end def clear_custom_id_flag @using_custom_id = nil end end end # Document end # MongoMapper