These are my notes from the Ruby on Rails and MongoDB presentation at Geekle's Ruby on Rails Global Summit 23.
A video of the talk is also available.
These are my notes from the Ruby on Rails and MongoDB presentation at Geekle's Ruby on Rails Global Summit 23.
A video of the talk is also available.
# make sure rails is setup
gem install rails
# output
Successfully installed rails-7.0.4
Parsing documentation for rails-7.0.4
Done installing documentation for rails after 0 seconds
1 gem installed
rails new contacts -j esbuild --css bootstrap --skip-active-record --skip-test --skip-system-test
cd contacts
code .
esbuild
for bundling JavaScript with Rails 7gem "mongoid"
gem "bootstrap_form"
# install Mongoid
bundle install
# list rails generators (point out Mongoid config generator)
rails g -h
# generate mongoid config
rails g mongoid:config
config/mongoid.yml
has been generated we can update it with our connection string from Atlasdevelopment:
# Configure available database clients. (required)
clients:
# Defines the default client. (required)
default:
# Mongoid can connect to a URI accepted by the driver:
uri: mongodb+srv://demo:[email protected]/test?retryWrites=true&w=majority
# generate scaffolding
rails g scaffold Contact
# start the rails server
rails s
contact
model to identify the fields we'll be trackingfield :first_name, type: String
field :last_name, type: String
field :email, type: String
field :birthday, type: Date
Next we'll update the _form.html.erb
partial
<%= bootstrap_form_for(@contact) do |form| %>
<% if contact.errors.any? %>
<div style="color: red">
<h2><%= pluralize(contact.errors.count, "error") %> prohibited this contact from being saved:</h2>
<ul>
<% contact.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<%= form.text_field :first_name %>
<%= form.text_field :last_name %>
<%= form.text_field :email %>
<%= form.date_field :birthday %>
<div>
<%= form.submit %>
</div>
<% end %>
Since we're using Bootstrap as the CSS framework we're also going to use the bootstrap_form
gem to allow us to more easily format our forms.
(TEST AND FAIL)
Rails introduced strong parameters in Rail 4.0. For those of you that have been working in the Rails ecosystem for a while you may remember this was a replacement for protected attributes (the whole attr_accessible/attr_protected thing in your models)
It looks like we need to permit our fields in the controller! Next let's update the ContactsController
with this information
params.fetch(:contact, {}).permit(:first_name, :last_name, :email, :birthday)
(TEST AND SUCCEED)
We're missing the boilerplate rendering partial for our contacts when we navigate to the show route. Let's plug that in quickly and refresh to see our data!
<div id="<%= dom_id contact %>">
<table class="table">
<tbody>
<tr>
<th style="width: 100px" scope="row">First Name</th>
<td><%= contact.first_name %></td>
</tr>
<tr>
<th style="width: 100px" scope="row">Last Name</th>
<td><%= contact.last_name %></td>
</tr>
<tr>
<th style="width: 100px" scope="row">Email</th>
<td><%= contact.email %></td>
</tr>
<tr>
<th style="width: 100px" scope="row">Birthday</th>
<td><%= contact.birthday %></td>
</tr>
</tbody>
</table>
</div>
(CREATE A COUPLE ENTRIES WITH EMPTY FIRST NAME OR LAST NAME VALUES)
Mongoid supports ActiveRecord validations within models. For example, if we want to add some basic validation. We can update our Contact
model with this information and it will work as expected.
validates :first_name, :last_name, presence: true
(TRY TO UPDATE AN EXISTING ENTRY WITH A BLANK FIRST/LAST NAME AND SHOW THE ERROR) (UPDATE RECORD SO THE EXAMPLE SAVES)
We haven't really added any custom functionality yet. So far everything has worked exactly as you'd expect a standard Rails tutorial to, so let's try adding a record counter to our index page.
First, modify the contacts_controller
index route to record the value:
@total = Contact.count
Next let's update the index.html.erb
view template to show the value
<h1>Contacts (<%= @total %>)</h1>
(DEMO AND CALL OUT THE TOTAL) (DELETE SOME DOCUMENTS TO SHOW IT WORKING)
Using mgeneratejs
, a tool that can be used to create documents based on a template let's generate a contact
mgeneratejs '{ "first_name": "$first", "last_name": "$last", "email": "$email", "birthday": "$date" }'
mgeneratejs '{ "first_name": "$first", "last_name": "$last", "email": "$email", "birthday": "$date" }' | mongoimport "mongodb+srv://demo:[email protected]/test" -c contacts
Cool, looks like that worked, so let's generate a couple thousand ...
mgeneratejs '{ "first_name": "$first", "last_name": "$last", "email": "$email", "birthday": "$date" }' | mongoimport -n 2000 "mongodb+srv://demo:[email protected]/test" -c contacts
(SHOW THIS IN THE APP)
Well that's not really great to work with anymore now is it. Maybe pagination might help ...
Like any other Rails application, we can add pagination with Kaminari. There is even a mongoid adapter ready for use. Let's add this to our Gemfile
along with any additional gems that will make this work seamlessly with our application
gem "kaminari-mongoid"
gem 'kaminari-actionview'
gem 'bootstrap5-kaminari-views'
# stop the rails server
bundle install
# start the rails server
rails s
To add pagination we'll just update the ContactsController
's index route again
@contacts = Contact.page(params[:page])
Updating the index.html.erb
view for the Contacts will also allow us to easily incorporate pagination
<br/>
<%= page_entries_info(@contacts) %>
<br/>
<%= paginate @contacts, theme: 'bootstrap-5',
pagination_class: "pagination-sm flex-wrap justify-content-center",
nav_class: "d-inline-block" %>
<hr />
If this all seems pretty familiar ... that's the point. Mongoid is providing a very similar CRUD API to what you've come to expect from ActiveRecord, which makes these types of operations seem obvious or second nature.
Let's try something a little different now.
Now that we have a whole bunch of data we may want to filter it as well. Let's do this by adding a search form to our contacts page by modifying the contacts index view
<%= form_tag contacts_path, method: :get do %>
<%= label_tag(:query, "Search Contacts: ") %>
<%= text_field_tag :query, params[:query] %>
<%= submit_tag("Search", name: nil) %>
<% end %>
(TRY SEARCHING TO SHOW NOTHING CHANGES)
We'll need to modify the controller to ensure we know what to do with the field we're trying to filter on
q = params[:query]
@contacts = if q.blank?
Contact.page(params[:page])
else
Contact.where(first_name: /#{q}/i).page(params[:page])
end
@total = Contact.count
(TEST IT OUT - TRY FILTERING BY LAST NAME TOO AND SHOW IT FAILING)
This will only filter by first name. To also search by last name we can build out the query using a DSL similar to what you would expect ActiveRecord to offer as well:
.or(last_name: /#{q}/i)
(APPEND THE ABOVE BEFORE THE .page(params))
We want to update our contacts model to include a new field called "Language". Doing this will require adding a single entry to the following:
Contact
modelfield :language, type: String
<tr>
<th style="width: 100px" scope="row">Language</th>
<td><%= contact.language %></td>
</tr>
contact_params
in the Controller# add :language
params.fetch(:contact, {}).permit(:first_name, :last_name, :email, :birthday, :language)
_form.html.erb
partial<%= form.text_field :language %>
That's it. You can start interacting with this field on all existing documets as well as creating new documents with this field. No migrations - No schema updates - No downtime
(DEMO - update existing documents and create a new document)
To finish off our contact management demo application let's add a report. I've always been a fan of charts, so let's build a report that shows off what the age distribution of our contacts is!
Let's kick this off by adding Chartkick to our gemfile then generating a controller for our reports:
gem 'chartkick'
We'll need to pin some dependencies in our config/importmap.rb
(create it if it doesn't exist)
pin "chartkick", to: "chartkick.js"
pin "Chart.bundle", to: "Chart.bundle.js"
We'll also add the necessary imports to our application's default javascript file (app/javascript/application.js
)
import "chartkick"
import "Chart.bundle"
And just to make sure everything works properly, we'll include the chartkick libraries from Google's CDN (application.html.erb
)
<%= javascript_include_tag "//www.google.com/jsapi", "chartkick" %>
And to ensure we precompile this asset we need to update our app/assets/config/manifests.js
file
//= link chartkick.js
# stop rails server
rails g controller reports index
Since we didn't generate any resources for our reports let's make sure we can route to the index by updating the routes.rb
get 'reports', to: "reports#index"
Finally let's install chartkick and restart the application
bundle install
# start rails server
rails s
(NAVIGATE TO REPORTS#INDEX)
Our ReportsController
will have an index route defined, but it won't do anything interesting
class ReportsController < ApplicationController
def index
# add the pipeline first (as is)
pipeline = [
{ :"$group" => {
_id: { :"$year" => "$birthday" },
total: { :"$sum" => 1 }
}},
{ :"$sort" => { _id: 1 } }
]
# next add the pipeline to_a (show this on the reports page as-is)
@report = Contact.collection.aggregate(pipeline).to_a
# Chartkick expects a specific format, so we map our results accordingly
@report = @report.each_with_object({}) { |d,h| h[d["_id"]] = d["total"] }
end
end
We'll also need to update the Report's view so it will render something!
(DON'T INCLUDE THE COMMENTED OUT PORTION AT FIRST)
<h1>Reports#index</h1>
<h2>Contacts by Birthyear<h2>
<tt><%= @report %></tt>
<%# <%= area_chart @report, ytitle: "Total Results", xtitle: "Birth Year" %>
As we can see we have a good number of users who have yet to be born. Let's hope that they also choose a career in software engineering and join us in building the applications of tomorrow using MongoDB and Ruby on Rails