build your first mongodb app in ruby @ strangeloop 2013

Post on 19-May-2015

7.271 Views

Category:

Technology

1 Downloads

Preview:

Click to see full reader

DESCRIPTION

A workshop given at StrangeLoop 2013 teaching you how to build your first MongoDB application in Ruby

TRANSCRIPT

Building your rst MongoDB Acaon

AgdaIntroduction to MongoDB

MongoDB Fundamentals

Running MongoDB

Schema Design

Ruby & Sinatra Crash Course

Building Our First App

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 ~@venues.each do |venue| %tr %td %a{:href => '/venue/' << venue['_id'].to_s}= venue['name'] %td= venue['location']['address'] ? venue['location']['address'] : '&nbsp' %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 ~@venues.each do |venue| %tr %td %a{:href => '/venue/' << venue['_id'].to_s}= venue['name'] %td= venue['location']['address'] ? venue['location']['address'] : '&nbsp' %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 !@suser.venues.nil? && !@suser.venues[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

top related