I see a lot of new Rails developers struggling with some of the basics of the framework. I've been using Rails for a while now, and I've picked up a few tricks along the way. I thought I'd share these tricks with you.
One action, many methods
Sometimes it is usefull to have one action, but make it work with both get
and post
(or any other). For example, if you have a page where user can generate a report. User requires to select some dates in range, and then click button to generate report. And what I often see is that people create two actions inside the controller:
# app/controllers/reports_controller.rb
class ReportsController < ApplicationController
def super_report
# render form
end
def super_report_post
# generate report
end
end
# config/routes.rb
get 'reports/super_report', to: 'reports#super_report'
post 'reports/super_report', to: 'reports#super_report_post'
But there is a better way to do this. Choose the style you like:
# config/routes.rb
# Option one
get 'reports/super_report', to: 'reports#super_report'
post 'reports/super_report', to: 'reports#super_report'
# Option two
match 'reports/super_report', to: 'reports#super_report', via: [:get, :post]
And in controller:
# app/controllers/reports_controller.rb
class ReportsController < ApplicationController
def super_report
if request.post?
# generate report
else
# render form
end
end
end
Now we don't split our logic into two actions, but we have one action that works with both get
and post
. It looks nicer, easier to read and maintain.
Namespacing
Namespacing is a way to group controllers and routes. It is usefull when you have many controllers and you want to group them. For example you have controllers: PostsController
, Admin::UsersController
and Admin::PostsController
. You want different views for admin and want authentication for it.
# config/routes.rb
resources :posts
namespace :admin do
resources :users
resources :posts
end
Now you can access Admin::UsersController
with url /admin/users
and Admin::PostsController
with url /admin/posts
. You should also use namespace inside the controller:
# app/controllers/admin/users_controller.rb
class Admin::BaseController < ApplicationController
layout "admin" # special view layout for admins
before_action :authenticate_admin!
end
class Admin::UsersController < Admin::BaseController
# ... Only admin can work with it
end
class Admin::PostsController < Admin::BaseController
# ... Only admin can work with it
end
# Views structure
app/views/layouts/admin.html.erb
app/views/posts/
app/views/admin/posts/
app/views/admin/users/
Scoped associations
Let's say we have two models: User
and Post
. And we want to get all posts for user. We can do this:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
end
user = User.first
user.posts
But what if we don't ever want to include posts for user which are published? Often I see people doing this:
class User < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
belongs_to :user
scope :published, -> { where(published: true) }
end
user = User.first
user.posts.published
And this is fine, but you can somehow forget to add published
scope to your query. And then you will get all posts, even unpublished. And this is not what you want. Some people use default_scope
for this, but it is not good idea. So what we can do? We can use scope
in association:
class User < ActiveRecord::Base
has_many :posts, -> { published }
has_many :not_published_posts, -> { not_published }
end
class Post < ActiveRecord::Base
belongs_to :user
scope :published, -> { where(published: true) }
scope :not_published, -> { where(published: false) }
end
user = User.first
user.posts # only published posts here
Migrations are not only for schema building
You can use migration to add some data to your database. For example you have model User
and you want to create admin with id -1 to database, so anywhere, in any environment you want to be sure, that such user exists. You can do this:
class AddAdmin < ActiveRecord::Migration
ADMIN = User.find_by(id: -1)
def up
User.create(name: 'Admin', id: -1, admin: true) unless ADMIN
end
def down
ADMIN.destroy if ADMIN
end
end
Wrapping up
I hope you will find these tricks useful. If you have any other cool examples, please share them in the comments.
P.S. Cover image is generated using DALL·E 2
Top comments (0)