Thursday, July 4, 2013

Setting up the MongoHQ connection

Heroku/MongoHQ supply guidance in this MongoHQ application note.  Since I didn't use the Heroku command line, I have to manually set up the environment variable.
heroku config:set MONGOHQ_URL=mongodb://[user]:[password]@[host].mongohq.com:10007/[database]
For reference, here are Heroku's notes on environment variables
I have slightly modified connect function (shown below). I supply a default to the local copy of MongoDB. I also corrected the check for db.password.nil?
def connect
  return @db if @db

  uri = 'http://localhost:27017/mydb'
  if( ENV.has_key?('MONGOHQ_URL')) then
    uri = ENV['MONGOHQ_URL']
  end

  db = URI.parse(uri)
  db_name = db.path.gsub(/^\//, '')

  @db = Mongo::Connection.new(db.host, db.port).db(db_name)
  @db.authenticate(db.user, db.password) unless (db.user.nil? || db.password.nil?)
  @db
end
When developing locally, I set the MONGOHQ_URL environment variable manually and then start rails.

Setting up MongoHQ

The application that I'm developing runs on the Heroku cloud. Heroku offers add-ons to their basic service, one category of which is database.

MongoHQ is a database-as-a-service provider and is independent of Heroku.  That's both good and bad.  The good part is that I can change my database supplier without changing Heroku.  The bad part is that I'm worried about performance of access between Heroku and MongoHQ.  Just a worry, nothing concrete.

From an application architecture things are bit cleaner for me.  I generate file-based reference data on my local system and push it into MongoDB for use by the application.  My picture now looks like:



I can now simply push my data into MongoHQ without worrying about Heroku.  In addition, I can prototype the database interactions on my own machines running (free) MongoDB and then repoint them at MongoHQ.

Setting it up

Being cheap, I wanted to start with the free MongoHQ sandbox.  Initially I tried using the "heroku addons:add ..." command line.  That failed as I'm using the free Heroku for development and it wanted my account to have a credit card on file.  IMO, if someone doesn't have my credit card number, I can't accidentally do something that results in charges to my account.

So I went directly to MongoHQ and got my account set up.  Even there, it looked like they wanted a credit card until I noticed the tiny link saying "skip this and get a free version".  That worked and I got my first database.

I had an initial point of confusion.  My database was created but I didn't have a username/password with which to access it.  I navigated to the database, went to the admin tab and added a user.  That worked and I could access the DB via my local mongo console.

Wednesday, July 3, 2013

Storing Binary Data in MongoDB

I needed to store some binary data in my MongoDB database.  I'm hosting my application on Heroku and the app has images that are closer to user data - they aren't part of the build and are added later.  Heroku has a funny file system which gets periodically cleaned.  They say data won't persist in the file system much longer than 24 hours.  As a result, I needed to keep my image data somewhere else and MongoDB seems to be a good choice.

Ruby is really good at easily reading a binary file into a Ruby string.  Ruby strings come tagged with character encoding so that magically just works.  MongoDB only uses UTF-8 encoding and will convert all strings to UTF-8 for you.  Sadly, png files don't convert automatically.

The solution to first convert the Ruby string to a BSON::Binary object.  The acronym BSON stands for "Binary JSON".  That conversion is handled in the BSON::Binary constructor.
io = File.open( fn, "rb")
mongo_binary_form = BSON::Binary.new( io.read)
io.close
You can just add/include the Binary object to your persisted object as you would any other data type.
@collection.insert( {png_data: mongo_binary_form} )
The BSON::Binary form is acceptable to use as-is in your send_data() function. The following is a snippet from a controller which returns a png image.
respond_to do |format|
  format.html do
    send_data(mongo_binary_form ,
              :filename => "your file name",
              :type=>"image/png")
  end
end

Tuesday, July 2, 2013

Basic Ruby API

The official MongoDB Ruby API isn't for beginners so here are some notes.

Mongo::MongoClient

As with most databases, you connect to it via a client.  You create a new client whose constructor takes the connection parameters.

I haven't spotted any security parameters thus far (user/passwords, etc.)

A client allows one to access databases via the '[ ]' operation.  Accessing a database will create it if it doesn't exist.

Mongo::DB

A Mongo::DB holds multiple collections which are accessed via the '[ ]'  operation.  Accessing a collection will create it if it doesn't exist.

Mongo::Collection

For basic use, I've come think of a MongoDB collection as an unordered set of associative arrays (aka hash table). I use the word set to imply that there is no natural index to the collection.

One inserts a hashtable into a collection using the #insert method.  MongoDB will extend the associative array with an additional key/value.  The key is "_id" and the value is a BSON::ObjectId.


In his book "Domain-Driven Design", Eric Evans talks about object retrieval.  There are two basic patterns which are by-name and by-property.  In the basic mode of operation, we let MongoDB assign a name (the _id attribute) and we use properties to locate and retrieve objects.

MongoDB Standalone

OSFedora 18
Ruby2.0.0

Rather than directly dive into adding MongoDB to my application, I thought I'd try it standalone first and get the bugs worked out.

I started by following the example form the MongoDB web-site:

http://api.mongodb.org/ruby/current/

The example didn't "just work". I needed the 'bson_ext' gem installed.
My Gemfile is now:
source 'https://rubygems.org'

# gem 'rubygems'

gem 'mongo'
gem 'bson_ext'
To get mongodb running, I followed the directions here: mongodb-service-not-running-in-fedora I had to start the service by hand and then enable it to automatically start on boot. The example now passes!