Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Build your first MongoDB App in Ruby @ StrangeLoop 2013

10,552 views

Published on

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

Published in: Technology

Build your first MongoDB App in Ruby @ StrangeLoop 2013

  1. 1. Building your rst MongoDB Acaon
  2. 2. Agda Introduction to MongoDB MongoDB Fundamentals Running MongoDB Schema Design Ruby & Sinatra Crash Course Building Our First App
  3. 3. @spf13Steve Francia AKA Chief Developer Advocate @ responsible for drivers, integrations, web & docs
  4. 4. Iroducon to mongodb
  5. 5. What Is MongoDB?
  6. 6. * Document * Open source * High performance * Horizontally scalable * Full featured MongoDB is a ___________ database
  7. 7. * 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 Documt Database
  8. 8. * MongoDB is an open source project * On GitHub * Licensed under the AGPL * Commercial licenses available * Contributions welcome Op Source
  9. 9. * 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 Pfoance
  10. 10. Hozoal Scable
  11. 11. * Ad Hoc queries * Real time aggregation * Rich query capabilities * Traditionally consistent * Geospatial features * Support for most programming languages * Flexible schema Full Ftured
  12. 12. Depth of Functionality Scalability&Performance Memcached MongoDB RDBMS Database ndscape
  13. 13. tp://w.mongodb.org/downloads
  14. 14. What is a Record?
  15. 15. K → Value * One-dimensional storage * Single value is a blob * Query on key only * No schema * Value cannot be updated, only replaced Key Blob
  16. 16. Raonal * 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
  17. 17. Documt * 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
  18. 18. Running Mongodb
  19. 19. MongoD
  20. 20. Mongo Shl
  21. 21. user = { username: 'fred.jones', first_name: 'fred', last_name: 'jones', } Sta wh an object (or ay, hash, dict, c)
  22. 22. > db.users.insert(user) Inst e record No collection creation needed
  23. 23. > db.users.findOne() { "_id" : ObjectId("50804d0bd94ccab2da652599"), "username" : "fred.jones", "first_name" : "fred", "last_name" : "jones" } Quying for e us
  24. 24. * _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
  25. 25. * ObjectId is a special 12 byte value * Guaranteed to be unique across your cluster * ObjectId("50804d0bd94ccab2da652599") |-------------||---------||-----||----------| ts mac pid inc ObjectId
  26. 26. Scha Design
  27. 27. Tdaonal scha design Focuses on data stoge
  28. 28. Documt scha design Focuses on use
  29. 29. 4 Building blocks of Documt Design
  30. 30. 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
  31. 31. Ays * 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
  32. 32. beed Documts * An acceptable value is a document * Nested documents provide structure * Query any field at any level * Can be indexed
  33. 33. * Object in your model * Associations with other entities  Aßociaon 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
  34. 34. Excise 1: Mod a busineß card
  35. 35. Busineß Card
  36. 36. Contacts { “_id”: 2, “name”: “Steven Jobs”, “title”: “VP, New Product Development”, “company”: “Apple Computer”, “phone”: “408-996-1010”, “address_id”: 1 } Rcing Addresses { “_id”: 1, “street”: “10260 Bandley Dr”, “city”: “Cupertino”, “state”: “CA”, “zip_code”: ”95014”, “country”: “USA” }
  37. 37. 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” } being
  38. 38. Excise 2: Store a busineß card
  39. 39. Contacts db.contacts.insert( { “_id”: 2, “name”: “Steven Jobs”, “title”: “VP, New Product Development”, “company”: “Apple Computer”, “phone”: “408-996-1010”, “address_id”: 1 } ) Insng wh Rce Addresses db.addresses.insert( { “_id”: 1, “street”: “10260 Bandley Dr”, “city”: “Cupertino”, “state”: “CA”, “zip_code”: ”95014”, “country”: “USA” } )
  40. 40. Excise 3: Re a busineß card
  41. 41. Contacts c = db.contacts.findOne( { “name”: “Steven Jobs”, } ) Quying wh Rce Addresses db.addresses.findOne( { “_id”: c.address_id, “street”: “10260 Bandley Dr”, “city”: “Cupertino”, “state”: “CA”, “zip_code”: ”95014”, “country”: “USA” } )
  42. 42. Building a MongoDB Acaon
  43. 43. MongoDB has nave bindings for nr allnguages
  44. 44. 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 dvs
  45. 45. 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
  46. 46. Ruby Csh Course
  47. 47. hing is an object 1.class 'a'.class :z.class class Foo end Foo.class Foo.new.class # => Fixnum # => String # => Symbol # => Class # => Foo
  48. 48. Structure Method Class Invocation def do_stuff(thing) thing.do_the_stuff end class TheThing def do_the_stuff puts "Stuff was done!" end end do_stuff(TheThing.new)
  49. 49. Stngs name = 'World' # => "World" "Hello, #{name}" # => "Hello, World" 'Hello, #{name}' # => "Hello, #{name}"
  50. 50. Numbs 1 + 1 # => 2 1 + 1.1 # => 2.1 6 * 7 # => 42 6 ** 7 # => 279936 Math.sqrt(65536) # => 256.0 1.class # => Fixnum (2 ** 42).class # => Fixnum (2 ** 64).class # => Bignum 1.1.class # => Float
  51. 51. Ays Array.new Array.new(3) [] a = [1,2,3] a[0] = 'one' a a[-1] a[1..2] # => [] # => [nil, nil, nil] # => [] # => [1, 2, 3] # => "one" # => ["one", 2, 3] # => 3 # => [2, 3]
  52. 52. Hashes Hash.new {} h = {1 => "one", 2 => "two"} h[1] h["1"] h[:one] = "einz" h[:one] h.keys h.values # => {} # => {} # => "one" # => nil # => "einz" # => "einz" # => [1, 2, :one] # => ["one", "two", "einz"]
  53. 53. Vaables & Names CamelCased # Classes, modules with_underscores # methods, local variables @instance_variable @@class_variable $GLOBAL_VARIABLE
  54. 54. Corol Structuresif condition # ... elsif other_condition # ... end unless condition # ... end while # ... end
  55. 55. Sinat is... not Rails not a framework a DSL for quickly creating web applications in Ruby with minimal effort
  56. 56. Hlo World # myapp.rb require 'sinatra' get '/' do 'Hello world!' end
  57. 57. TP Acons In 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
  58. 58. Roes Routes 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
  59. 59. Spt Route 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
  60. 60. Building our A in Ruby
  61. 61. Iroducing e mieu a
  62. 62. pre-popung our database
  63. 63. Download & Impo e vues 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
  64. 64. Mongo Shl
  65. 65. L’s lk at e vues > use milieu switched to db milieu > db.venues.count() 50 Database
  66. 66. L’s lk at e vues > 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 } }
  67. 67. Crng 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_" } ]
  68. 68. Skon
  69. 69. Sta wh a skon /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
  70. 70. Download & Install deps mkdir milieu cd milieu wget http://c.spf13.com/dl/GettingStarted.tgz tar zxvf GettingStarted.tgz bundle install Resolving 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.
  71. 71. 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
  72. 72. Op Brows to locaost:9393
  73. 73. Pri --- ror Scre
  74. 74. sng Vues
  75. 75. Connecng to MongoDB require 'mongo' require './model/mongoModule' require './model/user' # Connection code goes here CONNECTION = Mongo::Connection.new("localhost") DB = CONNECTION.db('milieu') # Alias to collections goes here USERS = DB['users'] VENUES = DB['venues'] CHECKINS = DB['checkins'] model/mongodb.rb
  76. 76. sng Vues get '/venues' do # Code to list all venues goes here @venues = VENUES.░░░░░ haml :venues end app.rb
  77. 77. sng Vues get '/venues' do # Code to list all venues goes here @venues = VENUES.find haml :venues end app.rb
  78. 78. sng Vues .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
  79. 79. sng Vueslocalhost:9393/venues
  80. 80. Paginang Vues get '/venues/?:page?' do @page = params.fetch('page', 1).to_i pp = 10 @venues = VENUES.find.░░░░░(░░░░).░░░░(░░░) @total_pages = (VENUES.░░░░░.to_i / pp).ceil haml :venues end app.rb # replaces the prior entry
  81. 81. Paginang Vues 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 :venues end app.rb # replaces the prior entry
  82. 82. .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') sng Vues views/venues.haml
  83. 83. paging rough Vueslocalhost:9393/venues
  84. 84. Crng Uss
  85. 85. Crng Uss Users 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
  86. 86. Crng Uss class 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) end end model/user.rb Inherited from MongoModule.rb
  87. 87. Crng Uss post '/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' end end app.rb
  88. 88. Loing in pa 1 configure do enable :sessions end before do unless session[:user] == nil @suser = session[:user] end end get '/user/:email/dashboard' do haml :user_dashboard end app.rb
  89. 89. Loing 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' end end app.rb
  90. 90. Loing 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
  91. 91. 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
  92. 92. Dashboard localhost:9393/dashboard
  93. 93. Viing Uss
  94. 94. 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_profile end app.rb
  95. 95. 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_profile end app.rb
  96. 96. Crng 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" } ]
  97. 97. A Vue
  98. 98. Showing a Vue get '/venue/:_id' do object_id = ░░░░░░░░░░░░░░ @venue = ░░░░░░░░░░( { ░░░░ => object_id }) haml :venue end app.rb
  99. 99. Showing a Vue get '/venue/:_id' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one( { :_id => object_id }) haml :venue end app.rb
  100. 100. Showing a Vue .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
  101. 101. A Vue localhost:9393/venue/{id}
  102. 102. Nrby VueS
  103. 103. Nrby Vues get '/venue/:_id' do object_id = BSON::ObjectId.from_string(params[:_id]) @venue = VENUES.find_one({ :_id => object_id }) @nearby_venues = ░░░░░.░░░░░( {░░░░░ =>{░░░░░=>[ ░░░░░,░░░░░]}} ).░░░░░(4).░░░░░(1) haml :venue end app.rb
  104. 104. Nrby Vues 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 :venue end app.rb
  105. 105. Nrby Vues 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 :venue end app.rb
  106. 106. Nrby Vues 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' => { :$near => [ @venue['location']['geo'][0], @venue['location']['geo'][1]] } }).limit(4).skip(1) haml :venue end app.rb
  107. 107. ... .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 sng Nrby Vues
  108. 108. Nrby Vues localhost:9393/venue/{id}
  109. 109. Checng IN
  110. 110. Checng 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
  111. 111. Checng in get '/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
  112. 112. Checng in get '/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
  113. 113. Checng in get '/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
  114. 114. Checng in get '/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
  115. 115. You’ve be he 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
  116. 116. Checn 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
  117. 117. A Vue localhost:9393/venue/{id}
  118. 118. What we’ve led * 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
  119. 119. Next ?
  120. 120. ’s on Ghub
  121. 121. Some Ids* Create interface to add venues * Connect to foursquare * Login w/twitter * Badges or Categories * Enable searching of venues * Tips / Reviews
  122. 122. E IF YOU KED ! Questions? http://spf13.com http://github.com/spf13 @spf13

×