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)