This project provides an example of integrating the MongoDB with Rails using the
MongoDB Ruby Driver. To implement this, we will create a model class that
encapsulates all interaction with the MongoDB collection. The class methods will
implement the standard ActiveModel all()
and find()
methods in addition to
convenience methods to return the mongo_client
and collection
. Instances of the class
will represent a specific document and its properties. The save()
, update()
, and
destroy()
instance methods are added to support further standard ActiveModel
behavior. Other properties required by the Rails scaffolding, which expects the model class
to be an ActiveModel instance, will also be added.
$ rails new zips
Add the Mongoid gem to the Gemfile
gem 'mongoid', '~> 5.0.0'
Run the bundler
$ bundle
Generate a mongo database configuration file.
$ rails g mongoid:config
(output:)
create config/mongoid.yml
While the defaults generated from the above command are fine, do make sure that the names
defined for development:clients:default:database
and test:clients:default:database
agree
with where you import the JSON in a later step.
$ cat config/mongoid.yml | grep -v \# | grep -v '^$'
development:
clients:
default:
database: zips_development
hosts:
- localhost:27017
options:
options:
test:
clients:
default:
database: zips_test
hosts:
- localhost:27017
options:
read:
mode: primary
max_pool_size: 1
Next, we need to add some configurations to config/application.rb
. This is used by stand-alone programs
like rails console
to be able to load the Mongoid environment with fewer steps. This also configures
which ORM your scaffold commands use by default. Adding the mongoid gem had the impact of making it
the default ORM. The lines below show how we can set it back to ether ActiveRecord or Mongoid. I am
leaving it as ActiveRecord here so that we do not rock the boat too soon.
module Zips
class Application < Rails::Application
...
# bootstraps mongoid within applications -- like rails console
Mongoid.load!('./config/mongoid.yml')
# which default ORM are we using with scaffold
# add --orm none, mongoid, or active_record
# to rails generate cmd line to be specific
config.generators {|g| g.orm :active_record}
# config.generators {|g| g.orm :mongoid}
# Do not swallow errors in after_commit/after_rollback callbacks.
config.active_record.raise_in_transactional_callbacks = true
end
end
$ rails s
Download the zips.json
from the MongoDB web site. The
following shows an example of downloading it using curl
. You may use a web browser. Place the
downloaded file in db/zips.json
, relative to the root of your application. The db
directory should
already exist.
curl http://media.mongodb.org/zips.json -o db/zips.json
Import the zips.json
into the zips
collection in the zips_development
database.
Note 1: The --db
name must match the name defined in config/mongoid.yml
Note 2: If not yet running, start your MongoDB Server using mongod
$ mongoimport --drop --db zips_development --collection zips --file db/zips.json
2015-09-12T19:44:10.785-0400 connected to: localhost
2015-09-12T19:44:11.554-0400 imported 29353 documents
To help familiarize you with the data we just imported, below is a represtantive document from
the zips.json
data set
{
"_id" : "01007",
"city" : "BELCHERTOWN",
"loc" : [ -72.410953, 42.275103 ],
"pop" : 10579,
"state" : "MA"
}
We will be manually creating an app/model/zip.rb
class that initially can obtain a connection to
the MongoDB server and database. We will be evolving this class in the subsequent sections, so to
start, the zip.rb
model will implement the following:
- a
mongo_client
class method that returns the default database connection - a
collection
class method that returns a reference to the zips collection.
These methods are consistent with methods implemented when using mongoid ORM.
class Zip
# convenience method for access to client in console
def self.mongo_client
Mongoid::Clients.default
end
# convenience method for access to zips collection
def self.collection
self.mongo_client['zips']
end
end
In the rails console
, invoke the zip
class methods to verify a connection can be made
and to ensure we are interacting with the intended database within Mongo. This database should
match with what was used as part of the mongoimport
command and should contain the zip codes
imported from zips.json
into our zips
collection.
$ rails c
Loading development environment (Rails 4.2.3)
> Zip.mongo_client
=> #<Mongo::Client:0x34369420 cluster=localhost:27017>
> Zip.mongo_client[:zips]
=> #<Mongo::Collection:0x22032060 namespace=zips_development.zips>
> Zip.collection
=> #<Mongo::Collection:0x21998940 namespace=zips_development.zips>
> Zip.collection.find.count
DEBUG | zips_development.count | STARTED | {"count"=>"zips", "query"=>{}}
=> 29353
Now that we have a connection to the database and can access the collection, it is time to get started implementing methods to perform CRUD against our model.
Lets add a method that will return all documents from the zips collection.
We will be starting with a
projection
that only returns the fields of interest -- in our case: _id
, city
, state
, and pop
. Since we
started off earlier examples by eliminating location, I am choosing to show a projection that eliminates
that property from our output.
def self.all
collection.find
.projection({_id:true, city:true, state:true, pop:true})
end
Within the rails console
, verify we can use our implementation of the all()
method that also
omits the location
property.
> Zip.all.count
=> 29353
> Zip.all.first
=> {"_id"=>"99743", "city"=>"HEALY", "pop"=>1058, "state"=>"AK"}
In order to expand this query, lets augment this method with the following:
- an optional find-by-prototype
- sorting
- paging parameters
Additionally, we will map the document term pop
with an internal term
population
so that we don't conflict with a reserved word later. Make sure
we keep a stable sort when manipulating the sort hash or our ordering
expressed to the DB could be randomized.
Please note that the example provided is a bit more complicated than required
because we have added a multi-level sort in addition to the field mappings.
The ordering of keys within the sort hash must state stable while performing the mapping
from population
within the application to pop
within the database..
def self.all(prototype={}, sort={:population=>1}, offset=0, limit=100)
#map internal :population term to :pop document term
tmp = {} #hash needs to stay in stable order provided
sort.each {|k,v|
k = k.to_sym==:population ? :pop : k.to_sym
tmp[k] = v if [:city, :state, :pop].include?(k)
}
sort=tmp
#convert to keys and then eliminate any properties not of interest
prototype=prototype.symbolize_keys.slice(:city, :state) if !prototype.nil?
Rails.logger.debug {"getting all zips, prototype=#{prototype}, sort=#{sort}, offset=#{offset}, limit=#{limit}"}
result=collection.find(prototype)
.projection({_id:true, city:true, state:true, pop:true})
.sort(sort)
.skip(offset)
result=result.limit(limit) if !limit.nil?
return result
end
Verify that the default arguments to our augmented all()
method returns only 100 documents. Observe
the MongoDB API debug statement to notice that the results are sorted by population.
> Zip.all.to_a.count
getting all zips, prototype={}, sort={:pop=>1}, offset=0, limit=100
zips_development.find | {"find"=>"zips", "filter"=>{},
"projection"=>{"_id"=>true, "city"=>true, "state"=>true, "pop"=>true},
"skip"=>0, "limit"=>100, "sort"=>{"pop"=>1}}
=> 100
Verify that the former default behavior can be obtained by passing in empty values for
prototype
, sort
, and limit
.
> Zip.all({},{},0,nil).to_a.count
getting all zips, prototype={}, sort={}, offset=0, limit=
zips_development.find | {"find"=>"zips", "filter"=>{},
"projection"=>{"_id"=>true, "city"=>true, "state"=>true, "pop"=>true}, "skip"=>0, "sort"=>{}}
=> 29353
Lets attempt a few additional query combinations for our updated all()
method
> Zip.all({'state':'NY'},{'population':-1},0,1).first
getting all zips, prototype={:state=>"NY"}, sort={:pop=>-1}, offset=0, limit=1
zips_development.find | {"find"=>"zips", "filter"=>{"state"=>"NY"},
"projection"=>{"_id"=>true, "city"=>true, "state"=>true, "pop"=>true},
"skip"=>0, "limit"=>1, "sort"=>{"pop"=>-1}}
=> {"_id"=>"11226", "city"=>"BROOKLYN", "pop"=>111396, "state"=>"NY"}
> Zip.all({'state':'NY'},{'population':1},0,1).first
getting all zips, prototype={:state=>"NY"}, sort={:pop=>1}, offset=0, limit=1
zips_development.find {"find"=>"zips", "filter"=>{"state"=>"NY"},
"projection"=>{"_id"=>true, "city"=>true, "state"=>true, "pop"=>true},
"skip"=>0, "limit"=>1, "sort"=>{"pop"=>1}}
=> {"_id"=>"13436", "city"=>"RAQUETTE LAKE", "pop"=>0, "state"=>"NY"}
Implement the initialize()
method so that it can accept a hash for both the internal (:id
and :populate
)
and external (:_id
and :pop
) views of our fields. The mapping of :_id
to :id
will help integrate with
Rails scaffold. The mapping from :pop
to :populate
helps us avoid overriding a method introduced later.
Notice the initialize()
method's params
hash must account for the id
coming in from the view as :id
,
while coming in from the database as :_id
. Both Rails and MongoDB have specific names they want for this
key and we must account for this difference.
attr_accessor :id, :city, :state, :population
def to_s
"#{@id}: #{@city}, #{@state}, pop=#{@population}"
end
# initialize for both Mongo and a Web hash
def initialize(params={})
#switch between both internal and external views of id and population
@id=params[:_id].nil? ? params[:id] : params[:_id]
@city=params[:city]
@state=params[:state]
@population=params[:pop].nil? ? params[:population] : params[:pop]
end
Test out the initializer by creating an instance of the Zip class from a document returned from MongoDB query.
> doc=Zip.all({'state':'NY'},{'population':-1},0,1).first
=> {"_id"=>"11226", "city"=>"BROOKLYN", "pop"=>111396, "state"=>"NY"}
> obj=Zip.new(doc)
=> #<Zip:0x0000000829c130 @id="11226", @city="BROOKLYN", @state="NY", @population=111396>
Enable find()
to query by the :_id=>id
passed into the method. If found, return a Zip
instance
initialized from the document hash returned by MongoDB.
def self.find id
Rails.logger.debug {"getting zip #{id}"}
doc=collection.find(:_id=>id)
.projection({_id:true, city:true, state:true, pop:true})
.first
return doc.nil? ? nil : Zip.new(doc)
end
Lets test this method by obtaining a Zip
instance for a particular zipcode and report its population.
> Zip.find("11226").population
getting zip 11226
zips_development.find | {"find"=>"zips", "filter"=>{"_id"=>"11226"},
"projection"=>{"_id"=>true, "city"=>true, "state"=>true, "pop"=>true}}
=> 111396
The save()
method should preserve the state of the current zip
instance.
Note that although we are manually assigning the _id
in this case and already
know the value without consulting the result
. However, for dyanamic _id
assignment
cases, we can get the inserted_id
from the result
.
def save
Rails.logger.debug {"saving #{self}"}
result=collection.insert_one(_id:@id, city:@city, state:@state, pop:@pop)
@id=result.inserted_id
end
We will create a Zip
instance and then call save()
to insert it into the Database
> zip=Zip.new({'id':"00001",'city':"Fake City",'state':"WY",'population':3})
=> #<Zip:0x00000006fd90c0 @id="00001", @city="Fake City", @state="WY", @population=3>
> zip.save
saving 00001: Fake City, WY, pop=3
zips_development.insert | STARTED | {"insert"=>"zips", "documents"=>[{"_id"=>"00001",
"city"=>"Fake City", "state"=>"WY", "pop"=>3}], "writeConcern"=>{"w"=>1}, "ordered"=>true}
=> "00001"
Create an update()
method that accepts a hash and performs an update on those
values after accounting for any name mappings.
Note that here -- again -- our method would be much simpler if we did
not have to manually map pop
to population
internally.
def update(updates)
Rails.logger.debug {"updating #{self} with #{updates}"}
# map internal :population term to :pop document term
updates[:pop]=updates[:population] if !updates[:population].nil?
updates.slice!(:city, :state, :pop) if !slice.nil?
self.class.collection
.find(_id:@id)
.update_one(:$set=>updates)
end
To test update()
we will obtain a zip instance using find()
, then update its population
from 3 to 4.
Notice that :$set
was used to change specific field(s), without changing fields not supplied.
> zip=Zip.find "00001"
getting zip 00001
=> #<Zip:0x00000005835f68 @id="00001", @city="Fake City", @state="WY", @population=3>
> zip.update({:population=>4})
updating 00001: Fake City, WY, pop=3 with {:population=>4}
zips_development.update | {"update"=>"zips", "updates"=>[{"q"=>{:_id=>"00001"},
"u"=>{:$set=>{:pop=>4}}, "multi"=>false, "upsert"=>false}], "writeConcern"=>{:w=>1}, "ordered"=>true}
=> #<Mongo::Operation::Result:46208000 documents=[{"ok"=>1, "nModified"=>1, "n"=>1}]>
> zip=Zip.find("00001").population
getting zip 00001
=> 4
destroy()
will delete the document from the database that is associated with
the instance's :id
.
def destroy
Rails.logger.debug {"destroying #{self}"}
self.class.collection
.find(_id:@id)
.delete_one
end
Load an instance with the state of one of the cities and remove that city from the database.
> zip=Zip.find "00001"
getting zip 00001
zips_development.find | {"find"=>"zips", "filter"=>{"_id"=>"00001"},
"projection"=>{"_id"=>true, "city"=>true, "state"=>true, "pop"=>true}}
=> #<Zip:0x0000000647cee8 @id="00001", @city="Fake City", @state="WY", @population=4>
> zip.destroy
destroying 00001: Fake City, WY, pop=4
zips_development.delete | {"delete"=>"zips",
"deletes"=>[{"q"=>{"_id"=>"00001"}, "limit"=>1}], "writeConcern"=>{"w"=>1}, "ordered"=>true}
=> #<Mongo::Operation::Result:52648160 documents=[{"ok"=>1, "n"=>1}]>
> zip=Zip.find "00001"
getting zip 00001
=> nil
We need to include the ActiveModel::Model
mixin and override its persisted?
implementation
to simply return the result of whether a primary key has been assigned. JSON marshalling
will also expect a created_at
and updated_at
by default.
class Zip
include ActiveModel::Model
...
def persisted?
!@id.nil?
end
def created_at
nil
end
def updated_at
nil
end
Our class should now be at a point where it can integrate with Rails as an official Model class -- with some help.
class Zip
include ActiveModel::Model
attr_accessor :id, :city, :state, :population
def to_s
"#{@id}: #{@city}, #{@state}, pop=#{@population}"
end
# initialize from both a Mongo and Web hash
def initialize(params={})
#switch between both internal and external views of id and population
@id=params[:_id].nil? ? params[:id] : params[:_id]
@city=params[:city]
@state=params[:state]
@population=params[:pop].nil? ? params[:population] : params[:pop]
end
# tell Rails whether this instance is persisted
def persisted?
!@id.nil?
end
def created_at
nil
end
def updated_at
nil
end
# convenience method for access to client in console
def self.mongo_client
Mongoid::Clients.default
end
# convenience method for access to zips collection
def self.collection
self.mongo_client['zips']
end
# implement a find that returns a collection of document as hashes.
# Use initialize(hash) to express individual documents as a class
# instance.
# * prototype - query example for value equality
# * sort - hash expressing multi-term sort order
# * offset - document to start results
# * limit - number of documents to include
def self.all(prototype={}, sort={:population=>1}, offset=0, limit=100)
#map internal :population term to :pop document term
tmp = {} #hash needs to stay in stable order provided
sort.each {|k,v|
k = k.to_sym==:population ? :pop : k.to_sym
tmp[k] = v if [:city, :state, :pop].include?(k)
}
sort=tmp
#convert to keys and then eliminate any properties not of interest
prototype=prototype.symbolize_keys.slice(:city, :state) if !prototype.nil?
Rails.logger.debug {"getting all zips, prototype=#{prototype}, sort=#{sort}, offset=#{offset}, limit=#{limit}"}
result=collection.find(prototype)
.projection({_id:true, city:true, state:true, pop:true})
.sort(sort)
.skip(offset)
result=result.limit(limit) if !limit.nil?
return result
end
# locate a specific document. Use initialize(hash) on the result to
# get in class instance form
def self.find id
Rails.logger.debug {"getting zip #{id}"}
doc=collection.find(:_id=>id)
.projection({_id:true, city:true, state:true, pop:true})
.first
return doc.nil? ? nil : Zip.new(doc)
end
# create a new document using the current instance
def save
Rails.logger.debug {"saving #{self}"}
self.class.collection
.insert_one(_id:@id, city:@city, state:@state, pop:@population)
end
# update the values for this instance
def update(updates)
Rails.logger.debug {"updating #{self} with #{updates}"}
#map internal :population term to :pop document term
updates[:pop]=updates[:population] if !updates[:population].nil?
updates.slice!(:city, :state, :pop) if !updates.nil?
self.class.collection
.find(_id:@id)
.update_one(:$set=>updates)
end
# remove the document associated with this instance form the DB
def destroy
Rails.logger.debug {"destroying #{self}"}
self.class.collection
.find(_id:@id)
.delete_one
end
end
Generate the controller and view using a scaffold command that does not create a model class.
$ rails g scaffold_controller Zip id city state population:integer
create app/controllers/zips_controller.rb
invoke erb
create app/views/zips
create app/views/zips/index.html.erb
create app/views/zips/edit.html.erb
create app/views/zips/show.html.erb
create app/views/zips/new.html.erb
create app/views/zips/_form.html.erb
invoke test_unit
create test/controllers/zips_controller_test.rb
invoke helper
create app/helpers/zips_helper.rb
invoke test_unit
invoke jbuilder
create app/views/zips/index.json.jbuilder
create app/views/zips/show.json.jbuilder
Verify the route to the new controller is in place in config/routes.rb
Rails.application.routes.draw do
resources :zips
Access the new page and observe an error between what was returned
by the Model (a hash with an :_id
key) and what is required by the view
(an instance of a class with the id() method).
http://localhost:3000/zips
undefined method `id' for {"_id"=>"99773", "city"=>"SHUNGNAK", "pop"=>0, "state"=>"AK"}:BSON::Document
<% @zips.each do |zip| %>
<tr>
<td><%= zip.id %></td>
<td><%= zip.city %></td>
<td><%= zip.state %></td>
<td><%= zip.population %></td>
Add the following helper method to the app/helpers/zips_helper.rb
to
convert a Mongo document to a Ruby class instance. We left it as a document
so the all()
method did not have to EAGERly access every document in the result
set before it was know it would be used.
module ZipsHelper
def toZip(value)
#change value to a Zip if not already a Zip
return value.is_a?(Zip) ? value : Zip.new(value)
end
end
Add a call to the helper method in app/views/zips/index.html.erb
<% @zips.each do |zip| %>
<% zip=toZip(zip) %>
Not there is a similar issue with the JSON view as well
http://localhost:3000/zips.json
key not found: :id
json.array!(@zips) do |zip|
json.extract! zip, :id, :id, :city, :state, :population
json.url zip_url(zip, format: :json)
end
Fix the JSON view error by calling the helper method in app/helpers/zips/index.json.jbuilder
.
json.array!(@zips) do |zip|
zip=toZip(zip)
json.extract! zip, :id, :id, :city, :state, :population
json.url zip_url(zip, format: :json)
end
In our ZipsController
, prior to a :show
, :edit
, :update
, or :destroy
action being executed,
we will first invoke the set_zip()
method to retreive the specific Zip
by id
. The generated helper
comes ready to call Zip.find
and expect to get an instance back.
The zip_params()
method restricts mass assignments for :zip
parameters to the fields of
:id
, :city
, :state
, and :populaton
.
class ZipsController < ApplicationController
before_action :set_zip, only: [:show, :edit, :update, :destroy]
private
# Use callbacks to share common setup or constraints between actions.
def set_zip
@zip = Zip.find(params[:id])
end
# Never trust parameters from the scary internet, only allow the white list through.
def zip_params
params.require(:zip).permit(:id, :city, :state, :population)
end
index()
retrieves all the Zips
. The generated action method comes ready to call Zip.all
.
http://localhost:3000/zips
http://localhost:3000/zips.json
def index
@zips = Zip.all
end
show()
retrieves a specific Zip
based upon its id
. The generated action method is fully
implemented by the generated set_zip
helper method.
#GET /zips/{id}
#GET /zips/{id}.json
before_action :set_zip, only: [:show, :edit, :update, :destroy]
def set_zip
@zip = Zip.find(params[:id])
end
def show
end
new()
returns an initial prototype to the form to create a new Zip
.
create()
accepts the results and creates a new Zip
instance in the database.
Both of these generated action methods come ready to call Zip.new
, which uses the initialize
method. The generated create action also comes ready to invoke save
on the Zip
instance.
#POST /zips/new
def new
@zip = Zip.new
end
#POST /zips
def create
@zip = Zip.new(zip_params)
respond_to do |format|
if @zip.save
format.html { redirect_to @zip, notice: 'Zip was successfully created.' }
format.json { render :show, status: :created, location: @zip }
else
format.html { render :new }
format.json { render json: @zip.errors, status: :unprocessable_entity }
end
end
end
edit()
retrieves the instance from the database based upon its id
.
update()
applies the changes to the retrieved instance provided by edit()
.
Both generated action methods rely on the before_action
and the generated update
action also comes ready to call update
on the Zip
instance.
http://localhost:3000/zips/00002/edit
#GET /zips/{id}
before_action :set_zip, only: [:show, :edit, :update, :destroy]
def set_zip
@zip = Zip.find(params[:id])
end
def edit
end
#PUT /zips/{id}
def update
respond_to do |format|
if @zip.update(zip_params)
format.html { redirect_to @zip, notice: 'Zip was successfully updated.' }
format.json { render :show, status: :ok, location: @zip }
else
format.html { render :edit }
format.json { render json: @zip.errors, status: :unprocessable_entity }
end
end
end
destroy()
removes a specific Zip
instance based upon its id
. The generated action
method comes ready to call destroy
on the Zip
instance.
#DELETE /zips/{id}
def destroy
@zip.destroy
respond_to do |format|
format.html { redirect_to zips_url, notice: 'Zip was successfully destroyed.' }
format.json { head :no_content }
end
end
Add a second line to config/routes.rb
to make zips the root application.
Rails.application.routes.draw do
root 'zips#index'
Add Pagination
gem 'will_paginate', '~> 3.0.7'
bundle
This command will add page properties to the displayed view and
controls advance paging. Specifically, it can pass :page
as
a number >= 1. will_paginate
also uses :per_page
to express the
row limit for a single page.
<table>
...
<tbody>
<% @zips.each do |zip| %>
<% zip=toZip(zip) %>
<tr>
<td><%= zip.id %></td>
...
</tr>
<% end %>
</tbody>
</table>
<%= will_paginate @zips %>
The controller is passing a controlled set of parameters to the
Model.paginate
call. The page number is currently the only value passed
but :per_page
could be specified here as well.
def index
#@zips = Zip.all
@zips = Zip.paginate(:page => params[:page])
end
This method implements a facade around the all()
method by translating the
will_paginate
inputs into all()
query inputs and converts the document
array results into a will_paginate
result that contains such things as
total number of documents.
def self.paginate(params)
Rails.logger.debug("paginate(#{params})")
page=(params[:page] ||= 1).to_i
limit=(params[:per_page] ||= 30).to_i
offset=(page-1)*limit
#get the associated page of Zips -- eagerly convert doc to Zip
zips=[]
all({}, {}, offset, limit).each do |doc|
zips << Zip.new(doc)
end
#get a count of all documents in the collection
total=all({}, {}, 0, 1).count
WillPaginate::Collection.create(page, limit, total) do |pager|
pager.replace(zips)
end
end
A reference on how to use will_paginate
As a quick test to verify our current will_paginate
implementation, the following URL should land us on the 3rd page of "Listing Zips"
http://localhost:3000/?page=3
Given the following URL:
http://localhost:3000/?page=38&per_page=10&sort=population:-1,city:1&state=MD
Lets implement additional functionality that will allow our application to:
- Specify a particular page number:
page=38
- Define a row limit per page:
per_page=10
- Order the results by population DESC, city ASC:
sort=population:-1,city:1
- Define zips for a particular state:
state=MD
Create a Helper Method in the Controller to Convert the Sort Query Param to a MongoDB Query Sort Hash
app/controller/zips_controller.rb
private
#create a hash sort spec from query param
#sort=state:1,city,population:-1
#{state:1, city:1, population:-1}
def get_sort_hash(sort)
order={}
if (!sort.nil?)
sort.split(",").each do |term|
args=term.split(":")
dir = args.length<2 || args[1].to_i >= 0 ? 1 : -1
order[args[0]] = dir
end
end
return order
end
This passes right to our Model.paginate
call where we can add a small amount of processing to pass it
through to the all()
method.
def index
#@zips = Zip.all
#@zips = Zip.paginate(params)
args=params.clone #update a clone of params
args[:sort]=get_sort_hash(args[:sort]) #replace sort with hash
@zips = Zip.paginate(args)
end
app/models/zip.rb
def self.paginate(params)
...
sort=params[:sort] ||= {}
...
all(params, sort, offset, limit).each do |doc|
...
total=all(params, sort, 0, 1).count
...
end
With our selection criteria and ordering logic now in place, the below URL:
http://localhost:3000/?page=38&per_page=10&sort=population:-1,city:1&state=MD
should render us results similiar to this:
###Listing Zips
Id | City | State | Population | |||
---|---|---|---|---|---|---|
21522 | BITTINGER | MD | 479 | Show | Edit | Destroy |
21156 | Upper Falls | MD | 464 | Show | Edit | Destroy |
20632 | FAULKNER | MD | 459 | Show | Edit | Destroy |
21677 | WOOLFORD | MD | 459 | Show | Edit | Destroy |
21816 | CHANCE | MD | 415 | Show | Edit | Destroy |
20630 | DRAYDEN | MD | 413 | Show | Edit | Destroy |
20779 | TRACYS LANDING | MD | 413 | Show | Edit | Destroy |
20615 | BROOMES ISLAND | MD | 404 | Show | Edit | Destroy |
21672 | TODDVILE | MD | 361 | Show | Edit | Destroy |
21840 | NANTICOKE | MD | 358 | Show | Edit | Destroy |
<-- Previous 1 2 ... 34 35 36 37 38 39 40 41 42 Next -->
If you flip the value of city:1
from 1
to -1
, should will see the ordering of DRAYDEN
and
TRACYS LANDING
switch places.
-
Create an
mLab Account
https://mlab.com/home
-
Create a (Free Sandbox) Database on mLab
-
From your mLab home page, select
Create New
MongoDB Deployments -
For
Cloud Provider:
selectAmazon Web Services
- On the
Location:
Pull Down Menu, select the Region that is geographically closest to you
- On the
-
Under Plan: select the
Single-node
Option- Select the Free
Sanbox
option underStandard Line
- Select the Free
-
Leave the
High Storage Line
options blank -
In the
Database name:
input field, supply a name (i.e., zips_production) -
Verify the
Price:
field calculator is$0 / month
-
Select the
Create new MongoDB deployment
button -
On your mLab home page, you should now see your database (i.e., zips_production) listed
-
Now select your newly created MongoDB deployment
-
The following URL template should be displayed with the details pertaining to your deployment
To connect using a driver via the standard MongoDB URI: mongodb://<dbuser>:<dbpass>@<dbhost>/<dbname>
-
-
Create a Database User and Password on mLab
- Select the
Users
Menu - Select the
Add database user
button - In the
Add new database user
form, supply a username and password:<dbuser>
<dbpass>
- then select
Create
- Select the
-
Import
zips.json
from MongoDB using the database and user account created above$ wget http://media.mongodb.org/zips.json $ mongoimport -h dbhost -d dbname -c zips -u dbuser -p dbpass --file zips.json 2015-12-07T18:03:34.015-0500 connected to: ds######.mlab.com:<port> 2015-12-07T18:03:36.416-0500 [################........] <dbname>.<collection_name> 2.1 MB/3.0 MB (68.5%) 2015-12-07T18:03:38.953-0500 imported 29353 documents
-
Create a
Heroku Account
- If not yet installed, download and install the Heroku Toolbelt.
- This client CLI will be used in later steps that rely on
heroku
commands
-
Register your application with Heroku by changing to the directory with a git repository and invoking
heroku apps:create (appname)
.Note that your application must be in the root directory of the development folder hosting the git repository.
$ cd zips $ heroku apps:create appname Creating appname... done, stack is cedar-14 https://appname.herokuapp.com/ | https://git.heroku.com/appname.git Git remote heroku added
This will add an additional remote to your git repository.
$ git remote --verbose heroku https://git.heroku.com/appname.git (fetch) heroku https://git.heroku.com/appname.git (push) ...
-
Add a
MONGOLAB_URI
environment variable wheredbhost
is both host and port# concatenated together, separated by a ":" (host:port) .$ heroku config:add MONGOLAB_URI=mongodb://dbuser:dbpass@dbhost/dbname
-
Add a
production
profile to theconfig/mongoid.yml
file. The followingMongoid connection information
was provided by theMongoLab
page on theHeroku
Dev Center page.production: clients: default: uri: <%= ENV['MONGOLAB_URI'] %> options: connect_timeout: 15
-
Update the
Gemfile
so that Heroku will accept and deploy our application.Restrict the
sqlite
gem inGemfile
to the development profile. Heroku does not supportsqlite
and this application does not use anRDBMS
. However, this gem was put there by byrails new
by default and required to stick around because we have not removed ActiveRecord from the application.gem 'sqlite3', group: :development
Add the postgres gem to the production profile. We have not neutered the application of ActiveRecord and Heroku wants a supported database for that platform.
group :production do #use postgres on heroku gem 'pg' gem 'rails_12factor' end
Be sure to run bundle when complete and check the Gemfile.lock file into git.
$ bundle
-
Commit changes to application
$ git commit -am "ready for heroku deploy"
-
Deploy application
$ git push heroku master
-
Access URL
http://appname.herokuapp.com
-
Access logs
$ heroku logs