build your first mongodb app in ruby @ strangeloop 2013
DESCRIPTION
A workshop given at StrangeLoop 2013 teaching you how to build your first MongoDB application in RubyTRANSCRIPT
Building your rst MongoDB Acaon
AgdaIntroduction to MongoDB
MongoDB Fundamentals
Running MongoDB
Schema Design
Ruby & Sinatra Crash Course
Building Our First App
@spf13Steve Francia
AKA
Chief Developer Advocate @ responsible for drivers, integrations, web & docs
Iroducon to
mongodb
What Is MongoDB?
*Document
*Open source
*High performance
*Horizontally scalable
*Full featured
MongoDB is a ___________ database
* Not for .PDF & .DOC files
* A document is essentially an associative array
* Document == JSON object
* Document == PHP Array
* Document == Python Dict
* Document == Ruby Hash
* etc
Documt Database
*MongoDB is an open source project
*On GitHub
* Licensed under the AGPL
*Commercial licenses available
*Contributions welcome
Op Source
* Written in C++
* Extensive use of memory-mapped files i.e. read-through write-through memory caching.
* Runs nearly everywhere
* Data serialized as BSON (fast parsing)
* Full support for primary & secondary indexes
* Document model = less work
High Pfoance
Hozoal Scable
* Ad Hoc queries
* Real time aggregation
* Rich query capabilities
* Traditionally consistent
* Geospatial features
* Support for most programming languages
* Flexible schema
Full Ftured
Depth of Functionality
Scal
abili
ty &
Per
form
ance
Memcached
MongoDB
RDBMS
Database ndscape
tp://w.mongodb.org/downloads
What is a Record?
K → Value* One-dimensional storage
* Single value is a blob
* Query on key only
* No schema
* Value cannot be updated, only replaced
Key Blob
Raonal* Two-dimensional storage
(tuples)
* Each field contains a single value
* Query on any field
* Very structured schema (table)
* In-place updates
* Normalization process requires many tables, joins, indexes, and poor data locality
Primary Key
Documt* N-dimensional storage
* Each field can contain 0, 1, many, or embedded values
* Query on any field & level
* Flexible schema
* Inline updates *
* Embedding related data has optimal data locality, requires fewer indexes, has better performance
_id
Running
Mongodb
MongoD
Mongo Shl
user = { username: 'fred.jones', first_name: 'fred', last_name: 'jones',}
Sta wh an object (or ay, hash, dict, c)
> db.users.insert(user)
Inst e record
No collection creation needed
> db.users.findOne()
{
"_id" : ObjectId("50804d0bd94ccab2da652599"),
"username" : "fred.jones",
"first_name" : "fred",
"last_name" : "jones"
}
Quying for e us
* _id is the primary key in MongoDB
*Automatically indexed
*Automatically created as an ObjectId if not provided
*Any unique immutable value could be used
_id
*ObjectId is a special 12 byte value
*Guaranteed to be unique across your cluster
* ObjectId("50804d0bd94ccab2da652599") |-------------||---------||-----||----------| ts mac pid inc
ObjectId
SchaDesign
Tdaonal scha design Focuses on data stoge
Documt scha design
Focuses on use
4 Building blocksof Documt Design
exibi* Choices for schema design
* Each record can have different fields
* Field names consistent for programming
* Common structure can be enforced by application
* Easy to evolve as needed
Ays* Each field can be:
* Absent
* Set to null
* Set to a single value
* Set to an array of many values
* Query for any matching value
* Can be indexed and each value in the array is in the index
beed Documts* An acceptable value is a
document
* Nested documents provide structure
* Query any field at any level
* Can be indexed
*Object in your model
*Associations with other entities
Aßociaon
Referencing (Relational) Embedding (Document)
has_one embeds_one
belongs_to embedded_in
has_many embeds_many
has_and_belongs_to_many
MongoDB has both referencing and embedding for universal coverage
Excise 1:Mod a busineß card
Busineß Card
Contacts
{ “_id”: 2, “name”: “Steven Jobs”, “title”: “VP, New Product Development”, “company”: “Apple Computer”, “phone”: “408-996-1010”, “address_id”: 1}
RcingAddresses
{ “_id”: 1, “street”: “10260 Bandley Dr”, “city”: “Cupertino”, “state”: “CA”, “zip_code”: ”95014”, “country”: “USA”}
Contacts
{ “_id”: 2, “name”: “Steven Jobs”, “title”: “VP, New Product Development”, “company”: “Apple Computer”, “address”: { “street”: “10260 Bandley Dr”, “city”: “Cupertino”, “state”: “CA”, “zip_code”: ”95014”, “country”: “USA” }, “phone”: “408-996-1010”}
being
Excise 2:Store a busineß card
Contactsdb.contacts.insert({ “_id”: 2, “name”: “Steven Jobs”, “title”: “VP, New Product Development”, “company”: “Apple Computer”, “phone”: “408-996-1010”, “address_id”: 1})
Insng wh RceAddressesdb.addresses.insert({ “_id”: 1, “street”: “10260 Bandley Dr”, “city”: “Cupertino”, “state”: “CA”, “zip_code”: ”95014”, “country”: “USA”})
Excise 3:Re a busineß card
Contactsc = db.contacts.findOne({ “name”: “Steven Jobs”,})
Quying wh RceAddressesdb.addresses.findOne({ “_id”: c.address_id, “street”: “10260 Bandley Dr”, “city”: “Cupertino”, “state”: “CA”, “zip_code”: ”95014”, “country”: “USA”})
Building aMongoDBAcaon
MongoDB has nave bindings
for nr all nguages
Official Support for 12 languages
Community drivers for tons more
Drivers connect to mongo servers
Drivers translate BSON into native types
mongo shell is not a driver, but works like one in some ways
Installed using typical means (npm, pecl, gem, pip)
MongoDB dvs
Building an a in Ruby?Had to pick a language
Sinatra is very minimal and approachable
Wanted to focus on MongoDB interaction
Ruby gems are awesome
Works well on Windows, OS X & Linux
Seemed like a good idea at the time
RubyCsh Course
hing is an object1.class'a'.class:z.class
class Foo end
Foo.classFoo.new.class
# => Fixnum# => String# => Symbol
# => Class
# => Foo
StructureMethod
Class
Invocation
def do_stuff(thing) thing.do_the_stuffend
class TheThing def do_the_stuff puts "Stuff was done!" endend
do_stuff(TheThing.new)
Stngsname = 'World' # => "World"
"Hello, #{name}" # => "Hello, World"
'Hello, #{name}' # => "Hello, \#{name}"
Numbs1 + 1 # => 21 + 1.1 # => 2.16 * 7 # => 426 ** 7 # => 279936
Math.sqrt(65536) # => 256.01.class # => Fixnum(2 ** 42).class # => Fixnum(2 ** 64).class # => Bignum1.1.class # => Float
AysArray.newArray.new(3)[]
a = [1,2,3]a[0] = 'one'aa[-1]a[1..2]
# => []# => [nil, nil, nil]# => []
# => [1, 2, 3]# => "one"# => ["one", 2, 3]# => 3# => [2, 3]
HashesHash.new {} h = {1 => "one", 2 => "two"}h[1] h["1"]h[:one] = "einz"h[:one]
h.keysh.values
# => {}# => {}
# => "one"# => nil# => "einz"# => "einz"
# => [1, 2, :one]# => ["one", "two", "einz"]
Vaables & NamesCamelCased # Classes, moduleswith_underscores # methods, local variables@instance_variable@@class_variable$GLOBAL_VARIABLE
Corol Structuresif condition # ...elsif other_condition # ...end
unless condition # ...end
while# ...end
Sinat is...not Rails
not a framework
a DSL for quickly creating web applications in Ruby with minimal effort
Hlo World
# myapp.rbrequire 'sinatra'
get '/' do 'Hello world!'end
TP AconsIn Sinatra, a route is an HTTP method paired with a URL-matching pattern.
Each route is associated with a block:
get '/' do .. show something ..end
post '/' do .. create something ..end
put '/' do .. replace something ..end
delete '/' do .. annihilate something ..end
RoesRoutes are matched in the order they are defined. The first route that matches the request is invoked.
Route patterns may include named parameters, accessible via the params hash:
get '/hello/:name' do # matches "GET /hello/foo" and "GET /hello/bar" # params[:name] is 'foo' or 'bar' "Hello #{params[:name]}!"end
#You can also access named parameters via block parameters:get '/hello/:name' do |n| "Hello #{n}!"end
SptRoute patterns may also include splat (or wildcard) parameters, accessible via the params[:splat] array:
get '/say/*/to/*' do # matches /say/hello/to/world params[:splat] # => ["hello", "world"]end
get '/download/*.*' do # matches /download/path/to/file.xml params[:splat] # => ["path/to/file", "xml"]end
Building our A in Ruby
Iroducing e mieu a
pre-popung our database
Download & Impo e vues⇒ curl -L http://j.mp/StrangeLoopVenues | \ mongoimport -d milieu -c venues
wget http://c.spf13.com/dl/StrangeLoopVenues.json mongoimport -d milieu -c venues StrangeLoopVenues.json
Database
Collection
Mongo Shl
L’s lk at e vues> use milieuswitched to db milieu
> db.venues.count()50
Database
L’s lk at e vues> db.venues.findOne(){ "_id" : ObjectId("52335163695c9d31c2000001"), "location" : { "address" : "1820 Market St", "distance" : 85, "postalCode" : "63103", "city" : "Saint Louis", "state" : "MO", "country" : "United States", "cc" : "US", "geo" : [ -90.20761747801353, 38.62893438211461 ] }, "name" : "St. Louis Union Station Hotel- A DoubleTree by Hilton", "contact" : { "phone" : "3146215262", "formattedPhone" : "(314) 621-5262", "url" : "http://www.stlunionstationhotel.com" }, "stats" : { "checkinsCount" : 0, "usersCount" : 0 }}
Crng a Geo index> db.venues.ensureIndex({ 'location.geo' : '2d'})> db.venues.getIndexes()[ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "milieu.venues", "name" : "_id_" }, { "v" : 1, "key" : { "location.geo" : "2d" }, "ns" : "milieu.venues", "name" : "location.geo_" }]
Skon
Sta wh a skon/Users/steve/Code/milieu/app/
▸ config/▸ helpers/▾ model/ mongodb.rb mongoModule.rb user.rb▾ public/ ▸ bootstrap/ ▾ css/ styles.css ▸ images/▾ views/ footer.haml index.haml layout.haml
login.haml navbar.haml register.haml user_dashboard.haml user_profile.haml venue.haml venues.haml app.rb config.ru Gemfile Rakefile README
Download & Install deps⇒ mkdir milieu⇒ cd milieu⇒ wget http://c.spf13.com/dl/GettingStarted.tgz⇒ tar zxvf GettingStarted.tgz⇒ bundle installResolving dependencies...Using bson (1.9.2)Using bson_ext (1.9.2)Using googlestaticmap (1.1.4)Using tilt (1.4.1)Using haml (4.0.3)Using mongo (1.9.2)Using rack (1.5.2)Using rack-protection (1.5.0)Using shotgun (0.9)Using sinatra (1.4.3)Using bundler (1.3.5)Your bundle is complete! Use `bundle show [gemname]` to see where a bundled gem is installed.
Run a⇒ shotgun== Shotgun/WEBrick on http://127.0.0.1:9393/[2013-09-13 21:25:43] INFO WEBrick 1.3.1[2013-09-13 21:25:43] INFO ruby 2.0.0 (2013-06-27) [x86_64-darwin12.3.0][2013-09-13 21:25:43] INFO WEBrick::HTTPServer#start: pid=85344 port=9393
Op Brows to locaost:9393
Pri --- ror Scre
sngVues
Connecng to MongoDB
require 'mongo'require './model/mongoModule'require './model/user'
# Connection code goes hereCONNECTION = Mongo::Connection.new("localhost")DB = CONNECTION.db('milieu')
# Alias to collections goes hereUSERS = DB['users']VENUES = DB['venues']CHECKINS = DB['checkins']
model/mongodb.rb
sng Vues
get '/venues' do # Code to list all venues goes here @venues = VENUES.░░░░░ haml :venuesend
app.rb
sng Vues
get '/venues' do # Code to list all venues goes here @venues = VENUES.find haml :venuesend
app.rb
sng Vues.container .content %h2 Venues %table.table.table-striped %thead %tr %th Name %th Address %th Longitude %th Latitude
%tbody [email protected] do |venue| %tr %td %a{:href => '/venue/' << venue['_id'].to_s}= venue['name'] %td= venue['location']['address'] ? venue['location']['address'] : ' ' %td= venue['location']['geo'][0].round(2) %td= venue['location']['geo'][1].round(2)
views/venues.haml
sng Vueslocalhost:9393/venues
Paginang Vues
get '/venues/?:page?' do @page = params.fetch('page', 1).to_i pp = 10 @venues = VENUES.find.░░░░░(░░░░).░░░░(░░░) @total_pages = (VENUES.░░░░░.to_i / pp).ceil haml :venuesend
app.rb
# replaces the prior entry
Paginang Vues
get '/venues/?:page?' do @page = params.fetch('page', 1).to_i pp = 10 @venues = VENUES.find.skip((@page - 1) * pp).limit(pp) @total_pages = (VENUES.count.to_i / pp).ceil haml :venuesend
app.rb
# replaces the prior entry
.container .content %h2 Venues %table.table.table-striped %thead %tr %th Name %th Address %th Longitude %th Latitude
%tbody [email protected] do |venue| %tr %td %a{:href => '/venue/' << venue['_id'].to_s}= venue['name'] %td= venue['location']['address'] ? venue['location']['address'] : ' ' %td= venue['location']['geo'][0].round(2) %td= venue['location']['geo'][1].round(2)
=pager('/venues')
sng Vuesviews/venues.haml
paging rough Vueslocalhost:9393/venues
Crng
Uss
Crng UssUsers are a bit special in our app
Not just data
Special considerations for secure password handling
Not complicated on MongoDB side, but slightly complicated on Ruby side
Crng Ussclass User
attr_accessor :_id, :name, :email, :email_hash, :salt, :hashed_password, :collection, :updated_at
def init_collection self.collection = 'users' end
def password=(pass) self.salt = random_string(10) unless self.salt self.hashed_password = User.encrypt(pass, self.salt) end
def save col = DB[self.collection] self.updated_at = Time.now col.save(self.to_hash) endend
model/user.rb
Inherited fromMongoModule.rb
Crng Usspost '/register' do u = User.new u.email = params[:email] u.password = params[:password] u.name = params[:name]
if u.save() flash("User created") session[:user] = User.auth( params["email"], params["password"]) redirect '/user/' << session[:user].email.to_s << "/dashboard" else tmp = [] u.errors.each do |e| tmp << (e.join("<br/>")) end flash(tmp) redirect '/create' endend
app.rb
Loing in pa 1configure do enable :sessionsend
before do unless session[:user] == nil @suser = session[:user] endend
get '/user/:email/dashboard' do haml :user_dashboardend
app.rb
Loing in pa 2
post '/login' do if session[:user] = User.auth(params["email"], params["password"]) flash("Login successful") redirect "/user/" << session[:user].email << "/dashboard" else flash("Login failed - Try again") redirect '/login' endend
app.rb
Loing in pa 3
def self.auth(email, pass) u = USERS.find_one("email" => email.downcase) return nil if u.nil? return User.new(u) if User.encrypt( pass, u['salt']) == u['hashed_password'] nil end
user.rb
Us Dashboard.container .content .page-header -unless @suser == nil? %h2="Dashboard" %br %image{src: "http://www.gravatar.com/avatar/" << @suser.email_hash.to_s << '.png'}
%h3= @suser.name.to_s -else redirect '/' %small %a{href: "/user/" << @suser.email.to_s << "/profile"} profile .container#main-topic-nav
views/user_dashboard.haml
Dashboardlocalhost:9393/dashboard
Viing
Uss
nding a us
get '/user/:email/profile' do u = USERS.░░░░░( ░░░░░ => ░░░░░.░░░░░) if u == nil return haml :profile_missing else @user = User.new(u) end haml :user_profileend
app.rb
nding a us
get '/user/:email/profile' do u = USERS.find_one( "email" => params[:email].downcase) if u == nil return haml :profile_missing else @user = User.new(u) end haml :user_profileend
app.rb
Crng an INdex> db.users.ensureIndex({email : 1})
> db.users.getIndexes()[ { "v" : 1, "key" : { "_id" : 1 }, "ns" : "milieu.users", "name" : "_id_" }, { "v" : 1, "key" : { "email" : 1 }, "ns" : "milieu.users", "name" : "email_1" }]
A Vue
Showing a Vue
get '/venue/:_id' do object_id = ░░░░░░░░░░░░░░ @venue = ░░░░░░░░░░( { ░░░░ => object_id }) haml :venueend
app.rb
Showing a Vue
get '/venue/:_id' do object_id = BSON::ObjectId.from_string(params[:_id])
@venue = VENUES.find_one( { :_id => object_id }) haml :venueend
app.rb
Showing a Vue .row .col-md-4 %h2= @venue['name'].to_s %p =@venue['location']['address'].to_s %br= @venue['location']['city'].to_s + ' ' + @venue['location']['state'].to_s + ' ' + @venue['location']['postalCode'].to_s
.col-md-8 %image{:src => '' << gmap_url(@venue, {:height => 300, :width => 450}) }
views/venue.haml
A Vuelocalhost:9393/venue/{id}
Nrby VueS
Nrby Vues
get '/venue/:_id' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id }) @nearby_venues = ░░░░░.░░░░░( {░░░░░ =>{░░░░░=>[ ░░░░░,░░░░░]}} ).░░░░░(4).░░░░░(1) haml :venueend
app.rb
Nrby Vues
get '/venue/:_id' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id }) @nearby_venues = VENUES.find( { :'location.geo' => { ░░░░░ => [ ░░░░░,░░░░░] } }).░░░░░(4).░░░░░(1) haml :venueend
app.rb
Nrby Vues
get '/venue/:_id' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id }) @nearby_venues = VENUES.find( { :'location.geo' => { ░░░░░ => [ ░░░░░,░░░░░] } }).limit(4).skip(1) haml :venueend
app.rb
Nrby Vuesget '/venue/:_id' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id }) @nearby_venues = VENUES.find( { :'location.geo' => { :$near => [ @venue['location']['geo'][0], @venue['location']['geo'][1]] } }).limit(4).skip(1) haml :venueend
app.rb
... .row - @nearby_venues.each do |nearby| .col-md-3 %h2 %a{:href => '/venue/' + nearby['_id'].to_s}= nearby['name'].to_s %p =nearby['location']['address'].to_s %br= nearby['location']['city'].to_s + ' ' + nearby['location']['state'].to_s + ' ' + nearby['location']['postalCode'].to_s
%a{:href => '/venue/' + nearby['_id'].to_s} %image{:src => '' << gmap_url(nearby, {:height => 150, :width => 150, :zoom => 17}) }
views/venue.haml
sng Nrby Vues
Nrby Vueslocalhost:9393/venue/{id}
Checng IN
Checng in
get '/venue/:_id/checkin' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id })
user = USERS. ░░░░░_and_░░░░░(░░░░░) if ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ else
░░░░░ ░░░░░ ░░░░░ ░░░░░ end flash('Thanks for checking in') redirect '/venue/' + params[:_id]end
app.rb
Checng inget '/venue/:_id/checkin' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id }) user = USERS.find_and_modify(
:query => ░░░░░, :update => ░░░░░, :new => 1)
if ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ else
░░░░░ ░░░░░ ░░░░░ ░░░░░ end flash('Thanks for checking in') redirect '/venue/' + params[:_id]end
app.rb
Checng inget '/venue/:_id/checkin' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id }) user = USERS.find_and_modify( :query => { :_id => @suser._id}, :update => {:$inc =>{ "venues." << object_id.to_s => 1 } }, :new => 1)
if ░░░░░ ░░░░░ ░░░░░ ░░░░░ ░░░░░ else
░░░░░ ░░░░░ ░░░░░ ░░░░░ end flash('Thanks for checking in') redirect '/venue/' + params[:_id]end
app.rb
Checng inget '/venue/:_id/checkin' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id }) user = USERS.find_and_modify(:query => { :_id => @suser._id}, :update => {:$inc => { "venues." << object_id.to_s => 1 } }, :new => 1) if user['venues'][params[:_id]] == 1
VENUES.update(░░░░░) else
VENUES.update(░░░░░) end flash('Thanks for checking in') redirect '/venue/' + params[:_id]end
app.rb
Checng inget '/venue/:_id/checkin' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id }) user = USERS.find_and_modify(:query => { :_id => @suser._id}, :update => {:$inc => { "venues." << object_id.to_s => 1 } }, :new => 1) if user['venues'][params[:_id]] == 1 VENUES.update({ :_id => @venue['_id']}, { :$inc => { :'stats.usersCount' => 1, :'stats.checkinsCount' => 1}}) else VENUES.update({ _id: @venue['_id']}, { :$inc => { :'stats.checkinsCount' => 1}}) end flash('Thanks for checking in') redirect '/venue/' + params[:_id]end
app.rb
You’ve be he def user_times_at if logged_in? times = 'You have checked in here ' if [email protected]? && [email protected][params[:_id]].nil? times << @suser.venues[params[:_id]].to_s else times << '0' end times << ' times' else times = 'Please <a href=\'/login\'>login</a> to join them.' end end
helpers/milieu.rb
Checn In
%p %a.btn.btn-primary.btn-large{:href => '/venue/' + @venue['_id'].to_s + '/checkin'} Check In Here%p =@venue['stats']['usersCount'].ceil.to_s + ' users have checked in here ' + @venue['stats']['checkinsCount'].ceil.to_s + ' times'%p=user_times_at
views/venue.haml
A Vue localhost:9393/venue/{id}
What we’ve led* Model data for
MongoDB
* Use MongoDB tools to import data
* Create records from shell & ruby
* Update records
* Atomic updates
* Create an index
* Create a geo index
* Query for data by matching
* GeoQueries
* Pagination
* Single Document Transactions
* Some ruby, sinatra, haml, etc
Next ?
’s on Ghub
Some Ids* Create interface to add venues
* Connect to foursquare
* Login w/twitter
* Badges or Categories
* Enable searching of venues
* Tips / Reviews
E IF YOU KED !
Questions?
http://spf13.comhttp://github.com/spf13@spf13