In general, Rails doesn’t encourage developers to factor code into namespaces. Most apps end up with hundreds of files and very few directories in
app/models and similarly flat hierarchies in other
app directories. Rails Engines are a great way to pull your code into isolated, reusable modules, but there’s a significant amount of overhead associated with an Engine that might be too expensive for a small feature, or you might just have a few conceptually similar models that you want to “live near each other”.
Rails has support for namespacing in its MVC architecture, but there are a few things that might cause confusion if you haven’t used it before.
Pretend your app has a user-configurable dashboard. Users can add widgets — like charts and tables — to the dashboard, and they can lay the widgets out on the dashboard in rows and columns. You might model it like this:
Dashboard::Dashboard, which contains
Dashboard::Row, which contains
Dashboard::Widget, which has a position within its
I’ll use these models in the examples below.
Most of the gnarly bits of namespacing happen in the models, so I’ll start there.
First, you’ll need to create a module for your namespace and define the class method
table_name_prefix (see Migrations):
# app/models/dashboard.rb module Dashboard def self.table_name_prefix 'dashboard_' end end
Once you’re “inside” the namespace, you generally won’t refer to it when declaring associations. For example,
Widget will belong to
Row will belong to
# app/models/dashboard/widget.rb module Dashboard class Widget < ApplicationRecord belongs_to :row end end # app/models/dashboard/row.rb module Dashboard class Row < ApplicationRecord belongs_to :dashboard has_many :widgets end end # app/models/dashboard/dashboard.rb module Dashboard class Dashboard < ApplicationRecord has_many :rows end end
Outside the namespace, though, you’ll have to tell Rails the class name of any association that uses your namespaced model. Otherwise, it will guess incorrectly:
# app/models/user.rb class User < ApplicationRecord # WRONG => NameError: uninitialized constant User::DashboardDashboard has_many :dashboard_dashboards # RIGHT has_many :dashboard_dashboards, class_name: 'Dashboard::Dashboard' # ALSO RIGHT has_many :dashboards, class_name: 'Dashboard::Dashboard' end
Since most databases don’t support namespaces for tables, Rails expects you to prefix your table names with the “underscored” name of your namespace. You also won’t be able to pass
foreign_key: true to the
references helper method, so you’ll have to create constraints yourself.
To create the migration for
$ rails generate migration create_dashboard_widgets
Edit the generated file:
# db/migrate/YYYYMMDDHHMMSS_create_dashboard_widgets.rb class CreateDashboardWidgets < ActiveRecord::Migration create_table :dashboard_widgets do |t| # adding "foreign_key: true" here will cause an error like this: # PG::UndefinedTable: ERROR: relation "rows" does not exist t.references :row, index: true end add_foreign_key :dashboard_widgets, :dashboard_rows, column: :row_id end
Simply wrap your routes in a call to
# config/routes.rb namespace :dashboard do resources :dashboards # ... end
Your URLs will look like
/dashboard/dashboards/1, and path helpers will look like
Controllers are also straightforward:
# app/controllers/dashboard/dashboards_controller.rb module Dashboard class DashboardsController < ApplicationController def index @dashboards = current_user.dashboards end # ... end end
You may get a confusing error if you try to render a partial of a model from outside the namespace.
Say one of your widgets renders a list of recently active users:
# app/views/dashboard/widgets/_active_user_widget.html.erb <%# This can result in an error: Missing partial dashboard/users/_user %> <%= render @widget.active_users %>
Rails is attempting to render a different partial than you might expect since you’re inside a namespace. The idea is that you might want a different
_user partial in, say, an admin area than you do in the public area.
Since I’m generally using namespaces to isolate features rather than create distinct layouts, I turn this behavior off:
# config/application.rb config.action_view.prefix_partial_path_with_controller_namespace = false
Unfortunately, it’s a global setting, so you can’t change it for each call to
render. You can always explicitly render a template if you need the default behavior:
<%= render partial: 'admin/users/user', collection: @users %>
If you’re using FactoryGirl for testing, you’ll need to tell her the name of the factory to use for your namespaced associations:
# spec/factories/dashboard/widget.rb FactoryGirl.define do factory :dashboard_widget, class: Dashboard::Widget do association :row, factory: :dashboard_row end end
After many years of cramming everything directly into
app/models, I’ve had great luck with namespacing. It keeps related concepts together and provides a nice level of isolation without the overhead of full-fledged Engines or microservices.
I hope your experience is as pleasant as mine!