Skip to content

Instantly share code, notes, and snippets.

@alexbevi
Last active January 24, 2023 21:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save alexbevi/e25f0214c1d131db27043e38fc5069d3 to your computer and use it in GitHub Desktop.
Save alexbevi/e25f0214c1d131db27043e38fc5069d3 to your computer and use it in GitHub Desktop.
Ruby on Rails Global Summit 23: Ruby on Rails and MongoDB

Install Rails 7

# 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

Create Application Skeleton

rails new contacts -j esbuild --css bootstrap --skip-active-record --skip-test --skip-system-test
cd contacts
code .
  • we'll be skipping unit and system tests for the demo
  • we'll also be using esbuild for bundling JavaScript with Rails 7
  • since Rails 7 adds a css framework option to the application generator, we'll use bootstrap here (only because I haven't gotten around to learning tailwind yet)
  • once everything is setup we'll open our project in Visual Studio Code (make sure to increase font size so it's easier to read)

Update Gemfile and update gems and configure Mongoid

gem "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
  • now that config/mongoid.yml has been generated we can update it with our connection string from Atlas
  • (go over a couple of the options in the generated file, then replace it with the following)
development:
 # 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:ys8FsMJeQhhzKwnS@contacts.qgcjttv.mongodb.net/test?retryWrites=true&w=majority

Create our Contact scaffolding using default Rails generators

# generate scaffolding
rails g scaffold Contact
# start the rails server
rails s
  • this will generate basic MVC scaffolding for our contacts
  • we'll also start the rails server so we can start working with our data and application as we make changes
  • since we aren't using ActiveRecord we'll first need to update our contact model to identify the fields we'll be tracking
field :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>

Delete, Update and Validate

(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)


Add a Counter

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)


Generate Content

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:ys8FsMJeQhhzKwnS@contacts.qgcjttv.mongodb.net/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:ys8FsMJeQhhzKwnS@contacts.qgcjttv.mongodb.net/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 ...


Pagination with Kaminari

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.


What about filtering this data?

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))


Adding a new field to our model ... live

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 model
field :language, type: String
  • Contact Partial
  <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)
  • Contact _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)


CRUD isn't everything - let's generate a report!

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment