# Ruby on Rails 面接質問: 2026年トップ25 > Ruby on Railsで最も多く問われる面接質問25選。MVCアーキテクチャ、Active Record、マイグレーション、RSpecテスト、REST APIを詳細な解説とコード例とともに解説。 - Published: 2026-02-03 - Updated: 2026-04-27 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, ruby, 面接, active record, 技術面接 - Reading time: 22 min --- Ruby on Railsの面接では、最も人気の高いRubyフレームワークへの習熟度、MVCアーキテクチャの理解、Active Record ORMの理解、そして「Convention over Configuration」の哲学に従って堅牢なWebアプリケーションを構築する能力が評価されます。本ガイドでは、Railsの基本から本番環境向けの高度なパターンまで、最も多く問われる25の質問を解説します。 > **面接のヒント** > > 採用担当者は、Railsの哲学である「Convention over Configuration」、DRY (Don't Repeat Yourself)、Rails Wayパターンを理解している候補者を高く評価します。Railsが特定のアーキテクチャ上の選択を行う理由を説明できることが差別化要因になります。 ## Ruby on Railsの基礎 ### 質問1: Ruby on RailsにおけるMVCパターンを説明してください Model-View-Controller (MVC) パターンはRailsのアーキテクチャの中核です。責務を3つの異なる層に分離することで、コードの保守性とテスト容易性を高めます。 ```ruby # app/models/article.rb # Modelはデータとビジネスロジックを管理します class Article < ApplicationRecord # データのバリデーション validates :title, presence: true, length: { minimum: 5 } validates :body, presence: true # 他のモデルとの関連 belongs_to :author, class_name: 'User' has_many :comments, dependent: :destroy has_many :tags, through: :article_tags # 再利用可能なクエリのためのスコープ scope :published, -> { where(published: true) } scope :recent, -> { order(created_at: :desc).limit(10) } # ライフサイクルコールバック before_save :generate_slug private def generate_slug self.slug = title.parameterize if title_changed? end end ``` ```ruby # app/controllers/articles_controller.rb # Controllerはリクエストを受け取り、レスポンスを取りまとめます class ArticlesController < ApplicationController before_action :authenticate_user!, except: [:index, :show] before_action :set_article, only: [:show, :edit, :update, :destroy] def index @articles = Article.published.recent.includes(:author) end def show @comments = @article.comments.includes(:user) end def create @article = current_user.articles.build(article_params) if @article.save redirect_to @article, notice: '記事を作成しました。' else render :new, status: :unprocessable_entity end end private def set_article @article = Article.find(params[:id]) end def article_params params.require(:article).permit(:title, :body, :published, tag_ids: []) end end ``` ```erb <%# app/views/articles/show.html.erb %> <%# Viewはデータを HTML 形式で表示します %>

<%= @article.title %>

投稿者: <%= @article.author.name %> • <%= l @article.created_at, format: :long %>

