What’s that you say? We have it backwards? Ruby on Rails is supposed to favor “convention over configuration”? Well, we’re going to break that rule today…
When I was first learning Rails years ago, I thought that the model, view, and controller were inseparable. If I was creating an ArticlesController
, then no matter what, it had to have a show
method that used Article.find
to store a model in @article
, to be rendered in the articles/show.html.erb
template. When the code did something unusual (like the ArticlesController
loading in a Comment
model) I got confused.
Rails learners, I want to save you from that trap. So in this post, we’re going to throw aside scaffolds and other conventions. We’re going to configure a totally unrelated model, view, and controller to work with each other. When we’re done, you’ll see they’re not as tightly coupled as you may have thought.
Model
From your terminal, create a new Rails app, named whatever you want.
$ rails new anapp
Open up the app directory in your favorite editor.
Next, create a new file in the app/models/
subdirectory, named a_model.rb
. You don’t have to use rails generate model ...
or anything like that; creating the file manually will work just fine. In that file, type the following code (you can exclude the explanatory comments, if you like):
app/models/a_model.rb
# Any subclass of ApplicationRecord is considered a model.
class AModel < ApplicationRecord
# By default Rails looks at the class name and determines
# the database table name based on that. "AModel" would
# default to a table name of "a_models", so instead the
# below line overrides that with something more sensible.
self.table_name = "a_table"
end
Now we need a migration to create a_table
in our database. If you had used rails generate model ...
to create AModel
, it would have created a migration along with it. But we can create our own pretty easily. Create another file within the db/migrate/
subdirectory (you might have to create the migrate/
directory first), named 20170101000000_a_migration.rb
. (We have to put that 20170101000000
date at the start of the file name so that it’s recognized as a migration, but it doesn’t matter what date we use.)
db/migrate/20170101000000_a_migration.rb
# Again, any subclass of ActiveRecord::Migration[5.0] is
# considered a migration.
class AMigration < ActiveRecord::Migration[5.0]
def change
# This will create a database table named "a_table".
create_table :a_table do |t|
# This creates a "string" column named "an_attribute".
t.string :an_attribute
end
end
end
Save that, and run the migration from your terminal with:
$ bin/rails db:migrate
Rails sets up attribute reader and writer methods on the model class for every column in the database table. Because you specified in AModel
that it should use the a_table
table, and a_table
has an an_attribute
column, you can now assign an_attribute
on any AModel
instance from your Ruby code. We can save a new AModel
object from the Rails console:
$ bin/rails console
Loading development environment (Rails 5.0.1)
2.3.0 :001 > model = AModel.new
=> #<AModel id: nil, an_attribute: nil>
2.3.0 :002 > model.an_attribute = "A value!"
=> "A value!"
2.3.0 :003 > model.save
(0.1ms) begin transaction
SQL (0.3ms) INSERT INTO "a_table" ("an_attribute") VALUES (?)
[["an_attribute", "A value!"]]
(1.1ms) commit transaction
=> true
2.3.0 :004 > exit
$
Controller
Now let’s see if we can view that AModel
object from a web browser. First, we’re going to need to direct requests from a particular URL to a particular method on a controller class. We can do that by adding a route. Edit the existing routes file to look like this:
config/routes.rb
Rails.application.routes.draw do
# When you access "http://localhost:3000/apath" in your
# browser, the "anaction" method on the "AController"
# class will be called. (Rails sees "a", capitalizes it,
# and adds "Controller" onto the end.)
get "/apath", to: "a#anaction"
end
Now we need to create the AController
class, and give it an anaction
method:
app/controllers/a_controller.rb
# Any subclass of ApplicationController is treated as a
# controller.
class AController < ApplicationController
def anaction
# Load the object we created in the console. We can
# store it in any variable we want.
@a_variable = AModel.first
# If you add a "text:" argument, "render" will just add
# the given string to the response instead of loading a
# template.
render text: @a_variable.an_attribute
end
end
At this point, you can run bin/rails server
from your terminal, and visit http://localhost:3000/apath
in your browser. You should see the text “A value!”.
View
Now that we know our controller’s working, we can set it up with a proper view. The render text:
line causes Rails to render the given text instead of a view, so remove that line from app/controllers/a_controller.rb
, or the next step won’t work.
By default, Rails uses the name of the current controller and action method to determine where to look for a view template. Since we’re invoking AController
‘s anaction
method, it will look in the app/views/a/
directory for a file named anaction.html.erb
. So, let’s create an ERB template file there:
app/views/a/anaction.html.erb
<h1><%= @a_variable.an_attribute %></h1>
Refreshing the browser should show you “A value!” as a level 1 heading.
But this blog post isn’t about using the defaults, it’s about using configuration to override them. So let’s move the template file to a new subfolder, and give it a new file name: app/views/a_subfolder/a_template.html.erb
.
Then, let’s revise the controller to load the template from the new location:
app/controllers/a_controller.rb
class AController < ApplicationController
def anaction
@a_variable = AModel.first
# "app/views/" and ".html.erb" are added by default.
render "a_subfolder/a_template"
end
end
Refresh your browser, and Rails should load the template from your custom path instead.
Don’t Try this in Production, Kids
Is reconfiguring Rails so you can use names like AModel and AController best practice? Definitely not. But if you someday have a model or a template that you want to share between two completely different controllers, you can do that; Rails will let you. When it makes sense to do so, you can step outside the Rails conventions, as long as you know how to tweak the configuration.
If you liked this post, you’ll love these Rails courses and workshops on Treehouse:
Start learning to code today with a free trial on Treehouse.
Wow, some great points and very thorough! Convention over Configuration is more of a problem with estimates and prediction than scaling. Sometimes it just seems like everything is easy and all of a sudden, you will spend a lot of time on something that you thought was a 5 minute work. But all of this is actually a learning problem. Once you get proficient enough with Rails, the would be no danger anymore.
Nice write up. Just the abstract examples “AModel” are a bit weird to read through 😉
But in general i agree. Rails just says it favors convention over configuration. You can still configure a ton of stuff – and whatever has no real configuration can always be adjusted with the sledgehammer (“monkey patching” style). It just hides most of it under to hood 🙂
I used Rails a couple of times in combination with legacy projects. Usually hooking onto the old database for read access to get data from it.
It’s actually pretty easy with rails to adjust table name, attributes, and even building relationships with tables that are totally not in the rails scheme.
And at the end working with the legacy database is easier in rails than it was in whatever application it was build for.
The separation between controller and model is also a lot more common today. Sure usually you have a UsersController to your User Model. But especially for more than just little scaffold applications this usually changes and you start having different controllers using the same model or even put in another layer, for example with form objects.
This is really great man! I really enjoyed learning that you can tweak it in those ways. I got really excited about rendering a specific view with an unconventional name and path.