Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extracted and repackaged Mongo hydration from DocumentHydrator
- Loading branch information
0 parents
commit 0cfb83e
Showing
8 changed files
with
291 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
.bundle | ||
Gemfile.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
source :rubygems | ||
|
||
gem 'mongo' | ||
gem 'document_hydrator' | ||
|
||
group :development do | ||
gem 'bson_ext', :platforms => :ruby | ||
gem 'system_timer', :platforms => :ruby_18 | ||
gem 'rspec', '~> 2.6.0' | ||
gem 'jeweler' | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
Copyright (c) 2011 Greg Spurrier | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining | ||
a copy of this software and associated documentation files (the | ||
"Software"), to deal in the Software without restriction, including | ||
without limitation the rights to use, copy, modify, merge, publish, | ||
distribute, sublicense, and/or sell copies of the Software, and to | ||
permit persons to whom the Software is furnished to do so, subject to | ||
the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be | ||
included in all copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | ||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | ||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | ||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | ||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | ||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | ||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# MongoHydrator | ||
MongoHydrator makes turning a document with embedded references like this: | ||
|
||
status_update = { | ||
"_id" => 37, | ||
"user_id" => 1, | ||
"text" => "May the Force be with you.", | ||
"liker_ids" => [3, 4], | ||
"comments" => [ | ||
{ "user_id" => 2, | ||
"text" => "Thanks, but I'll stick with my blaster." | ||
}, | ||
{ "user_id" => 3, | ||
"text" => "Hey, show some respect!" | ||
} | ||
] | ||
} | ||
|
||
into a document with expanded subdocuments like this: | ||
|
||
{ | ||
"_id" => 37, | ||
"user" => { "_id"=>1, "name"=>"Obi-Wan Kenobi", "occupation"=>"Hermit" }, | ||
"text" => "May the Force be with you.", | ||
"likers" => [ | ||
{"_id" => 3, "name" => "Luke Skywalker", "occupation" => "Farmer"}, | ||
{"_id" => 4, "name" => "Yoda", "occupation" => "Jedi Master"} | ||
], | ||
"comments" => [ | ||
{ "user" => { "_id" => 2, "name" => "Han Solo", "occupation" => "Smuggler" }, | ||
"text" => "Thanks, but I'll stick with my blaster." | ||
}, | ||
{ "text" => "Hey, show some respect!", | ||
"user" => { "_id" => 3, "name" => "Luke Skywalker", "occupation" => "Farmer"} | ||
} | ||
] | ||
} | ||
|
||
as simple as this: | ||
|
||
# users is an instance of Mongo::Collection | ||
hydrator = MongoHydrator.new(users) | ||
hydrator.hydrate_document(status_update, | ||
['user_id', 'liker_ids', 'comments.user_id']) | ||
|
||
Behind the scenes, a single MongoDB query is used to retrieve the user | ||
documents corresponding to the IDs referenced by the specified paths: | ||
'user_id', 'liker_ids', and 'comments.user_id'. | ||
|
||
Integers are used above to make the example cleaner, but, of course, any form of valid MongoDB IDs can be used. | ||
|
||
## Installation | ||
Install the gem: | ||
|
||
gem install mongo_hydrator | ||
|
||
Require the file: | ||
|
||
require 'mongo_hydrator' | ||
|
||
Or, if you use Bundler, add this to your Gemfile: | ||
|
||
gem 'mongo_hydrator' | ||
|
||
## Paths | ||
A call to MongoHydrator#hydrate_document requires one or more paths to tell the hydrator which key or keys to replace in the original document. Paths use the same dot notation used in MongoDB queries. The example above uses three paths: | ||
|
||
* user_id -- a top-level key holding an ID | ||
* liker_ids -- an top-level key holding an array of IDs | ||
* comments.user_id -- an array of objects, each with an embedded ID | ||
|
||
Intermediate steps in the path may be hashes or arrays of hashes. The final step in the path may be an ID or an array of IDs. | ||
|
||
MongoHydrate#hydrate_document accepts either a single path or an array of paths. E.g.: | ||
|
||
hydrator.hydrate_document(document, 'user_id') | ||
hydrator.hydrate_document(document, ['user_id', 'liker_ids']) | ||
|
||
## ID Suffix Stripping | ||
If the paths in the original dehydrated document end in '_id' or '_ids', those suffixes will be stripped during hydration so that the key names continue to make sense. Pluralization is taken into account, so 'user_id' becomes 'user' and 'user_ids' becomes 'users'. | ||
|
||
## Limiting Fields | ||
To limit the fields that are included in the hydrated subdocuments, use the `:fields` option when creating the hydrator: | ||
|
||
hydrator = MongoHydrator.new(users_collection, :fields => { :_id => 0, :name => 1 }) | ||
|
||
Then only the specified fields will show up in the hydrated result. E.g.,: | ||
|
||
hydrator.hydrate_document(status_update, | ||
['user_id', 'liker_ids', 'comments.user_id']) | ||
# => | ||
|
||
## Hydrating Multiple Documents | ||
To hydrate multiple documents at once, use `hydrate_documents`. The arguments are the same as for `hydrate_document` with the exception that the first argument is an array of documents to hydrate. As with `hydrate_document` a single MongoDB query will be used to retrieve the required documents. | ||
|
||
## Additional Notes | ||
MongoHydrator expects the document being hydrated to have strings for keys. This will already be the case if the document came from the Mongo driver. If, however, the document is using symbols for keys, you will need to convert the keys to strings before hydration. | ||
|
||
## Copyright | ||
Copyright (c) 2011 Greg Spurrier. Released under the MIT license. See LICENSE.txt for further details. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
require 'rubygems' | ||
require 'bundler' | ||
begin | ||
Bundler.setup(:default, :development) | ||
rescue Bundler::BundlerError => e | ||
$stderr.puts e.message | ||
$stderr.puts "Run `bundle install` to install missing gems" | ||
exit e.status_code | ||
end | ||
require 'rake' | ||
|
||
require 'jeweler' | ||
Jeweler::Tasks.new do |gem| | ||
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options | ||
gem.name = "mongo_hydrator" | ||
gem.homepage = "http://github.com/gregspurrier/mongo_hydrator" | ||
gem.license = "MIT" | ||
gem.summary = %Q{MongoHydrator makes expanding MongoDB IDs into embedded subdocuments quick and easy.} | ||
gem.description = %Q{MongoHydrator takes a document, represented as a Ruby Hash, and efficiently updates it so that embedded references to MongoDB documents are replaced with their corresponding subdocuments.} | ||
gem.email = "greg.spurrier@gmail.com" | ||
gem.authors = ["Greg Spurrier"] | ||
# dependencies defined in Gemfile | ||
end | ||
Jeweler::RubygemsDotOrgTasks.new | ||
|
||
require 'rspec/core/rake_task' | ||
RSpec::Core::RakeTask.new | ||
|
||
task :default => :spec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
require 'mongo' | ||
require 'document_hydrator' | ||
|
||
class MongoHydrator | ||
# Create a new MongoHydrator instance | ||
# | ||
# collection -- The Mongo::Collection instance from which to fetch | ||
# subdocuments during hydration | ||
# options -- Optional hash containing options to pass to | ||
# collection.find. Typically used to specify a :fields | ||
# option to limit the fields included in the subdocuments. | ||
# | ||
# Returns the new MongoHydrator instance. | ||
def initialize(collection, options = {}) | ||
@hydration_proc = Proc.new do |ids| | ||
if options[:fields] | ||
# We need the_id key in order to assemble the results hash. | ||
# If the caller has requested that it be omitted from the | ||
# result, re-enable it and then strip later. | ||
field_selectors = options[:fields] | ||
id_key = field_selectors.keys.detect { |k| k.to_s == '_id' } | ||
if id_key && field_selectors[id_key] == 0 | ||
field_selectors.delete(id_key) | ||
strip_id = true | ||
end | ||
end | ||
subdocuments = collection.find({ '_id' => { '$in' => ids } }, options) | ||
subdocuments.inject({}) do |hash, subdocument| | ||
hash[subdocument['_id']] = subdocument | ||
subdocument.delete('_id') if strip_id | ||
hash | ||
end | ||
end | ||
end | ||
|
||
def hydrate_document(document, path_or_paths) | ||
DocumentHydrator.hydrate_document(document, path_or_paths, @hydration_proc) | ||
end | ||
|
||
def hydrate_document(document, path_or_paths) | ||
DocumentHydrator.hydrate_document(document, path_or_paths, @hydration_proc) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
require 'spec_helper' | ||
|
||
# The heavy lifting is tested in DocumentHydrator's tests. Here we just | ||
# make sure that things are fetched from the database as expected. | ||
|
||
describe MongoHydrator, '#hydrate_document' do | ||
before(:each) do | ||
db = Mongo::Connection.new.db('mongo_hydrator_test') | ||
@users_collection = db['users'] | ||
@users_collection.remove | ||
@users_collection.insert(:_id => 1, :name => 'Obi-Wan Kenobi', :occupation => 'Hermit') | ||
@users_collection.insert(:_id => 2, :name => 'Han Solo', :occupation => 'Smuggler') | ||
@users_collection.insert(:_id => 3, :name => 'Luke Skywalker', :occupation => 'Farmer') | ||
@users_collection.insert(:_id => 4, :name => 'Yoda', :occupation => 'Jedi Master') | ||
end | ||
|
||
context 'for a hydrator with no options' do | ||
before(:each) do | ||
@document = { | ||
"user_id" => 1, | ||
"text" => "May the Force be with you.", | ||
"liker_ids" => [3, 4], | ||
"comments" => [ | ||
{ "user_id" => 2, | ||
"text" => "Thanks, but I'll stick with my blaster." | ||
}, | ||
{ "user_id" => 3, | ||
"text" => "Hey, show some respect!" | ||
} | ||
] | ||
} | ||
@hydrator = MongoHydrator.new(@users_collection) | ||
end | ||
|
||
it 'hydrates the document' do | ||
expected = @document.dup | ||
expected['user'] = @users_collection.find_one(:_id => expected.delete('user_id')) | ||
expected['likers'] = expected.delete('liker_ids').map do |user_id| | ||
@users_collection.find_one(:_id => user_id) | ||
end | ||
expected['comments'].each do |comment| | ||
comment['user'] = @users_collection.find_one(:_id => comment.delete('user_id')) | ||
end | ||
|
||
@hydrator.hydrate_document(@document, ['user_id', 'liker_ids', 'comments.user_ids']) | ||
@document.should == expected | ||
end | ||
end | ||
|
||
context 'for a hydrator with a limited field set' do | ||
before(:each) do | ||
@document = { | ||
"user_id" => 1, | ||
"text" => "May the Force be with you.", | ||
"liker_ids" => [3, 4], | ||
"comments" => [ | ||
{ "user_id" => 2, | ||
"text" => "Thanks, but I'll stick with my blaster." | ||
}, | ||
{ "user_id" => 3, | ||
"text" => "Hey, show some respect!" | ||
} | ||
] | ||
} | ||
@options = { :fields => { :_id => 0, :name => 1 } } | ||
@hydrator = MongoHydrator.new(@users_collection, @options) | ||
end | ||
|
||
it 'hydrates the document, using only the requested fields' do | ||
expected = @document.dup | ||
expected['user'] = @users_collection.find_one({ :_id => expected.delete('user_id') }, @options) | ||
expected['likers'] = expected.delete('liker_ids').map do |user_id| | ||
@users_collection.find_one({ :_id => user_id }, @options) | ||
end | ||
expected['comments'].each do |comment| | ||
comment['user'] = @users_collection.find_one({ :_id => comment.delete('user_id') }, @options) | ||
end | ||
|
||
@hydrator.hydrate_document(@document, ['user_id', 'liker_ids', 'comments.user_ids']) | ||
@document.should == expected | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
require 'rubygems' | ||
require 'mongo_hydrator' | ||
|