<%= simple_format @article.body %>
<%# コメント用のパーシャル %> <%= render @comments %>
``` 典型的な流れは次のとおりです。リクエストはRouterに到達し、適切なControllerに振り分けられます。ControllerはModelと連携してデータを取得または更新し、そのデータをViewに渡してHTMLとしてレンダリングします。 ### 質問2: Active RecordとはなにですRailsのORMはどのように機能しますか? Active RecordはRailsのORM (Object-Relational Mapping) であり、Active Recordパターンを実装します。各Modelクラスはデータベースの1テーブルを表し、各インスタンスは1レコードを表します。 ```ruby # app/models/user.rb # Active Recordはカラムを属性に自動的にマッピングします class User < ApplicationRecord # 'users' テーブルが自動的に対応付けられます # カラム: id, email, name, created_at, updated_at has_secure_password # パスワード用のBCrypt has_many :articles, foreign_key: :author_id has_one :profile, dependent: :destroy has_and_belongs_to_many :roles # バリデーション validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP } # コールバック before_save :normalize_email # クエリ用のクラスメソッド def self.admins joins(:roles).where(roles: { name: 'admin' }) end private def normalize_email self.email = email.downcase.strip end end ``` ```ruby # Active Recordのクエリ例 # Railsコンソールやサービス内で使用 # 作成 user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123') # 条件付きの読み取り active_users = User.where(active: true).order(:name) user = User.find_by(email: 'dev@example.com') # チェーンクエリ (lazy evaluation) recent_admins = User.admins .where('created_at > ?', 1.month.ago) .includes(:profile) .limit(10) # eager loading による N+1 の防止 articles = Article.includes(:author, :comments).published # 更新 user.update!(name: 'Alice Martin') # トランザクション User.transaction do user.debit_balance!(100) recipient.credit_balance!(100) Payment.create!(from: user, to: recipient, amount: 100) end ``` Active RecordはRubyのメソッドを最適化されたSQLクエリに変換します。`where`, `joins`, `includes` などのメソッドは遅延評価で、イテレートしたり `to_a` を呼び出した時点で初めて実行されます。 ### 質問3: Railsのマイグレーションシステムを説明してください マイグレーションはデータベーススキーマをRubyでバージョン管理する仕組みです。リバーシブルであり、データ構造の制御された進化を可能にします。 ```ruby # db/migrate/20260203100000_create_products.rb # テーブル作成のためのマイグレーション class CreateProducts < ActiveRecord::Migration[7.1] def change create_table :products do |t| t.string :name, null: false t.text :description t.decimal :price, precision: 10, scale: 2, null: false t.integer :stock_quantity, default: 0 t.references :category, null: false, foreign_key: true t.boolean :active, default: true t.timestamps # created_at と updated_at を自動付与 end # パフォーマンスのためのインデックス add_index :products, :name add_index :products, [:category_id, :active] end end ``` ```ruby # db/migrate/20260203110000_add_slug_to_products.rb # 既存テーブルを変更するマイグレーション class AddSlugToProducts < ActiveRecord::Migration[7.1] def change add_column :products, :slug, :string add_index :products, :slug, unique: true # 既存スラッグを埋める reversible do |dir| dir.up do Product.find_each do |product| product.update_column(:slug, product.name.parameterize) end end end # 埋めた後にNOT NULLに変更 change_column_null :products, :slug, false end end ``` ```bash # 必須のマイグレーションコマンド rails db:migrate # 保留中のマイグレーションを実行 rails db:rollback # 直前のマイグレーションを取り消し rails db:rollback STEP=3 # 直前3件のマイグレーションを取り消し rails db:migrate:status # マイグレーション状態を確認 rails db:seed # db/seeds.rb を実行 rails db:reset # Drop, create, migrate, seed ``` マイグレーションはリバーシブルである必要があります。`change` メソッドは賢く、よくある操作は自動で逆実行できます。複雑な場合には `up` と `down` を別々に書きましょう。 ## Active Record の応用 ### 質問4: RailsでN+1クエリを最適化する方法は? N+1問題は、最初のクエリの後に関連データを取得するためのN個の追加クエリが発生することで起こります。Railsはこの問題を解決する eager loading 手法をいくつか提供しています。 ```ruby # app/controllers/orders_controller.rb class OrdersController < ApplicationController def index # ❌ N+1問題: 1クエリ + 注文ごとにNクエリ # @orders = Order.all # ビュー内で order.user.name が注文ごとにクエリを発行 # ✅ includes (eager loading) による解決策 @orders = Order.includes(:user, :items) .where(status: 'completed') .order(created_at: :desc) # 合計で3クエリのみ発行 end def show # includes: 関連を別のクエリで読み込む (2-3クエリ) @order = Order.includes(items: :product).find(params[:id]) # preload: 別クエリでの読み込みを強制 @order = Order.preload(:items, :user).find(params[:id]) # eager_load: LEFT OUTER JOIN を強制 (1クエリ) @order = Order.eager_load(:items).find(params[:id]) end end ``` ```ruby # app/models/order.rb class Order < ApplicationRecord belongs_to :user has_many :items, class_name: 'OrderItem' has_many :products, through: :items # デフォルトの includes 付きスコープ scope :with_details, -> { includes(:user, items: :product) } # COUNT クエリ回避のための counter cache # 必要: add_column :users, :orders_count, :integer, default: 0 belongs_to :user, counter_cache: true end ``` ```ruby # Bullet gem による N+1 検出 (development) # config/environments/development.rb config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.rails_logger = true end # Bullet が次の場合に警告を表示します: # - N+1 クエリが検出された # - 不要な eager loading が存在する # - counter cache を使用すべき ``` ルール: 既定では `includes` を使用 (Rails が最適な戦略を選びます)、別クエリを強制したい場合は `preload`、関連でフィルタリングする場合は `eager_load` を使います。 ### 質問5: Railsの Scope と Query Object を説明してください スコープは再利用可能なクエリ条件をカプセル化します。複雑なクエリには Query Object の方が整理しやすく、テスト容易性も向上します。 ```ruby # app/models/product.rb class Product < ApplicationRecord # シンプルなスコープ scope :active, -> { where(active: true) } scope :in_stock, -> { where('stock_quantity > 0') } scope :featured, -> { where(featured: true) } # 引数を受け取るスコープ scope :cheaper_than, ->(price) { where('price < ?', price) } scope :in_category, ->(category) { where(category: category) } # チェーン可能なスコープ scope :available, -> { active.in_stock } # joins を含むスコープ scope :with_recent_orders, -> { joins(:order_items) .where('order_items.created_at > ?', 30.days.ago) .distinct } # サブクエリを含むスコープ scope :bestsellers, -> { where(id: OrderItem.group(:product_id) .order('COUNT(*) DESC') .limit(10) .select(:product_id)) } end ``` ```ruby # app/queries/products_search_query.rb # 複雑な検索のための Query Object class ProductsSearchQuery def initialize(relation = Product.all) @relation = relation end def call(params) @relation = filter_by_category(params[:category]) @relation = filter_by_price_range(params[:min_price], params[:max_price]) @relation = filter_by_search(params[:q]) @relation = apply_sorting(params[:sort]) @relation end private def filter_by_category(category) return @relation if category.blank? @relation.where(category_id: category) end def filter_by_price_range(min, max) @relation = @relation.where('price >= ?', min) if min.present? @relation = @relation.where('price <= ?', max) if max.present? @relation end def filter_by_search(query) return @relation if query.blank? @relation.where('name ILIKE ? OR description ILIKE ?', "%#{query}%", "%#{query}%") end def apply_sorting(sort) case sort when 'price_asc' then @relation.order(price: :asc) when 'price_desc' then @relation.order(price: :desc) when 'newest' then @relation.order(created_at: :desc) else @relation.order(:name) end end end # コントローラーでの使用 @products = ProductsSearchQuery.new(Product.active).call(params) ``` スコープはシンプルで再利用可能な条件に最適です。Query Object は複数の任意フィルタや組み合わせロジックを伴う複雑な検索に向いています。 ## ルーティングとコントローラー ### 質問6: Rails の RESTful ルーティングの仕組みは? Rails は HTTP動詞をCRUD操作に対応付けるRESTfulなルートを推奨します。ルーターはURLを特定のコントローラーアクションに変換します。 ```ruby # config/routes.rb Rails.application.routes.draw do # 標準的な RESTful ルート (7アクション) resources :articles do # ネストされたルート resources :comments, only: [:create, :destroy] # メンバールート (1インスタンスに対して動作) member do post :publish delete :archive end # コレクションルート (コレクションに対して動作) collection do get :drafts get :search end end # namespace を使った API ルート namespace :api do namespace :v1 do resources :products, only: [:index, :show, :create, :update] do resources :reviews, shallow: true end end end # カスタムルート get 'dashboard', to: 'dashboard#index' # ルートの制約 constraints(SubdomainConstraint.new) do resources :admin_settings end # ルートパス root 'home#index' end ``` ```bash # rails routes - 生成されたすべてのルートを表示 # # Verb URI Pattern Controller#Action # GET /articles articles#index # POST /articles articles#create # GET /articles/new articles#new # GET /articles/:id/edit articles#edit # GET /articles/:id articles#show # PATCH /articles/:id articles#update # DELETE /articles/:id articles#destroy # POST /articles/:id/publish articles#publish # GET /articles/drafts articles#drafts ``` 生成されるルートヘルパー (`article_path(@article)` や `new_article_path`) を使えば、URL を動的かつ保守しやすく参照できます。 ### 質問7: コントローラーのコールバックとフィルターを説明してください コールバック (before_action, after_action, around_action) は、コントローラーアクションの前・後・周辺でコードを実行する仕組みです。 ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # CSRF 保護はデフォルトで有効 protect_from_forgery with: :exception # 認証用のグローバルコールバック before_action :authenticate_user! # グローバルなエラーハンドリング rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::ParameterMissing, with: :bad_request private def not_found render json: { error: 'リソースが見つかりません' }, status: :not_found end def bad_request(exception) render json: { error: exception.message }, status: :bad_request end end ``` ```ruby # app/controllers/admin/products_controller.rb class Admin::ProductsController < ApplicationController # オプション付きコールバック before_action :require_admin before_action :set_product, only: [:show, :edit, :update, :destroy] after_action :log_activity, only: [:create, :update, :destroy] # 条件付きコールバック before_action :check_stock, only: [:update], if: :stock_changed? def create @product = Product.new(product_params) if @product.save redirect_to [:admin, @product], notice: '商品を作成しました。' else render :new, status: :unprocessable_entity end end def update if @product.update(product_params) redirect_to [:admin, @product], notice: '商品を更新しました。' else render :edit, status: :unprocessable_entity end end private def require_admin redirect_to root_path unless current_user&.admin? end def set_product @product = Product.find(params[:id]) end def stock_changed? params[:product][:stock_quantity].present? end def log_activity ActivityLog.create!( user: current_user, action: action_name, resource: @product ) end def product_params params.require(:product).permit(:name, :price, :description, :stock_quantity) end end ``` コールバックは宣言順に実行されます。サブクラスで継承したコールバックを無効化するには `skip_before_action` を使います。ビジネスロジックを多く含むコールバックは避け、Service Object を優先しましょう。 ## サービスとアーキテクチャ ### 質問8: Rails で Service Object を実装するには? Service Object は、ModelにもControllerにも属さない複雑なビジネスロジックをカプセル化します。テスト容易性が向上し、単一責任の原則に従いやすくなります。 ```ruby # app/services/order_processor.rb # 標準化されたインターフェースを持つ Service Object class OrderProcessor def initialize(order, payment_method:) @order = order @payment_method = payment_method end def call return failure('注文は処理済みです') if @order.processed? ActiveRecord::Base.transaction do validate_stock! process_payment! update_inventory! send_confirmation! @order.update!(status: 'completed', processed_at: Time.current) end success(@order) rescue PaymentError => e failure("支払いに失敗しました: #{e.message}") rescue InsufficientStockError => e failure("在庫不足: #{e.message}") end private def validate_stock! @order.items.each do |item| unless item.product.stock_quantity >= item.quantity raise InsufficientStockError, item.product.name end end end def process_payment! result = PaymentGateway.charge( amount: @order.total, method: @payment_method, description: "注文 ##{@order.id}" ) raise PaymentError, result.error unless result.success? @order.update!(payment_reference: result.transaction_id) end def update_inventory! @order.items.each do |item| item.product.decrement!(:stock_quantity, item.quantity) end end def send_confirmation! OrderMailer.confirmation(@order).deliver_later end def success(data) Result.new(success: true, data: data) end def failure(error) Result.new(success: false, error: error) end Result = Struct.new(:success, :data, :error, keyword_init: true) do def success? = success def failure? = !success end end ``` ```ruby # app/controllers/orders_controller.rb class OrdersController < ApplicationController def create @order = current_user.orders.build(order_params) if @order.save result = OrderProcessor.new(@order, payment_method: params[:payment_method]).call if result.success? redirect_to @order, notice: '注文を確定しました!' else @order.update!(status: 'payment_failed') flash.now[:alert] = result.error render :new, status: :unprocessable_entity end else render :new, status: :unprocessable_entity end end end ``` Service Object パターンはシンプルな規約に従います: 1クラスにつき1責務、公開メソッドは `call` ひとつ。Result オブジェクトを返すことで成功と失敗の取り扱いを明確にできます。 ### 質問9: Rails の Concern を説明してください Concern を使うと、Model や Controller の間でコードを抽出して共有できます。`ActiveSupport::Concern` を使うことで、include 構文を簡潔に書けます。 ```ruby # app/models/concerns/sluggable.rb # スラッグ生成のための再利用可能な Concern module Sluggable extend ActiveSupport::Concern included do # include 時に実行されるコード before_validation :generate_slug, if: :should_generate_slug? validates :slug, presence: true, uniqueness: true end # クラスメソッド class_methods do def find_by_slug!(slug) find_by!(slug: slug) end def sluggable_source(column = :title) @sluggable_source = column end def sluggable_source_column @sluggable_source || :title end end # インスタンスメソッド def to_param slug end private def should_generate_slug? slug.blank? || send("#{self.class.sluggable_source_column}_changed?") end def generate_slug source = send(self.class.sluggable_source_column) return if source.blank? base_slug = source.parameterize self.slug = unique_slug(base_slug) end def unique_slug(base) slug = base counter = 1 while self.class.where(slug: slug).where.not(id: id).exists? slug = "#{base}-#{counter}" counter += 1 end slug end end ``` ```ruby # app/models/article.rb class Article < ApplicationRecord include Sluggable sluggable_source :title # 任意、既定は :title end # app/models/product.rb class Product < ApplicationRecord include Sluggable sluggable_source :name end ``` ```ruby # app/controllers/concerns/pagination.rb # コントローラー用の Concern module Pagination extend ActiveSupport::Concern included do helper_method :page_param, :per_page_param end private def paginate(relation) relation.page(page_param).per(per_page_param) end def page_param params[:page]&.to_i || 1 end def per_page_param [params[:per_page]&.to_i || 25, 100].min end end ``` Concern は本当に共通のコードのために使うのが適切です。Model を「短くする」だけの目的で Concern を作るのは避けましょう: 複雑さを減らさず隠してしまいます。 ## RSpec を用いたテスト ### 質問10: Rails の RSpec テストはどう構成しますか? RSpec は Rails の標準的なテストフレームワークです。良いテスト構成には Model spec、Controller spec、Service spec、結合テストが含まれます。 ```ruby # spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do # FactoryBot のファクトリ let(:user) { build(:user) } let(:admin) { build(:user, :admin) } describe 'validations' do it { is_expected.to validate_presence_of(:email) } it { is_expected.to validate_uniqueness_of(:email).case_insensitive } it 'メールの形式を検証する' do user.email = 'invalid' expect(user).not_to be_valid expect(user.errors[:email]).to include('is invalid') end end describe 'associations' do it { is_expected.to have_many(:articles).dependent(:destroy) } it { is_expected.to have_one(:profile) } it { is_expected.to belong_to(:organization).optional } end describe '#full_name' do it '姓名を結合して返す' do user = build(:user, first_name: 'John', last_name: 'Doe') expect(user.full_name).to eq('John Doe') end it '姓が無い場合も処理する' do user = build(:user, first_name: 'John', last_name: nil) expect(user.full_name).to eq('John') end end describe '.active' do it 'アクティブなユーザーのみを返す' do active = create(:user, active: true) inactive = create(:user, active: false) expect(User.active).to include(active) expect(User.active).not_to include(inactive) end end end ``` ```ruby # spec/services/order_processor_spec.rb require 'rails_helper' RSpec.describe OrderProcessor do let(:user) { create(:user) } let(:product) { create(:product, stock_quantity: 10, price: 100) } let(:order) { create(:order, user: user, items: [build(:order_item, product: product, quantity: 2)]) } subject { described_class.new(order, payment_method: 'card') } describe '#call' do context '注文が有効なとき' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: true, transaction_id: 'txn_123') ) end it '注文を正常に処理する' do result = subject.call expect(result).to be_success expect(order.reload.status).to eq('completed') end it '商品の在庫を減らす' do expect { subject.call }.to change { product.reload.stock_quantity }.by(-2) end it '確認メールを送信する' do expect { subject.call } .to have_enqueued_mail(OrderMailer, :confirmation) .with(order) end end context '支払いが失敗するとき' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: false, error: 'Card declined') ) end it '失敗結果を返す' do result = subject.call expect(result).to be_failure expect(result.error).to include('Card declined') end it '注文ステータスは更新しない' do expect { subject.call }.not_to change { order.reload.status } end end end end ``` ```ruby # spec/requests/api/v1/products_spec.rb require 'rails_helper' RSpec.describe 'API V1 Products', type: :request do let(:user) { create(:user) } let(:headers) { { 'Authorization' => "Bearer #{user.api_token}" } } describe 'GET /api/v1/products' do let!(:products) { create_list(:product, 3, :active) } it '商品一覧を返す' do get '/api/v1/products', headers: headers expect(response).to have_http_status(:ok) expect(json_response['data'].size).to eq(3) end it 'カテゴリで絞り込む' do category = create(:category) categorized = create(:product, category: category) get '/api/v1/products', params: { category_id: category.id }, headers: headers expect(json_response['data'].map { |p| p['id'] }).to eq([categorized.id]) end end describe 'POST /api/v1/products' do let(:valid_params) do { product: { name: '新商品', price: 99.99, category_id: create(:category).id } } end it '新しい商品を作成する' do expect { post '/api/v1/products', params: valid_params, headers: headers }.to change(Product, :count).by(1) expect(response).to have_http_status(:created) end end end ``` ベストプラクティス: データには `let`、メソッドや文脈には `describe`、条件には `context`、具体的なアサーションには `it` を使用します。1つのテストでは1つのことだけを検証しましょう。 ### 質問11: FactoryBot でファクトリを使うには? FactoryBot は宣言的かつ保守しやすい形式でテストデータを生成できます。ファクトリは静的なフィクスチャを置き換えます。 ```ruby # spec/factories/users.rb FactoryBot.define do factory :user do # 一意性確保のためのシーケンス sequence(:email) { |n| "user#{n}@example.com" } first_name { Faker::Name.first_name } last_name { Faker::Name.last_name } password { 'password123' } confirmed_at { Time.current } # バリエーションのためのトレイト trait :admin do role { 'admin' } after(:create) do |user| user.permissions.create!(name: 'admin_access') end end trait :unconfirmed do confirmed_at { nil } end trait :with_profile do after(:create) do |user| create(:profile, user: user) end end trait :with_articles do transient do articles_count { 3 } end after(:create) do |user, evaluator| create_list(:article, evaluator.articles_count, author: user) end end # 継承ファクトリ factory :admin_user do admin with_profile end end end ``` ```ruby # spec/factories/orders.rb FactoryBot.define do factory :order do user status { 'pending' } trait :with_items do transient do items_count { 2 } end after(:create) do |order, evaluator| create_list(:order_item, evaluator.items_count, order: order) order.recalculate_total! end end trait :completed do status { 'completed' } processed_at { Time.current } with_items end trait :high_value do after(:create) do |order| create(:order_item, order: order, quantity: 10, unit_price: 500) order.recalculate_total! end end end end ``` ```ruby # テストでの使用 RSpec.describe OrderProcessor do # build: 永続化しないインスタンス let(:user) { build(:user) } # create: DBに保存 let(:order) { create(:order, :with_items, user: user) } # create_list: 複数インスタンス let(:products) { create_list(:product, 5) } # トレイトの組み合わせ let(:admin) { create(:user, :admin, :with_profile) } # 属性の上書き let(:expensive_order) { create(:order, :with_items, items_count: 10) } # build_stubbed: 高速、ユニットテスト向け let(:stubbed_user) { build_stubbed(:user) } end ``` 永続化が不要な場合は `create` ではなく `build` または `build_stubbed` を選びましょう: テストが大幅に速くなります。 ## バックグラウンドジョブ ### 質問12: Rails で Active Job と Sidekiq を使うには? Active Job はバックエンド (Sidekiq、Resque など) を問わずバックグラウンドジョブの統一インターフェースを提供します。Sidekiq は Redis との組み合わせによる性能で人気の選択肢です。 ```ruby # app/jobs/process_order_job.rb class ProcessOrderJob < ApplicationJob queue_as :default # リトライ設定 retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10 discard_on ActiveJob::DeserializationError # Sidekiq オプション (バックエンドが Sidekiq の場合) sidekiq_options retry: 5, backtrace: true def perform(order_id) order = Order.find(order_id) OrderProcessor.new(order).call rescue ActiveRecord::RecordNotFound # キュー投入から実行までの間に注文が削除された場合 Rails.logger.warn("Order #{order_id} not found, skipping job") end end ``` ```ruby # app/jobs/batch_email_job.rb class BatchEmailJob < ApplicationJob queue_as :mailers # Sidekiq Enterprise や throttle gem によるレート制限 sidekiq_options throttle: { threshold: 100, period: 1.minute } def perform(user_ids, template_id) template = EmailTemplate.find(template_id) User.where(id: user_ids).find_each do |user| UserMailer.custom_email(user, template).deliver_later end end end ``` ```ruby # ジョブのキュー投入 # 即時 ProcessOrderJob.perform_later(order.id) # 遅延 ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id) # 指定時刻 ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id) # 特定のキュー ProcessOrderJob.set(queue: :critical).perform_later(order.id) # 同期実行 (テストやデバッグ用) ProcessOrderJob.perform_now(order.id) ``` ```ruby # config/sidekiq.yml :concurrency: 10 :queues: - [critical, 3] # 高優先度、重み 3 - [default, 2] # 中優先度、重み 2 - [mailers, 1] # 低優先度、重み 1 - [low, 1] :schedule: cleanup_job: cron: '0 3 * * *' # 毎日午前 3 時 class: CleanupJob ``` Active Job はバックエンドを抽象化しますが、バッチやレートリミットなどのバックエンド固有機能を使うには、選択したバックエンドへの結合がしばしば必要になります。 ## API 開発 ### 質問13: Rails で RESTful API を構築するには? Rails は API-only コントローラーとシリアライザーを使って JSON API の構築を容易にします。良い API はバージョン管理され、文書化され、安全であることが必要です。 ```ruby # app/controllers/api/v1/base_controller.rb module Api module V1 class BaseController < ActionController::API include ActionController::HttpAuthentication::Token::ControllerMethods before_action :authenticate_token! rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity rescue_from ActionController::ParameterMissing, with: :bad_request private def authenticate_token! authenticate_or_request_with_http_token do |token, options| @current_user = User.find_by(api_token: token) end end def current_user @current_user end def not_found(exception) render json: { error: 'リソースが見つかりません', details: exception.message }, status: :not_found end def unprocessable_entity(exception) render json: { error: 'バリデーションに失敗しました', details: exception.record.errors }, status: :unprocessable_entity end def bad_request(exception) render json: { error: 'リクエストが不正です', details: exception.message }, status: :bad_request end end end end ``` ```ruby # app/controllers/api/v1/products_controller.rb module Api module V1 class ProductsController < BaseController before_action :set_product, only: [:show, :update, :destroy] def index @products = Product.active .includes(:category) .page(params[:page]) .per(params[:per_page] || 20) render json: { data: ProductSerializer.new(@products).serializable_hash, meta: pagination_meta(@products) } end def show render json: ProductSerializer.new(@product, include: [:category, :reviews]) end def create @product = Product.new(product_params) @product.save! render json: ProductSerializer.new(@product), status: :created end def update @product.update!(product_params) render json: ProductSerializer.new(@product) end def destroy @product.destroy! head :no_content end private def set_product @product = Product.find(params[:id]) end def product_params params.require(:product).permit(:name, :description, :price, :category_id) end def pagination_meta(collection) { current_page: collection.current_page, total_pages: collection.total_pages, total_count: collection.total_count } end end end end ``` ```ruby # app/serializers/product_serializer.rb # jsonapi-serializer gem を使用 class ProductSerializer include JSONAPI::Serializer attributes :id, :name, :description, :price, :created_at attribute :formatted_price do |product| "$#{product.price.to_f.round(2)}" end belongs_to :category has_many :reviews link :self do |product| Rails.application.routes.url_helpers.api_v1_product_url(product) end end ``` API のベストプラクティス: namespace 経由のバージョニング、適切な HTTP コードの使用、コレクションのページネーション、明瞭なエラーメッセージの提供です。 ### 質問14: Rails で JWT 認証を実装するには? JWT (JSON Web Tokens) は API 向けに人気のあるステートレス認証方式です。トークンはユーザーのアイデンティティと有効性を符号化します。 ```ruby # app/services/jwt_service.rb class JwtService SECRET_KEY = Rails.application.credentials.secret_key_base ALGORITHM = 'HS256'.freeze class << self def encode(payload, exp = 24.hours.from_now) payload[:exp] = exp.to_i payload[:iat] = Time.current.to_i JWT.encode(payload, SECRET_KEY, ALGORITHM) end def decode(token) decoded = JWT.decode(token, SECRET_KEY, true, algorithm: ALGORITHM) HashWithIndifferentAccess.new(decoded.first) rescue JWT::ExpiredSignature raise AuthenticationError, 'トークンの有効期限が切れています' rescue JWT::DecodeError raise AuthenticationError, 'トークンが無効です' end end end ``` ```ruby # app/controllers/api/v1/auth_controller.rb module Api module V1 class AuthController < ActionController::API def login user = User.find_by(email: params[:email]) if user&.authenticate(params[:password]) token = JwtService.encode(user_id: user.id) render json: { token: token, user: UserSerializer.new(user), expires_at: 24.hours.from_now } else render json: { error: '認証情報が正しくありません' }, status: :unauthorized end end def refresh token = JwtService.encode(user_id: current_user.id) render json: { token: token, expires_at: 24.hours.from_now } end end end end ``` ```ruby # app/controllers/concerns/jwt_authenticatable.rb module JwtAuthenticatable extend ActiveSupport::Concern included do before_action :authenticate_jwt! end private def authenticate_jwt! header = request.headers['Authorization'] token = header&.split(' ')&.last raise AuthenticationError, 'トークンがありません' unless token decoded = JwtService.decode(token) @current_user = User.find(decoded[:user_id]) rescue AuthenticationError => e render json: { error: e.message }, status: :unauthorized rescue ActiveRecord::RecordNotFound render json: { error: 'ユーザーが見つかりません' }, status: :unauthorized end def current_user @current_user end end ``` 本番では、リフレッシュトークン、ログアウト時のトークンブラックリスト、短い有効期限などを検討してください。`devise-jwt` のような gem を使うと実装が簡素化されます。 ## キャッシュとパフォーマンス ### 質問15: Rails でキャッシュを実装するには? Rails は複数のキャッシュ階層を提供します: フラグメントキャッシング、ロシアンドールキャッシング、ローレベルキャッシング。利用ケースに応じて選びます。 ```ruby # config/environments/production.rb config.action_controller.perform_caching = true config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'], namespace: 'myapp:cache', expires_in: 1.day, race_condition_ttl: 10.seconds } ``` ```erb <%# app/views/products/index.html.erb %> <%# 自動キャッシュキーによるフラグメントキャッシング %> <% @products.each do |product| %> <%# 商品の updated_at をベースとしたキャッシュ %> <% cache product do %> <%= render product %> <% end %> <% end %> <%# ロシアンドールキャッシング - ネストしたキャッシュ %> <% cache ['v1', @category] do %>

