The Model Layer (ActiveRecord)
Let’s imagine we’re a manufacturing company looking to automate the administration of the industrial equipment used in our warehouses. One of the strengths of Ruby on Rails when building an application is its ability to easily align the code implementation with our actual business terminology.
# app/models/warehouse.rb
class Warehouse < ApplicationRecord
has_many :equipment
end
# app/models/equipment.rb
class Equipment < ApplicationRecord
belongs_to :warehouse
end
Here, we’ve created two “models” to represent our business case. We have our warehouses, and the equipment installed in those warehouses. The above is already giving us quite a bit of functionality for free.
For instance, with the associations we’ve defined here, we can easily grab all of the equipment installed at a specific warehouse with:
warehouse.equipment
But it’s straightforward to add considerably more powerful interactions here. Let’s take a look at the database configuration for our Equipment model:
# db/migrate/20230101010000_create_equipment.rb
class CreateEquipment < ActiveRecord::Migration[7.0]
def change
create_table :equipment do |t|
t.string :name, null: false
t.references :warehouse, null: false, foreign_key: true
t.integer :status, default: 0
end
end
end
This migration file creates an “equipment” table in our database with a name field, a reference to a Warehouse record, and a status field. With Rails’s philosophy of convention over configuration, the Equipment model we defined earlier doesn’t need to have any of this information provided to it! Because the table name aligns with our model name, our application can automatically identify the fields we’ve added to the database and understand how to interact with them.
Let’s put that status field to use:
# app/models/equipment.rb
class Equipment < ApplicationRecord
belongs_to :warehouse
enum status: [:ordered, :delivered, :installed, :configured, :running]
end
Here, we’ve told the application that our status field should be treated as an enumeration over several different workflow states for our equipment. We might have different criteria for moving between these states, but this one line immediately grants us the ability to filter our Equipment records in multiple ways. Now if we want to pull a list of all the equipment at a warehouse that has been delivered but not installed, we can simply do this:
warehouse.equipment.delivered
This is a very barebones but hopefully illuminating peek into why ActiveRecord, the model layer of Ruby on Rails, is so powerful for quickly implementing web applications.
The Controller Layer (ActionController)
Let’s take this example another step, and see how ActionController, the Controller layer of Rails, fits into our application:
# app/controllers/equipment_controller.rb
class EquipmentController < ApplicationController
def create
@equipment = Equipment.new(resource_params)
if @equipment.save
redirect_to equipment_index_path, notice: "You successfully added new equipment!"
else
render :new
end
end
private
def resource_params
params.require(:equipment).permit(:name, :warehouse_id, :status)
end
end
There’s a bit more going on here, because we’re setting up a lot of new functionality. With the appropriate routes configured for our application, the “create” action we’ve defined here will accept HTTP POST requests to “www.manufacturing.co/equipment”, use the submitted information to construct an Equipment record, and persist it to the database so we can interact with it elsewhere in our code.
The resource_params method above is a baked-in security feature of Rails––before any information can be used to create or update a record, it has to be marked as safe / expected by the controller.
This mechanism is useful for ensuring that users can’t update internal fields that shouldn’t be modified; for instance, if we wanted our Equipment “status” to come from an external processing system, we could remove it from the supported params list here to prevent users from updating the status manually through our API.
The View Layer (ActionView)
We have our API in place now, but how might a non-engineer interacting with our application create a piece of Equipment themselves? This is where the View layer, powered by ActionView in a Rails application, comes in:
# app/views/equipment/new.html.erb
<%= form_for @equipment do |form| %>
<%= form.label :name %>
<%= form.text_field :name %>
<%= form.label :warehouse_id %>
<%= form.collection_select :warehouse_id, Warehouse.all, :id, :name, prompt: true %>
<%= form.label :status %>
<%= form.select :status, options_for_select(Equipment.statuses) %>
<%= form.submit %>
<% end %>
While we didn’t write any actual HTML here, ActionView takes this template and generates the HTML for us to render back to the browser. Here, we’re rendering a form that allows the user to populate all of the fields our Equipment model currently supports, and posts the information to the correct URL when the user submits the form.
We have two selects here – one to allow the user to select which Warehouse the equipment should be associated with, and another for the user to select the proper status description for the equipment. While we’re keeping this example simple, it’s easy to see how we could substitute in a different / smaller collection of warehouses (perhaps based on the current user’s permissions) as this interface evolves.
This very basic form comes with a lot of web accessibility baked in for free – the labels we’ve declared in this view are automatically associated with the relevant inputs, and will be populated with human-readable text that can be defined in localization files supported out of the box by Rails.
There’s so much more flexibility and power that Rails gives us to quickly build functional, RESTful applications, but hopefully these examples illustrate the framework’s focus on intuitiveness and convention over configuration. Rails’s key feature, and the reason we love it here at TXI, is that we can do so much work with very little boilerplate or repetition.