Skip to content

Commit

Permalink
Extracted and repackaged Mongo hydration from DocumentHydrator
Browse files Browse the repository at this point in the history
  • Loading branch information
gregspurrier committed Jun 21, 2011
0 parents commit 0cfb83e
Show file tree
Hide file tree
Showing 8 changed files with 291 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
@@ -0,0 +1,2 @@
.bundle
Gemfile.lock
11 changes: 11 additions & 0 deletions Gemfile
@@ -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
20 changes: 20 additions & 0 deletions LICENSE.txt
@@ -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.
100 changes: 100 additions & 0 deletions README.markdown
@@ -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.
29 changes: 29 additions & 0 deletions Rakefile
@@ -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
43 changes: 43 additions & 0 deletions lib/mongo_hydrator.rb
@@ -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
83 changes: 83 additions & 0 deletions spec/mongo_hydrator_spec.rb
@@ -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
3 changes: 3 additions & 0 deletions spec/spec_helper.rb
@@ -0,0 +1,3 @@
require 'rubygems'
require 'mongo_hydrator'

0 comments on commit 0cfb83e

Please sign in to comment.