<%= @category.name %>

<% @category.products.each do |product| %> <% cache ['v1', product] do %> <%= render product %> <% end %> <% end %> <% end %> <%# 条件付きキャッシュ %> <% cache_if current_user.nil?, @product do %> <%= render @product %> <% end %> ``` ```ruby # app/models/product.rb class Product < ApplicationRecord # ロシアンドールキャッシュ無効化のため親を touch belongs_to :category, touch: true # カスタムキャッシュキー def cache_key_with_version "#{super}/#{reviews.maximum(:updated_at)&.to_i}" end end ``` ```ruby # サービスでのローレベルキャッシング class DashboardStatsService def call Rails.cache.fetch('dashboard:stats', expires_in: 15.minutes) do { total_users: User.count, active_users: User.where('last_sign_in_at > ?', 30.days.ago).count, total_orders: Order.completed.count, revenue_mtd: Order.completed.where(created_at: Time.current.beginning_of_month..).sum(:total) } end end end # レースコンディション保護付きキャッシュ Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do Product.bestsellers.limit(10).to_a end # 明示的な無効化 Rails.cache.delete('dashboard:stats') Rails.cache.delete_matched('products:*') ``` ロシアンドールキャッシングは変更されたフラグメントのみが再生成されるため効率的です。関連には `touch: true` を指定して無効化を伝播させましょう。 ### 質問16: Rails アプリケーションのパフォーマンスを最適化するには? Rails の最適化は DB クエリ、キャッシュ、アセット、アーキテクチャなど複数の側面に及びます。監視を伴う体系的なアプローチが必須です。 ```ruby # データベース最適化 # config/database.yml production: pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> prepared_statements: true advisory_locks: true # app/models/order.rb class Order < ApplicationRecord # 頻繁なクエリ向けの複合インデックス # add_index :orders, [:user_id, :status, :created_at] # 必要な列のみ選択 scope :summary, -> { select(:id, :status, :total, :created_at) } # 大量データ向けのバッチ処理 def self.process_pending pending.find_each(batch_size: 1000) do |order| ProcessOrderJob.perform_later(order.id) end end # 繰り返し計算を避ける def self.revenue_by_month completed .group("DATE_TRUNC('month', created_at)") .sum(:total) end end ``` ```ruby # メモリ最適化 # config/puma.rb workers ENV.fetch("WEB_CONCURRENCY") { 2 } threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } threads threads_count, threads_count preload_app! before_fork do ActiveRecord::Base.connection_pool.disconnect! end on_worker_boot do ActiveRecord::Base.establish_connection end ``` ```ruby # rack-mini-profiler によるプロファイリング # Gemfile group :development do gem 'rack-mini-profiler' gem 'memory_profiler' gem 'stackprof' end # config/initializers/mini_profiler.rb if defined?(Rack::MiniProfiler) Rack::MiniProfiler.config.position = 'bottom-right' Rack::MiniProfiler.config.start_hidden = true end ``` ```ruby # 遅延読み込みとページネーション class ProductsController < ApplicationController def index @products = Product.active .includes(:category, :primary_image) .page(params[:page]) .per(24) # 次ページのプリフェッチ if @products.next_page Rails.cache.fetch("products:page:#{@products.next_page}", expires_in: 5.minutes) do Product.active.page(@products.next_page).per(24).to_a end end end end ``` 主要ツール: プロファイリングには `rack-mini-profiler`、N+1 検出には `bullet`、本番監視には `New Relic` や `Scout`。 ## セキュリティ ### 質問17: Rails のセキュリティのベストプラクティスは? Rails は一般的な脆弱性に対する既定の保護を備えています。これらの保護を理解し正しく構成することが重要です。 ```ruby # CSRF 保護 # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # 既定で有効、トークン無効時に例外を発生 protect_from_forgery with: :exception # API では :null_session を使う # protect_from_forgery with: :null_session end # ビューでは form 要素にトークンが自動で含まれます # <%= form_with ... %> は authenticity_token を含む # AJAX リクエスト向け # csrf_meta_tags の値で X-CSRF-Token ヘッダーを追加 ``` ```ruby # SQL インジェクションの予防 # ✅ 補間されるパラメータは自動的にエスケープ User.where('email = ?', params[:email]) User.where(email: params[:email]) # ❌ 危険 - 直接的な文字列補間 User.where("email = '#{params[:email]}'") # ✅ 動的な ORDER 句のため ALLOWED_SORTS = %w[name created_at price].freeze sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name' Product.order(sort_column) ``` ```ruby # XSS 保護 # Rails はビュー内の HTML を自動でエスケープ # ✅ 自動エスケープ <%= user.name %> # ❌ 危険 - エスケープされない内容 <%== user.bio %> <%= raw user.bio %> <%= user.bio.html_safe %> # ✅ 安全な HTML には sanitize を使用 <%= sanitize user.bio, tags: %w[p br strong em] %> ``` ```ruby # Strong Parameters class UsersController < ApplicationController def update @user.update!(user_params) end private def user_params # 許可属性の明示的なホワイトリスト params.require(:user).permit(:name, :email, :avatar) # 管理者のみ if current_user.admin? params.require(:user).permit(:name, :email, :role, :active) else params.require(:user).permit(:name, :email) end end end ``` ```ruby # セキュリティヘッダー # config/initializers/secure_headers.rb Rails.application.config.action_dispatch.default_headers = { 'X-Frame-Options' => 'SAMEORIGIN', 'X-XSS-Protection' => '1; mode=block', 'X-Content-Type-Options' => 'nosniff', 'X-Download-Options' => 'noopen', 'X-Permitted-Cross-Domain-Policies' => 'none', 'Referrer-Policy' => 'strict-origin-when-cross-origin' } # Content Security Policy Rails.application.config.content_security_policy do |policy| policy.default_src :self policy.script_src :self policy.style_src :self, :unsafe_inline policy.img_src :self, :data, 'https:' end ``` `brakeman` (静的セキュリティ解析) で定期的に監査し、`bundle audit` で gem を最新に保ちましょう。 ### 質問18: Rails で認証と認可をどう扱うか? 認証は本人確認、認可は権限制御です。Devise が認証を担い、Pundit や CanCanCan が認可を担当します。 ```ruby # Devise セットアップ # app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable, :lockable, :trackable enum role: { user: 0, moderator: 1, admin: 2 } def admin? role == 'admin' end end ``` ```ruby # Pundit ポリシー # app/policies/article_policy.rb class ArticlePolicy < ApplicationPolicy def index? true end def show? record.published? || owner_or_admin? end def create? user.present? end def update? owner_or_admin? end def destroy? owner_or_admin? end def publish? user&.admin? || user&.moderator? end # コレクション用スコープ class Scope < Scope def resolve if user&.admin? scope.all elsif user scope.where(published: true).or(scope.where(author: user)) else scope.where(published: true) end end end private def owner_or_admin? user&.admin? || record.author == user end end ``` ```ruby # Pundit を用いたコントローラー class ArticlesController < ApplicationController include Pundit::Authorization after_action :verify_authorized, except: :index after_action :verify_policy_scoped, only: :index def index @articles = policy_scope(Article).includes(:author).page(params[:page]) end def show @article = Article.find(params[:id]) authorize @article end def update @article = Article.find(params[:id]) authorize @article if @article.update(article_params) redirect_to @article, notice: '記事を更新しました。' else render :edit, status: :unprocessable_entity end end def publish @article = Article.find(params[:id]) authorize @article @article.update!(published: true, published_at: Time.current) redirect_to @article, notice: '記事を公開しました。' end rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "この操作を実行する権限がありません。" redirect_back(fallback_location: root_path) end end ``` Pundit は CanCanCan よりも明示的でテストしやすい設計です。各アクションに対応するポリシーメソッドが存在し、スコープによりコレクションを自動的にフィルタリングします。 ## Rails 応用 ### 質問19: Rails の Repository パターンを説明してください Repository パターンはデータアクセスロジックをアプリケーションの他の部分から分離します。Rails は Active Record (別パターン) を採用していますが、Repository は複雑なケースで有用です。 ```ruby # app/repositories/base_repository.rb class BaseRepository def initialize(model_class) @model_class = model_class end def all @model_class.all end def find(id) @model_class.find(id) end def find_by(attributes) @model_class.find_by(attributes) end def create(attributes) @model_class.create(attributes) end def update(record, attributes) record.update(attributes) end def delete(record) record.destroy end end ``` ```ruby # app/repositories/product_repository.rb class ProductRepository < BaseRepository def initialize super(Product) end def active @model_class.where(active: true) end def in_category(category_id) @model_class.where(category_id: category_id) end def search(query) @model_class.where('name ILIKE ? OR description ILIKE ?', "%#{query}%", "%#{query}%") end def with_stock @model_class.where('stock_quantity > 0') end def bestsellers(limit: 10) @model_class .joins(:order_items) .group(:id) .order('COUNT(order_items.id) DESC') .limit(limit) end def for_homepage active .with_stock .includes(:category, :primary_image) .order(featured: :desc, created_at: :desc) .limit(12) end end ``` ```ruby # サービスでの利用 class ProductSearchService def initialize(repository: ProductRepository.new) @repository = repository end def call(params) products = @repository.active products = products.in_category(params[:category]) if params[:category] products = products.search(params[:query]) if params[:query].present? products = products.with_stock if params[:in_stock] products end end # モックを使ったテストが容易 RSpec.describe ProductSearchService do let(:repository) { instance_double(ProductRepository) } let(:service) { described_class.new(repository: repository) } it 'カテゴリで絞り込む' do products = double('products') allow(repository).to receive(:active).and_return(products) allow(products).to receive(:in_category).with(1).and_return(products) service.call(category: 1) expect(products).to have_received(:in_category).with(1) end end ``` Active Record は十分に優れたパターンなので、Rails では Repository は任意です。複雑なクエリやストレージ分離が重要な場合に活用しましょう。 ### 質問20: Rails で CQRS パターンを実装するには? CQRS (Command Query Responsibility Segregation) は読み込みと書き込みの操作を分離します。Rails ではクエリとコマンドを別クラスにすることで実現します。 ```ruby # app/commands/base_command.rb class BaseCommand include ActiveModel::Validations def self.call(*args) new(*args).call end def call return failure(errors) unless valid? execute end private def execute raise NotImplementedError end def success(data = nil) CommandResult.success(data) end def failure(errors) CommandResult.failure(errors) end end CommandResult = Struct.new(:success, :data, :errors, keyword_init: true) do def success? = success def failure? = !success def self.success(data) new(success: true, data: data, errors: []) end def self.failure(errors) new(success: false, data: nil, errors: Array(errors)) end end ``` ```ruby # app/commands/orders/create_order_command.rb module Orders class CreateOrderCommand < BaseCommand attr_reader :user, :items, :shipping_address validates :user, presence: true validates :items, presence: true validate :validate_items_availability def initialize(user:, items:, shipping_address:) @user = user @items = items @shipping_address = shipping_address end private def execute order = nil ActiveRecord::Base.transaction do order = Order.create!( user: user, shipping_address: shipping_address, status: 'pending' ) items.each do |item| order.items.create!( product_id: item[:product_id], quantity: item[:quantity], unit_price: Product.find(item[:product_id]).price ) end order.calculate_total! end OrderCreatedEvent.broadcast(order) success(order) rescue ActiveRecord::RecordInvalid => e failure(e.message) end def validate_items_availability items.each do |item| product = Product.find_by(id: item[:product_id]) unless product&.stock_quantity&.>= item[:quantity] errors.add(:items, "商品 #{item[:product_id]} は利用できません") end end end end end ``` ```ruby # app/queries/orders/user_orders_query.rb module Orders class UserOrdersQuery def initialize(user, params = {}) @user = user @params = params end def call orders = @user.orders.includes(:items, items: :product) orders = apply_status_filter(orders) orders = apply_date_filter(orders) orders = apply_sorting(orders) orders.page(@params[:page]).per(@params[:per_page] || 20) end private def apply_status_filter(orders) return orders unless @params[:status] orders.where(status: @params[:status]) end def apply_date_filter(orders) orders = orders.where('created_at >= ?', @params[:from]) if @params[:from] orders = orders.where('created_at <= ?', @params[:to]) if @params[:to] orders end def apply_sorting(orders) case @params[:sort] when 'oldest' then orders.order(created_at: :asc) when 'total_desc' then orders.order(total: :desc) else orders.order(created_at: :desc) end end end end ``` ```ruby # CQRS を使ったコントローラー class OrdersController < ApplicationController def index @orders = Orders::UserOrdersQuery.new(current_user, filter_params).call end def create result = Orders::CreateOrderCommand.call( user: current_user, items: order_params[:items], shipping_address: order_params[:shipping_address] ) if result.success? redirect_to result.data, notice: '注文を作成しました!' else flash.now[:alert] = result.errors.join(', ') render :new, status: :unprocessable_entity end end end ``` CQRS は読み書き要求が非対称な複雑なアプリケーションで威力を発揮します。シンプルな CRUD には過剰設計です。 ### 質問21: Action Cable で WebSocket をどう扱うか? Action Cable は Rails に WebSocket を統合し、双方向のリアルタイム通信を実現します。サーバー間の同期に Redis を利用します。 ```ruby # app/channels/application_cable/connection.rb module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user end private def find_verified_user # セッション cookie 経由 if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user # API 用の JWT 経由 elsif verified_user = verify_jwt_token verified_user else reject_unauthorized_connection end end def verify_jwt_token token = request.params[:token] return nil unless token decoded = JwtService.decode(token) User.find(decoded[:user_id]) rescue nil end end end ``` ```ruby # app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel def subscribed @room = ChatRoom.find(params[:room_id]) # 権限を確認 unless @room.accessible_by?(current_user) reject return end stream_for @room # 他の参加者へ入室通知 broadcast_presence(:joined) end def unsubscribed broadcast_presence(:left) if @room end def send_message(data) message = @room.messages.create!( user: current_user, content: data['content'] ) # すべての購読者へ配信 ChatChannel.broadcast_to(@room, { type: 'message', message: MessageSerializer.new(message).as_json }) end def typing ChatChannel.broadcast_to(@room, { type: 'typing', user: current_user.name }) end private def broadcast_presence(action) ChatChannel.broadcast_to(@room, { type: 'presence', action: action, user: current_user.name, online_count: @room.online_users_count }) end end ``` ```javascript // app/javascript/channels/chat_channel.js import consumer from "./consumer" const chatChannel = consumer.subscriptions.create( { channel: "ChatChannel", room_id: roomId }, { connected() { console.log("Connected to chat") }, disconnected() { console.log("Disconnected from chat") }, received(data) { switch(data.type) { case 'message': this.appendMessage(data.message) break case 'typing': this.showTypingIndicator(data.user) break case 'presence': this.updatePresence(data) break } }, sendMessage(content) { this.perform('send_message', { content: content }) }, notifyTyping() { this.perform('typing') } } ) ``` Action Cable は再接続と同期を自動で扱います。本番では Redis をアダプタとして設定し、同時接続数に応じてスケールしましょう。 ### 質問22: Rails でマルチテナンシーを実装するには? マルチテナンシーは1つのアプリケーションで複数の独立した顧客 (テナント) を扱う仕組みです。主な方式はデータベースレベル、スキーマレベル、行レベルの3つです。 ```ruby # 行レベルのマルチテナンシー (ActsAsTenant か手動実装) # app/models/concerns/tenant_scoped.rb module TenantScoped extend ActiveSupport::Concern included do belongs_to :tenant # 現在のテナントへの既定スコープ default_scope -> { where(tenant: Current.tenant) if Current.tenant } # テナント検証 before_validation :set_tenant, on: :create end private def set_tenant self.tenant ||= Current.tenant end end # app/models/current.rb class Current < ActiveSupport::CurrentAttributes attribute :tenant, :user end ``` ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :set_current_tenant private def set_current_tenant Current.tenant = resolve_tenant Current.user = current_user end def resolve_tenant # サブドメイン経由 if request.subdomain.present? && request.subdomain != 'www' Tenant.find_by!(subdomain: request.subdomain) # ヘッダー経由 (API 用) elsif request.headers['X-Tenant-ID'].present? Tenant.find(request.headers['X-Tenant-ID']) # ユーザー経由 elsif current_user current_user.tenant end rescue ActiveRecord::RecordNotFound redirect_to root_url(subdomain: 'www'), alert: 'テナントが見つかりません' end end ``` ```ruby # app/models/project.rb class Project < ApplicationRecord include TenantScoped has_many :tasks belongs_to :owner, class_name: 'User' end # app/models/user.rb class User < ApplicationRecord include TenantScoped has_many :projects, foreign_key: :owner_id # 管理者は複数のテナントに所属可能 has_many :tenant_memberships has_many :accessible_tenants, through: :tenant_memberships, source: :tenant end ``` ```ruby # Apartment gem によるスキーマレベル (PostgreSQL) # config/initializers/apartment.rb Apartment.configure do |config| config.excluded_models = %w[Tenant User] config.tenant_names = -> { Tenant.pluck(:subdomain) } end # 利用例 Apartment::Tenant.switch('acme') do # このブロック内のクエリは 'acme' スキーマを使用 Project.all # SELECT * FROM acme.projects end ``` 行レベルは最もシンプルですが、漏えいに常に注意が必要です。スキーマレベルは隔離が強固ですが、マイグレーションが複雑化します。セキュリティとスケーラビリティの要件に応じて選びましょう。 ### 質問23: Rails でマイクロサービス構成をセットアップするには? Rails は HTTP/gRPC やメッセージキューによる通信を伴うマイクロサービス構成の基盤として機能します。境界の定義が鍵となります。 ```ruby # HTTP サービスクライアント # app/services/payment_service_client.rb class PaymentServiceClient include HTTParty base_uri ENV.fetch('PAYMENT_SERVICE_URL') def initialize @options = { headers: { 'Content-Type' => 'application/json', 'X-Service-Token' => ENV.fetch('SERVICE_TOKEN') }, timeout: 10 } end def create_charge(amount:, currency:, source:, metadata: {}) response = self.class.post('/charges', @options.merge( body: { amount: amount, currency: currency, source: source, metadata: metadata }.to_json )) handle_response(response) end def get_charge(charge_id) response = self.class.get("/charges/#{charge_id}", @options) handle_response(response) end private def handle_response(response) case response.code when 200..299 ServiceResult.success(response.parsed_response) when 400..499 ServiceResult.failure(response.parsed_response['error'], code: response.code) else ServiceResult.failure('サービスは利用できません', code: response.code) end rescue Net::OpenTimeout, Net::ReadTimeout ServiceResult.failure('サービスタイムアウト') end end ``` ```ruby # Sidekiq/Redis を使ったイベント駆動通信 # app/events/order_events.rb module OrderEvents class Created include Wisper::Publisher def call(order) broadcast(:order_created, order) end end end # app/listeners/inventory_listener.rb class InventoryListener def order_created(order) order.items.each do |item| InventoryServiceClient.new.reserve_stock( product_id: item.product_id, quantity: item.quantity, reference: order.id ) end end end # config/initializers/wisper.rb Wisper.subscribe(InventoryListener.new, async: true) Wisper.subscribe(NotificationListener.new, async: true) ``` ```ruby # API ゲートウェイパターン # app/controllers/api/v1/gateway_controller.rb module Api module V1 class GatewayController < BaseController # 複数サービスを集約 def dashboard results = Parallel.map([:orders, :inventory, :analytics], in_threads: 3) do |service| fetch_from_service(service) end render json: { orders: results[0], inventory: results[1], analytics: results[2] } end private def fetch_from_service(service) case service when :orders OrderServiceClient.new.recent_orders(limit: 5) when :inventory InventoryServiceClient.new.low_stock_alerts when :analytics AnalyticsServiceClient.new.daily_summary end rescue => e { error: "#{service} は利用できません", message: e.message } end end end end ``` Rails のマイクロサービスでは、明確な API 契約 (OpenAPI) の定義、サーキットブレーカ (gem `circuitbox`) の導入、分散トレーシング (gem `opentelemetry`) の活用を行いましょう。 ### 質問24: Rails アプリケーションを本番環境にデプロイするには? 近年の Rails デプロイはコンテナや PaaS を利用します。堅牢な本番設定はアセット、データベース、監視を網羅します。 ```ruby # config/environments/production.rb Rails.application.configure do config.cache_classes = true config.eager_load = true config.consider_all_requests_local = false # アセット config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? config.assets.compile = false config.assets.digest = true # ロギング config.log_level = ENV.fetch('LOG_LEVEL', 'info').to_sym config.log_tags = [:request_id] config.logger = ActiveSupport::Logger.new(STDOUT) .tap { |logger| logger.formatter = Logger::Formatter.new } .then { |logger| ActiveSupport::TaggedLogging.new(logger) } # キャッシュ config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'], expires_in: 1.day } # SSL の強制 config.force_ssl = true config.ssl_options = { hsts: { subdomains: true } } # Action Mailer config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: ENV['SMTP_HOST'], port: ENV['SMTP_PORT'], user_name: ENV['SMTP_USER'], password: ENV['SMTP_PASSWORD'], authentication: :plain, enable_starttls_auto: true } end ``` ```dockerfile # Dockerfile FROM ruby:3.3-alpine AS builder RUN apk add --no-cache build-base postgresql-dev nodejs yarn WORKDIR /app COPY Gemfile Gemfile.lock ./ RUN bundle config set --local deployment true && \ bundle config set --local without 'development test' && \ bundle install COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile COPY . . RUN bundle exec rails assets:precompile # 本番イメージ FROM ruby:3.3-alpine RUN apk add --no-cache postgresql-client tzdata WORKDIR /app COPY --from=builder /app /app COPY --from=builder /usr/local/bundle /usr/local/bundle ENV RAILS_ENV=production ENV RAILS_LOG_TO_STDOUT=true EXPOSE 3000 CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"] ``` ```yaml # docker-compose.production.yml version: '3.8' services: web: build: . environment: - DATABASE_URL=postgres://user:pass@db/app_production - REDIS_URL=redis://redis:6379/0 - SECRET_KEY_BASE=${SECRET_KEY_BASE} depends_on: - db - redis deploy: replicas: 3 resources: limits: memory: 512M sidekiq: build: . command: bundle exec sidekiq environment: - DATABASE_URL=postgres://user:pass@db/app_production - REDIS_URL=redis://redis:6379/0 depends_on: - db - redis deploy: replicas: 2 db: image: postgres:15-alpine volumes: - postgres_data:/var/lib/postgresql/data environment: - POSTGRES_PASSWORD=${DB_PASSWORD} redis: image: redis:7-alpine volumes: - redis_data:/data volumes: postgres_data: redis_data: ``` 本番チェックリスト: SSL 必須、ENV による秘密情報、ヘルスチェック、自動 DB バックアップ、監視 (APM + ログ + メトリクス)、アラート設定です。 ### 質問25: 知っておくべき Rails 7+ の新機能は? Rails 7+ は重要な変更をもたらしました: 既定の Hotwire、import map、強化された暗号化資格情報、多数の最適化です。 ```ruby # Hotwire - Turbo Frames # app/views/articles/index.html.erb <%= turbo_frame_tag "articles" do %> <% @articles.each do |article| %> <%= turbo_frame_tag dom_id(article) do %> <%= render article %> <% end %> <% end %> <%= link_to "もっと読み込む", articles_path(page: @page + 1), data: { turbo_frame: "articles" } %> <% end %> # リアルタイム更新のための Turbo Streams # app/controllers/comments_controller.rb def create @comment = @article.comments.create!(comment_params.merge(user: current_user)) respond_to do |format| format.turbo_stream format.html { redirect_to @article } end end # app/views/comments/create.turbo_stream.erb <%= turbo_stream.append "comments", @comment %> <%= turbo_stream.update "comments_count", @article.comments.count %> <%= turbo_stream.replace "comment_form", partial: "comments/form", locals: { comment: Comment.new } %> ``` ```ruby # Stimulus コントローラー # app/javascript/controllers/search_controller.js import { Controller } from "@hotwired/stimulus" import { debounce } from "lodash-es" export default class extends Controller { static targets = ["input", "results"] static values = { url: String } connect() { this.search = debounce(this.search.bind(this), 300) } async search() { const query = this.inputTarget.value if (query.length < 2) return const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`) this.resultsTarget.innerHTML = await response.text() } } ``` ```ruby # Import Map (JavaScript バンドラー不要) # config/importmap.rb pin "application" pin "@hotwired/turbo-rails", to: "turbo.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin_all_from "app/javascript/controllers", under: "controllers" # CDN 経由のピン pin "lodash-es", to: "https://ga.jspm.io/npm:lodash-es@4.17.21/lodash.js" ``` ```ruby # Active Record Encryption (Rails 7+) # app/models/user.rb class User < ApplicationRecord encrypts :email, deterministic: true # 検索を許可 encrypts :phone_number # 既定では非決定的 encrypts :ssn, deterministic: true, downcase: true end # config/credentials.yml.enc active_record_encryption: primary_key: abc123... deterministic_key: def456... key_derivation_salt: ghi789... ``` ```ruby # クエリインターフェイスの改善 # Rails 7.1+ # 非同期クエリ users = User.where(active: true).load_async # クエリ実行中も処理を継続できる # 結果は users.to_a でアクセス # Common Table Expressions (CTE) User.with( recent_orders: Order.where('created_at > ?', 30.days.ago) ).joins('JOIN recent_orders ON recent_orders.user_id = users.id') # inverse_of の自動検出 class Author < ApplicationRecord has_many :books # inverse_of は自動検出 end # 既定で strict loading (N+1 を防止) class ApplicationRecord < ActiveRecord::Base self.strict_loading_by_default = true end ``` Rails 7+ はシンプルさ (既定で Webpack なし) と Hotwire による HTML-over-the-wire を志向します。このアプローチは JavaScript の複雑さを抑えながら、モダンな UX を提供します。 ## まとめ Ruby on Rails の面接ではフレームワーク全体の理解と規約への深い理解が問われます。覚えておきたい主要ポイント: ✅ **基礎**: MVC、Active Record、マイグレーション、バリデーション、関連 ✅ **アーキテクチャ**: Service Object、Concern、Query Object、CQRS パターン ✅ **パフォーマンス**: N+1 クエリ、キャッシュ (フラグメント、ロシアンドール、ローレベル)、eager loading ✅ **テスト**: RSpec、FactoryBot、リクエスト spec、テストのベストプラクティス ✅ **セキュリティ**: CSRF、SQLインジェクション、XSS、Strong Parameters、認証/認可 ✅ **API**: RESTful 設計、JWT、シリアライザ、バージョニング ✅ **本番**: バックグラウンドジョブ、WebSocket、デプロイ、監視 Rails の哲学 (Convention over Configuration、DRY、Rails Way) はあらゆるアーキテクチャ判断を導きます。これらの原則を体得し、いつ逸脱すべきかを見極められれば、確かな専門性を示せます。 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/ruby-on-rails/ruby-on-rails-interview-questions