DEV Community

Cover image for Rails tricks you may not know
Pavel Tkachenko
Pavel Tkachenko

Posted on

Rails tricks you may not know

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'
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# Views structure
app/views/layouts/admin.html.erb
app/views/posts/
app/views/admin/posts/
app/views/admin/users/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)