Origin is a DSL for building MongoDB queries.
Origin is tested against MRI 1.9.2, 1.9.3, 2.0.0, and JRuby (1.9).
Until the official website is complete, please use this README as a documentation reference.
Origin provides a DSL to mix in to any object to give it the ability
to build MongoDB queries easily. Simply include the module
Origin::Queryable
to get started.
class Criteria
include Origin::Queryable
end
From here on out when a Criteria
object is instantiated, assume it
already includes Origin::Queryable
.
The Queryable provides a default constructor that takes 2 optional parameters, a hash of aliased field mappings, and a hash of name and serializer pairs. Let's discuss aliases first.
In MongoDB, since the database is schemaless and the field names are included for every document in the JSON, much more storage space is required than traditional databases with schemas. Because of this, a common practice is to store very short field names in the documents to conserve space. This is not ideal from a business domain standpoint so mappers like Mongoid provide alias functionality to keep the domain model expressive. An example of this in Mongoid is:
class Band
include Mongoid::Document
field :n, as: :name, type: String
end
In the above example you can reference the field by Band#name
, but the
field in the database is stored as n
. We want to maintain this
expressiveness in querying the database as well, so the Queryable needs
a hash to know how to map the name to the field in the database.
Criteria.new({ "n" => "name" })
The aliases hash must be string keys and string values. Then you can do
this and it will get converted to the database field name n
:
criteria.where(name: "value")
The second optional parameter is a hash of field names with associated
serializers. This is useful for when MongoDB cannot handle the Ruby
object on the database side, so some form of transformation must occur.
Serializers are Ruby objects that respond to #evolve
. A good example
of this are Date objects in Ruby, which must be converted to UTC
times for MongoDB.
class DateSerializer
def evolve(date)
Time.utc(date.year, date.month, date.day, 0, 0, 0, 0)
end
end
criteria = Criteria.new({}, { "dob" => DateSerializer.new })
criteria.where(dob: Date.new(1970, 1, 1)) # Converts dob to a time.
If you'd like to have convenience methods added to a model, you can extend
Origin::Forwardable
and tell the queryable methods which class method
to delegate to. This will add all the public queryable methods to that
class.
class Band
extend Origin::Forwardable
select_with :criteria
def self.criteria
Criteria.new(aliases, serializers)
end
end
Band.where(name: "Depeche Mode")
From here on, assume all references to the Band
class have extended
Origin::Forwardable
.
The Queryable
provides convenience methods for every type of MongoDB
query operation you can perform. For example, to add $all selection to the
queryable.
Band.all(labels: [ "Mute", "Nothing" ])
This translates to the following selector.
{ "labels" => { "$all" => [ "Mute", "Nothing" ]}}
See the API documentation for all available methods that the Queryable
provides, in it's internal Selectable
and Optional
modules.
In addition to all the convenience methods for selection, Origin also adds
various convenience methods to Symbol
which can be used only within
#where
selection for syntax suger. For example:
Band.where(:name.in => [ "Depeche Mode", "New Order" ])
This translates to the following selector.
{ "name" => { "$in" => [ "Depeche Mode", "New Order" ] }}
The API documentation contains all the corresponding symbol methods as well in the example annotations for each standard method.
Queryable#where
is also special in that it can take a Javascript string as
a parameter, that will create the corresponding $where selection.
Band.where("this.name == 'Depeche Mode'")
Translates to:
{ "$where" => "this.name == 'Depeche Mode'" }
Note that passing Javascript alone by itself to MongoDB will cause a full collection scan.
The selection API also provides merge strategies for cases you want to
override the default behaviour with specific methods that expect array values.
The available strategies are intersect, override, and union and are used by
chaining the name of the strategy before calling any method. For example,
to override the default intersection behaviour of #in
:
Band.in(name: [ "Depeche Mode" ]).union.in(name: [ "New Order" ])
This translates to the following selector.
{ "name" => { "$in" => [ "Depeche Mode", "New Order" ] }}
The default behaviour for the array methods are:
Queryable#all
- defaults to union.Queryable#in
- defaults to intersect.Queryable#nin
- defaults to intersect.
Each time a method on the queryable is called, it clones itself and returns a new copy with the next selection added. This is to not modify the previous queryable in case it needs reuse later on without the additional selection.
By default, Origin's sort options are in a format that is compatible with Moped, but not the 10gen Ruby driver. (It's a hash vs a multi-dimensional array.)
If you would like the sorting options to be compatible with the 10gen
driver, then when instantiating a Queryable
you'll need to pass :mongo
as the third parameter to the constructor.
Criteria.new(aliases, serializers, :mongo)