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.
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):
# 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.)
# 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 $
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:
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
# 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!”.
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
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:
<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:
Then, let’s revise the controller to load the template from the new location:
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:
Comments are closed.