RailsのConcerns 入門

はじめに

Rails 4から追加されたConcernは、共通ロジックの整理・再利用を目的とした便利な仕組みです。
ChatGPTやネットの色々なページを見つつまとめてみました。 

目次

Concernとは

 公式で触れられてるページ
出典:Railsガイド –
16.4 共通コードをconcernに抽出する

 RailsのConcernは、モジュールを綺麗にまとめて、特定の機能やロジックを複数のクラスで共通利用したいときに使う仕組みです。

大きく分けて

  • controllerのconcern
  • modelのconcern

の2種類があり、両方とも共通処理をmoduleにまとめて作って、includeして利用するイメージです。

controllerのconcern

全controllerで共通ではないが、複数のControllerでまとめて同じ処理を実行したい時に使う

たとえば

  • 認証状態をチェックしたい
  • とある条件を満たしたデータであるかをチェックしたい
  • 入力値のサニタイズ
  • とあるエラーが発生したらログアウトさせたい

など 

実装例

例1 認証チェック

concern

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

  included do
    before_action :authenticate_user!
  end

  private

  def authenticate_user!
    redirect_to login_path unless current_user
  end
end

Controller

class LoginedHomeController < BaseController
  include Authenticatable

  def index

 

例2 paramsの各値に0バイト文字(u0000等)含まれてたら除去する

(ストロングパラメータ前に実施する想定)

concern

# app/controllers/concerns/sanitizable.rb
module Sanitizable
  extend ActiveSupport::Concern

  included do
    before_action :sanitize_params
  end

  private

  def sanitize_params
    sanitize_hash!(params)
  end

  def sanitize_hash!(hash)
    hash.each do |key, value|
      case value
      when String
        hash[key] = value.delete("\u0000")
      when ActionController::Parameters
        sanitize_hash!(value)
      when Array
        value.map! do |v|
          if v.is_a?(String)
            v.delete("\u0000")
          elsif v.is_a?(ActionController::Parameters) || v.is_a?(Hash)
            sanitize_hash!(v)
            v
          else
            v
          end
        end
      end
    end
  end
end

 controller

class ApplicationController < ActionController::Base
  include Sanitizable
end

⚠️Controller Concern利用の鉄則

  • 1Concern 1責務
    • 複数の責務(認証して権限チェックしてSELECTして、、を全部やらせる)のはNG
  • Controller特化のものならそのControllerの中に書く
    • Concernにする必要があるものだけやる
  • ビジネスロジックは持たせない
    • ModelやServiceに持たせよう
  • 副作用のあるbefore_actionは慎重に
    • メソッドだけ提供して各Controllerでbefore_actionにセットする方が良い時も
  • 小さく保って、可読性と見通し優先
    • 巨大化させない

modelのconcern

ActiveRecordの機能拡張、共通ロジックの再利用に使う

 たとえば

  • ステータス管理
  • 削除フラグセット
  • 特定カラムへのUUID自動採番
  • バリデーションの共通化
  • スコープの共通化

など

実装例

例1 「特定カラム」へのUUIDを自動採番する

「特定カラム」はmodelごとに指定可能なパターン

concern

module UuidAssignable
  extend ActiveSupport::Concern

  included do
    class_attribute :uuid_assignable_column
    before_create :assign_uuid
  end

  class_methods do
    def uuid_assignable_to(column_name)
      self.uuid_assignable_column = column_name
    end
  end

  private

  def assign_uuid
    if self.class.uuid_assignable_column.present?
      self[self.class.uuid_assignable_column] ||= SecureRandom.uuid
    end
  end
end

model側

# Userモデル
class User < ApplicationRecord
  include UuidAssignable

  uuid_assignable_to :user_uuid
end

# Postモデル
class Post < ApplicationRecord
  include UuidAssignable

  uuid_assignable_to :post_uuid
end

例2 論理削除のdefault_scope

日時を持つdeleted_at を考慮したdefault_scopeを利用する

concern

module SoftDeletable
  extend ActiveSupport::Concern

  included do
    default_scope { where(deleted_at: nil) }
  end

  # ソフトデリートするメソッド
  def soft_delete
    update(deleted_at: Time.current)
  end

  # 削除済みかどうか判定
  def deleted?
    deleted_at.present?
  end

  module ClassMethods
    # 削除済みのレコードも含めて取得したい場合用
    def with_deleted
      unscope(where: :deleted_at)
    end

    # 削除済みのみ
    def only_deleted
      unscoped.where.not(deleted_at: nil)
    end
  end
end

 model

class User < ApplicationRecord
  include SoftDeletable
end

利用例

User.all               # => deleted_atがnilのレコードのみ
User.with_deleted      # => 全件取得
User.only_deleted      # => 削除済みのみ取得

user = User.find(1)
user.soft_delete       # => deleted_atに現在時刻セット
user.deleted?          # => true

⚠️Model Concern利用の鉄則

  • 1Concern 1責務
    • UUID付与・論理削除・ステータス管理を1つにまとめない
  • モデル固有のロジックは入れない
    • 他のモデルで使えないならConcern化せず、モデル内に書く
  • ビジネスロジックは持たせない
    • ServiceオブジェクトかModelメソッドに寄せるのが基本
  • default_scopeの乱用注意
    • 必ず解除方法(with_deletedとかunscoped)もセットで用意する
  • 小さく保つ。可読性と見通し優先
    • Fat Concernはすぐ技術的負債化する

ControllerとModel、どっちのconcernにするか迷ったら

✅ 対象の責務・役割を考える

判断ポイントModel ConcernController ConcernModel/Service層
データの状態・振る舞い に関するもの
リクエストの処理フロー に関するもの
複数のモデルで共通化したいActiveRecordの機能
複数のControllerで使いまわしたいbefore_actionや処理
ビジネスロジックやサービス呼び出し

タイミングで考える

  • DBレコードの作成・更新・削除前後の処理なら → Model Concern
    • 例: before_createでUUID付与
    • 例: 論理削除のフラグ付与
  • HTTPリクエストを受けたタイミングで行うべき処理なら → Controller Concern
    • 例: 認証・権限チェック
    • 例: リクエストパラメータの加工やサニタイズ

✅ ドメイン的に考える

  • ドメインオブジェクト(データ)に紐づく振る舞い → Model Concern
  • アプリケーションの操作フローに関わるもの → Controller Concern

✅ 迷ったら → Concernにしない選択肢も考える

  • そのController/Modelだけでしか使わないなら、まずはそこで実装
  • あとで共通化の必要が出たときにConcern化する方が安全

まとめ

Concern化するなら理由
UUIDを採番するModel Concernデータ生成時の状態管理
削除フラグ(論理削除)Model Concernデータの状態管理
認証・権限チェックController Concernリクエストフローの制御
パラメータの洗浄Controller Concernリクエストパラメータの前処理
外部API呼び出しの共通化Serviceクラス or Controller Concernビジネスロジック/フロー依存なので要注意

番外編:ルーティングのconcern機能

出典:Railsガイド – 2.8 ルーティングの「concern」機能

ルーティングでもconcernの記述を利用できるそうです

その他Concernで調べた時に出てきたページなど

出典:willnetの日記 – 2019-12-02 我々はConcernsとどう向き合うか

アンチパターンなどの記載があります

hookについては、メソッド提供だけにしておいてModelで呼び出す形が良さそうです

役に立ったらシェアしていただけると嬉しいです
  • URLをコピーしました!
  • URLをコピーしました!
目次