rails-concern について
Rails 開発者向けに、テスト駆動で ActiveSupport::Concern を作成する手順を示します。適切な用途、実装手順、よく使われるパターンを扱います。
主な機能
- 01実装前にスペックを作成
- 02モデル・コントローラー向けの指針
- 03一般的な Concern パターンの例
- 04抽象化を避ける場面の説明
- 05単一責任とスペックの確認
活用シーン
- 01複数モデルでバリデーションを共有
- 02共通スコープやコールバックを再利用
- 03コントローラー間で認証ヘルパーを共有
- 04テスト先行で Concern を作成
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ thibautbaissac/ rails_ai_agents --skill rails-concernインストール後、Claude Code で/rails-concernを入力して読み込まれたか確認します。
詳細
Rails Concern ジェネレーター(TDD)
共有動作のための concern(ActiveSupport::Concern モジュール)を、まず spec を作成してから生成します。
クイックスタート
- concern の動作をテストする失敗する spec を作成します
- spec を実行して RED を確認します
app/models/concerns/またはapp/controllers/concerns/に concern を実装します- spec を実行して GREEN を確認します
Concern を使用する場合
適したユースケース:
- 複数のモデルで共有するバリデーション
- 複数のモデルで使用する共通スコープ
- 共有コールバック(例:UUID の生成)
- コントローラーの認証/認可ヘルパー
- ページネーションまたはフィルタリングのロジック
次の場合は concern を避けてください:
- ロジックが 1 か所でしか使用されない(YAGNI)
- 関係のないメソッドを含む「神」concern を作成する
- ロジックをサービスオブジェクトにするべき
TDD ワークフロー
ステップ 1:Concern の spec を作成する(RED)
モデル concern の場合は、それを include するモデルを通じてテストします。
# spec/models/concerns/[concern_name]_spec.rb
RSpec.describe [ConcernName] do
# Create a test class that includes the concern
let(:test_class) do
Class.new(ApplicationRecord) do
self.table_name = "events" # Use existing table
include [ConcernName]
end
end
let(:instance) { test_class.new }
describe "included behavior" do
it "adds the expected methods" do
expect(instance).to respond_to(:method_from_concern)
end
end
describe "#method_from_concern" do
it "behaves as expected" do
expect(instance.method_from_concern).to eq(expected_value)
end
end
describe "class methods" do
it "adds scope" do
expect(test_class).to respond_to(:scope_name)
end
end
end
別の方法として、concern を使用する実際のモデルを通じてテストします。
# spec/models/event_spec.rb
RSpec.describe Event, type: :model do
describe "[ConcernName] behavior" do
describe "#method_from_concern" do
let(:event) { build(:event) }
it "does something" do
expect(event.method_from_concern).to eq(expected)
end
end
end
end
コントローラー concern の場合は、リクエスト spec を通じてテストします。
# spec/requests/[feature]_spec.rb
RSpec.describe "[Feature]", type: :request do
describe "pagination (from Paginatable concern)" do
let(:user) { create(:user) }
before { sign_in user }
it "paginates results" do
create_list(:resource, 30, account: user.account)
get resources_path
expect(response.body).to include("page")
end
end
end
ステップ 2:spec を実行する(RED を確認)
bundle exec rspec spec/models/concerns/[concern_name]_spec.rb
# OR
bundle exec rspec spec/models/[model]_spec.rb
ステップ 3:Concern を実装する(GREEN)
モデル concern:
# app/models/concerns/[concern_name].rb
module [ConcernName]
extend ActiveSupport::Concern
included do
# Callbacks
before_validation :generate_uuid, on: :create
# Validations
validates :uuid, presence: true, uniqueness: true
# Scopes
scope :with_uuid, ->(uuid) { where(uuid: uuid) }
scope :recent, -> { order(created_at: :desc) }
end
# Class methods
class_methods do
def find_by_uuid!(uuid)
find_by!(uuid: uuid)
end
end
# Instance methods
def generate_uuid
self.uuid ||= SecureRandom.uuid
end
def short_uuid
uuid&.split("-")&.first
end
end
コントローラー concern:
# app/controllers/concerns/[concern_name].rb
module [ConcernName]
extend ActiveSupport::Concern
included do
before_action :set_locale
helper_method :current_locale
end
class_methods do
def skip_locale_for(*actions)
skip_before_action :set_locale, only: actions
end
end
private
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
def current_locale
I18n.locale
end
end
ステップ 4:spec を実行する(GREEN を確認)
bundle exec rspec spec/models/concerns/[concern_name]_spec.rb
よくある Concern パターン
パターン 1:UUID の生成
# app/models/concerns/has_uuid.rb
module HasUuid
extend ActiveSupport::Concern
included do
before_validation :generate_uuid, on: :create
validates :uuid, presence: true, uniqueness: true
end
private
def generate_uuid
self.uuid ||= SecureRandom.uuid
end
end
パターン 2:ソフトデリート
# 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) }
default_scope { active }
end
def soft_delete
update(deleted_at: Time.current)
end
def restore
update(deleted_at: nil)
end
def deleted?
deleted_at.present?
end
end
パターン 3:検索可能
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
class_methods do
def search(query)
return all if query.blank?
where("name ILIKE :q OR email ILIKE :q", q: "%#{query}%")
end
end
end
パターン 4:監査可能
# app/models/concerns/auditable.rb
module Auditable
extend ActiveSupport::Concern
included do
has_many :audit_logs, as: :auditable, dependent: :destroy
after_create :log_creation
after_update :log_update
end
private
def log_creation
audit_logs.create(action: "created", changes: attributes)
end
def log_update
return unless saved_changes.any?
audit_logs.create(action: "updated", changes: saved_changes)
end
end
パターン 5:コントローラーのフィルタリング可能
# app/controllers/concerns/filterable.rb
module Filterable
extend ActiveSupport::Concern
private
def apply_filters(scope, allowed_filters)
allowed_filters.each do |filter|
if params[filter].present?
scope = scope.where(filter => params[filter])
end
end
scope
end
end
使用方法
モデルの場合:
class Event < ApplicationRecord
include HasUuid
include SoftDeletable
include Searchable
end
コントローラーの場合:
class ApplicationController < ActionController::Base
include Filterable
end
チェックリスト
- spec を最初に作成する(RED)
-
extend ActiveSupport::Concernを使用する - コールバック/バリデーション/スコープには
includedブロックを使用する - クラスレベルのメソッドには
class_methodsブロックを使用する - インスタンスメソッドはブロックの外に記述する
- 単一責任(concern ごとに目的は 1 つ)
- 内容を表す適切な名前を付ける
- すべての spec が GREEN
コメント
Markdown 対応