基本

Ruby on RailsのConcernsで共通処理をモジュール化する

Ruby on RailsのConcerns(コンサーン)は、複数のコントローラーやモデルで共有したい処理をモジュールとして切り出す仕組みです。ApplicationControllerにすべての共通処理を書くと肥大化してしまいますが、Concernsを使えば機能ごとにモジュールを分離し、必要なコントローラーにだけincludeできます。この記事では、Concernsの作成方法と活用パターンを解説します。

基本的な使い方

Concernsはapp/controllers/concerns/ディレクトリにモジュールとして定義します。ActiveSupport::Concernをextendすることで、includedブロック内にコールバックなどを記述できます。

app/controllers/concerns/authenticatable.rb
module Authenticatable
  extend ActiveSupport::Concern

  included do
    before_action :require_login
  end

  private

  def require_login
    unless current_user
      redirect_to login_path, alert: "ログインが必要です"
    end
  end

  def current_user
    @current_user ||= User.find_by(id: session[:user_id])
  end
end

このConcernを使いたいコントローラーでincludeするだけで、認証機能が有効になります。

app/controllers/posts_controller.rb
class PostsController < ApplicationController
  include Authenticatable

  def index
    @posts = Post.all
  end
end

ApplicationControllerとの違い

ApplicationControllerに定義した処理はすべてのコントローラーに適用されますが、Concernsはincludeしたコントローラーにだけ適用されます。

方法 適用範囲 使い分け
ApplicationController すべてのコントローラー 全画面で必要な処理(ロケール設定など)
Concerns includeしたコントローラーのみ 特定の機能を持つコントローラー群で共有する処理

実践的なConcernの例

ページネーション機能をConcernとして定義する例です。

app/controllers/concerns/paginatable.rb
module Paginatable
  extend ActiveSupport::Concern

  private

  def page_number
    (params[:page] || 1).to_i
  end

  def per_page
    (params[:per_page] || 20).to_i
  end

  def paginate(scope)
    scope.offset((page_number - 1) * per_page).limit(per_page)
  end
end
app/controllers/posts_controller.rb
class PostsController < ApplicationController
  include Paginatable

  def index
    @posts = paginate(Post.order(created_at: :desc))
  end
end

モデル用のConcerns

コントローラーだけでなく、モデルでもConcernsを使えます。app/models/concerns/に定義します。

app/models/concerns/soft_deletable.rb
module SoftDeletable
  extend ActiveSupport::Concern

  included do
    scope :active, -> { where(deleted_at: nil) }
    scope :deleted, -> { where.not(deleted_at: nil) }
  end

  def soft_delete
    update(deleted_at: Time.current)
  end

  def restore
    update(deleted_at: nil)
  end

  def deleted?
    deleted_at.present?
  end
end
app/models/post.rb
class Post < ApplicationRecord
  include SoftDeletable

  # Post.active で削除されていないレコードのみ取得
  # Post.deleted で削除済みレコードのみ取得
end

class_methodsの定義

Concernにクラスメソッドを定義する場合は、class_methodsブロックを使います。

app/models/concerns/searchable.rb
module Searchable
  extend ActiveSupport::Concern

  class_methods do
    def search(query)
      where("title LIKE ? OR content LIKE ?", "%#{query}%", "%#{query}%")
    end
  end
end
ポイント

Concernは機能単位で分割するのがベストプラクティスです。1つのConcernに複数の異なる責務を持たせると、結局肥大化してしまいます。「認証」「ページネーション」「論理削除」など、明確な単一機能ごとに分けましょう。

注意

Concernsの多用はコードの依存関係を複雑にする可能性があります。Concernが他のConcernに依存する場合や、includeの順序が重要になる場合は、設計を見直すことを検討してください。

まとめ

  • ConcernsはActiveSupport::Concernをextendしたモジュールとして定義する
  • includedブロック内にコールバックやスコープを記述できる
  • includeしたコントローラーやモデルにだけ機能が適用される
  • コントローラー用はapp/controllers/concerns/、モデル用はapp/models/concerns/に配置
  • class_methodsブロックでクラスメソッドを定義できる
  • 1つのConcernには単一の責務を持たせ、機能ごとに分割する