はじめに
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 Concern | Controller Concern | Model/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で呼び出す形が良さそうです