Questions d'entretien Ruby on Rails : Top 25 en 2026
Les 25 questions d'entretien Ruby on Rails les plus posées. Architecture MVC, Active Record, migrations, tests RSpec, API REST avec réponses détaillées et exemples de code.

Les entretiens Ruby on Rails évaluent la maîtrise du framework Ruby le plus populaire, la compréhension de l'architecture MVC, l'ORM Active Record, et la capacité à construire des applications web robustes suivant la philosophie "Convention over Configuration". Ce guide couvre les 25 questions les plus posées, des fondamentaux Rails jusqu'aux patterns avancés de production.
Les recruteurs apprécient les candidats qui comprennent la philosophie Rails : "Convention over Configuration", DRY (Don't Repeat Yourself), et les Rails Way patterns. Expliquer pourquoi Rails fait certains choix architecturaux fait la différence.
Fondamentaux Ruby on Rails
Question 1 : Expliquez le pattern MVC dans Ruby on Rails
Le pattern Model-View-Controller (MVC) est le cœur architectural de Rails. Il sépare les responsabilités en trois couches distinctes pour une meilleure maintenabilité et testabilité du code.
# app/models/article.rb
# Le Model gère les données et la logique métier
class Article < ApplicationRecord
# Validations des données
validates :title, presence: true, length: { minimum: 5 }
validates :body, presence: true
# Associations avec d'autres modèles
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
has_many :tags, through: :article_tags
# Scopes pour les requêtes réutilisables
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
# Callbacks du cycle de vie
before_save :generate_slug
private
def generate_slug
self.slug = title.parameterize if title_changed?
end
end# app/controllers/articles_controller.rb
# Le Controller reçoit les requêtes et orchestre la réponse
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: 'Article créé avec succès.'
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<%# app/views/articles/show.html.erb %>
<%# La View affiche les données au format HTML %>
<article class="article-detail">
<header>
<h1><%= @article.title %></h1>
<p class="meta">
Par <%= @article.author.name %> •
<%= l @article.created_at, format: :long %>
</p>
</header>
<div class="content">
<%= simple_format @article.body %>
</div>
<%# Partial pour les commentaires %>
<%= render @comments %>
</article>Le flux typique : la requête arrive au Router, qui dispatche vers le Controller approprié. Le Controller interagit avec le Model pour récupérer ou modifier les données, puis passe ces données à la View pour le rendu HTML.
Question 2 : Qu'est-ce qu'Active Record et comment fonctionne l'ORM Rails ?
Active Record est l'ORM (Object-Relational Mapping) de Rails qui implémente le pattern Active Record. Chaque classe Model représente une table de base de données, et chaque instance représente une ligne.
# app/models/user.rb
# Active Record mappe automatiquement les colonnes aux attributs
class User < ApplicationRecord
# La table 'users' est automatiquement associée
# Colonnes: id, email, name, created_at, updated_at
has_secure_password # BCrypt pour le mot de passe
has_many :articles, foreign_key: :author_id
has_one :profile, dependent: :destroy
has_and_belongs_to_many :roles
# Validations
validates :email, presence: true,
uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
# Callbacks
before_save :normalize_email
# Méthodes de classe pour les requêtes
def self.admins
joins(:roles).where(roles: { name: 'admin' })
end
private
def normalize_email
self.email = email.downcase.strip
end
end# Exemples de requêtes Active Record
# Console Rails ou dans un service
# Création
user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123')
# Lecture avec conditions
active_users = User.where(active: true).order(:name)
user = User.find_by(email: 'dev@example.com')
# Requêtes chaînées (lazy evaluation)
recent_admins = User.admins
.where('created_at > ?', 1.month.ago)
.includes(:profile)
.limit(10)
# N+1 prevention avec eager loading
articles = Article.includes(:author, :comments).published
# Mise à jour
user.update!(name: 'Alice Martin')
# Transactions
User.transaction do
user.debit_balance!(100)
recipient.credit_balance!(100)
Payment.create!(from: user, to: recipient, amount: 100)
endActive Record convertit les méthodes Ruby en requêtes SQL optimisées. Les méthodes comme where, joins, includes sont paresseuses (lazy) - la requête n'est exécutée qu'au moment de l'itération ou de l'appel à to_a.
Question 3 : Expliquez le système de migrations Rails
Les migrations permettent de versionner le schéma de base de données avec Ruby. Elles sont réversibles et permettent une évolution contrôlée de la structure de données.
# db/migrate/20260203100000_create_products.rb
# Migration pour créer une table
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 et updated_at automatiques
end
# Index pour les performances
add_index :products, :name
add_index :products, [:category_id, :active]
end
end# db/migrate/20260203110000_add_slug_to_products.rb
# Migration pour modifier une table existante
class AddSlugToProducts < ActiveRecord::Migration[7.1]
def change
add_column :products, :slug, :string
add_index :products, :slug, unique: true
# Remplir les slugs existants
reversible do |dir|
dir.up do
Product.find_each do |product|
product.update_column(:slug, product.name.parameterize)
end
end
end
# Rendre non-nullable après remplissage
change_column_null :products, :slug, false
end
end# Commandes de migration essentielles
rails db:migrate # Exécuter les migrations pending
rails db:rollback # Annuler la dernière migration
rails db:rollback STEP=3 # Annuler les 3 dernières migrations
rails db:migrate:status # Voir le statut des migrations
rails db:seed # Exécuter db/seeds.rb
rails db:reset # Drop, create, migrate, seedLes migrations doivent être réversibles. La méthode change est intelligente et peut inverser automatiquement les opérations courantes. Pour les cas complexes, utiliser up et down séparément.
Active Record Avancé
Question 4 : Comment optimiser les requêtes N+1 dans Rails ?
Le problème N+1 survient quand une requête initiale est suivie de N requêtes additionnelles pour charger les associations. Rails propose plusieurs méthodes d'eager loading pour résoudre ce problème.
# app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def index
# ❌ PROBLÈME N+1 : 1 requête + N requêtes par order
# @orders = Order.all
# Dans la view : order.user.name génère une requête par order
# ✅ SOLUTION avec includes (eager loading)
@orders = Order.includes(:user, :items)
.where(status: 'completed')
.order(created_at: :desc)
# Génère seulement 3 requêtes total
end
def show
# includes : charge les associations séparément (2-3 requêtes)
@order = Order.includes(items: :product).find(params[:id])
# preload : force le chargement séparé
@order = Order.preload(:items, :user).find(params[:id])
# eager_load : force un LEFT OUTER JOIN (1 requête)
@order = Order.eager_load(:items).find(params[:id])
end
end# app/models/order.rb
class Order < ApplicationRecord
belongs_to :user
has_many :items, class_name: 'OrderItem'
has_many :products, through: :items
# Scope avec includes par défaut
scope :with_details, -> { includes(:user, items: :product) }
# Counter cache pour éviter COUNT queries
# Nécessite: add_column :users, :orders_count, :integer, default: 0
belongs_to :user, counter_cache: true
end# Détection N+1 avec Bullet gem (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 affichera des alertes quand :
# - Une requête N+1 est détectée
# - Un eager loading inutile est présent
# - Un counter cache devrait être utiliséLa règle : utiliser includes par défaut (Rails choisit la stratégie optimale), preload quand on veut forcer des requêtes séparées, eager_load quand on filtre sur les associations.
Question 5 : Expliquez les scopes et les query objects dans Rails
Les scopes encapsulent des conditions de requête réutilisables. Pour des requêtes complexes, les Query Objects offrent une meilleure organisation et testabilité.
# app/models/product.rb
class Product < ApplicationRecord
# Scopes simples
scope :active, -> { where(active: true) }
scope :in_stock, -> { where('stock_quantity > 0') }
scope :featured, -> { where(featured: true) }
# Scopes avec paramètres
scope :cheaper_than, ->(price) { where('price < ?', price) }
scope :in_category, ->(category) { where(category: category) }
# Scopes chaînables
scope :available, -> { active.in_stock }
# Scope avec jointures
scope :with_recent_orders, -> {
joins(:order_items)
.where('order_items.created_at > ?', 30.days.ago)
.distinct
}
# Scope avec sous-requête
scope :bestsellers, -> {
where(id: OrderItem.group(:product_id)
.order('COUNT(*) DESC')
.limit(10)
.select(:product_id))
}
end# app/queries/products_search_query.rb
# Query Object pour recherches complexes
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
# Utilisation dans le controller
@products = ProductsSearchQuery.new(Product.active).call(params)Les scopes sont parfaits pour des conditions simples et réutilisables. Les Query Objects conviennent aux recherches complexes avec plusieurs filtres optionnels et une logique de composition.
Prêt à réussir tes entretiens Ruby on Rails ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Routing et Controllers
Question 6 : Comment fonctionne le routing RESTful dans Rails ?
Rails encourage les routes RESTful qui mappent les verbes HTTP aux actions CRUD. Le router traduit les URLs en appels de controller spécifiques.
# config/routes.rb
Rails.application.routes.draw do
# Routes RESTful standard (7 actions)
resources :articles do
# Routes imbriquées
resources :comments, only: [:create, :destroy]
# Routes membres (agissent sur une instance)
member do
post :publish
delete :archive
end
# Routes collection (agissent sur la collection)
collection do
get :drafts
get :search
end
end
# Routes API avec namespace
namespace :api do
namespace :v1 do
resources :products, only: [:index, :show, :create, :update] do
resources :reviews, shallow: true
end
end
end
# Route personnalisée
get 'dashboard', to: 'dashboard#index'
# Contraintes sur les routes
constraints(SubdomainConstraint.new) do
resources :admin_settings
end
# Route racine
root 'home#index'
end# rails routes - Affiche toutes les routes générées
#
# 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#draftsLes helpers de route générés (article_path(@article), new_article_path) permettent de référencer les URLs de manière dynamique et maintenable.
Question 7 : Expliquez les callbacks et filters dans les controllers
Les callbacks (before_action, after_action, around_action) permettent d'exécuter du code avant, après ou autour des actions du controller.
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Protection CSRF activée par défaut
protect_from_forgery with: :exception
# Callback global pour l'authentification
before_action :authenticate_user!
# Gestion des erreurs globale
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::ParameterMissing, with: :bad_request
private
def not_found
render json: { error: 'Resource not found' }, status: :not_found
end
def bad_request(exception)
render json: { error: exception.message }, status: :bad_request
end
end# app/controllers/admin/products_controller.rb
class Admin::ProductsController < ApplicationController
# Callbacks avec options
before_action :require_admin
before_action :set_product, only: [:show, :edit, :update, :destroy]
after_action :log_activity, only: [:create, :update, :destroy]
# Callback conditionnel
before_action :check_stock, only: [:update], if: :stock_changed?
def create
@product = Product.new(product_params)
if @product.save
redirect_to [:admin, @product], notice: 'Produit créé.'
else
render :new, status: :unprocessable_entity
end
end
def update
if @product.update(product_params)
redirect_to [:admin, @product], notice: 'Produit mis à jour.'
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
endLes callbacks s'exécutent dans l'ordre de déclaration. Utiliser skip_before_action dans les sous-classes pour désactiver un callback hérité. Éviter les callbacks avec trop de logique métier - préférer les Service Objects.
Services et Architecture
Question 8 : Comment implémenter des Service Objects dans Rails ?
Les Service Objects encapsulent la logique métier complexe qui ne appartient ni au Model ni au Controller. Ils améliorent la testabilité et respectent le principe de responsabilité unique.
# app/services/order_processor.rb
# Service Object avec interface standardisée
class OrderProcessor
def initialize(order, payment_method:)
@order = order
@payment_method = payment_method
end
def call
return failure('Order already processed') 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("Payment failed: #{e.message}")
rescue InsufficientStockError => e
failure("Stock insufficient: #{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 ##{@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# 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: 'Commande confirmée!'
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
endLe pattern Service Object suit une convention simple : une classe, une responsabilité, une méthode publique call. Le retour d'un objet Result permet une gestion propre des succès et échecs.
Question 9 : Expliquez les Concerns dans Rails
Les Concerns permettent d'extraire et partager du code entre Models ou Controllers. Ils utilisent ActiveSupport::Concern pour une syntaxe propre d'inclusion.
# app/models/concerns/sluggable.rb
# Concern réutilisable pour générer des slugs
module Sluggable
extend ActiveSupport::Concern
included do
# Code exécuté à l'inclusion
before_validation :generate_slug, if: :should_generate_slug?
validates :slug, presence: true, uniqueness: true
end
# Méthodes de classe
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
# Méthodes d'instance
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# app/models/article.rb
class Article < ApplicationRecord
include Sluggable
sluggable_source :title # Optionnel, :title par défaut
end
# app/models/product.rb
class Product < ApplicationRecord
include Sluggable
sluggable_source :name
end# app/controllers/concerns/pagination.rb
# Concern pour les controllers
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
endLes Concerns sont utiles pour le code véritablement partagé. Éviter de créer des Concerns pour simplement "raccourcir" un Model - cela masque la complexité sans la réduire.
Tests avec RSpec
Question 10 : Comment structurer les tests RSpec dans Rails ?
RSpec est le framework de test standard pour Rails. Une bonne structure de tests inclut les specs de Models, Controllers, Services, et les tests d'intégration.
# spec/models/user_spec.rb
require 'rails_helper'
RSpec.describe User, type: :model do
# Factories avec 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 'validates email format' 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 'returns first and last name combined' do
user = build(:user, first_name: 'John', last_name: 'Doe')
expect(user.full_name).to eq('John Doe')
end
it 'handles missing last name' do
user = build(:user, first_name: 'John', last_name: nil)
expect(user.full_name).to eq('John')
end
end
describe '.active' do
it 'returns only active users' 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# 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 'when order is valid' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: true, transaction_id: 'txn_123')
)
end
it 'processes the order successfully' do
result = subject.call
expect(result).to be_success
expect(order.reload.status).to eq('completed')
end
it 'decrements product stock' do
expect { subject.call }.to change { product.reload.stock_quantity }.by(-2)
end
it 'sends confirmation email' do
expect { subject.call }
.to have_enqueued_mail(OrderMailer, :confirmation)
.with(order)
end
end
context 'when payment fails' do
before do
allow(PaymentGateway).to receive(:charge).and_return(
OpenStruct.new(success?: false, error: 'Card declined')
)
end
it 'returns failure result' do
result = subject.call
expect(result).to be_failure
expect(result.error).to include('Card declined')
end
it 'does not update order status' do
expect { subject.call }.not_to change { order.reload.status }
end
end
end
end# 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 'returns list of products' do
get '/api/v1/products', headers: headers
expect(response).to have_http_status(:ok)
expect(json_response['data'].size).to eq(3)
end
it 'filters by category' 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: 'New Product', price: 99.99, category_id: create(:category).id } }
end
it 'creates a new product' 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
endLes bonnes pratiques : utiliser let pour les données, describe pour les méthodes/contextes, context pour les conditions, et it pour les assertions spécifiques. Un test doit tester une seule chose.
Question 11 : Comment utiliser les Factories avec FactoryBot ?
FactoryBot permet de créer des données de test de manière déclarative et maintenable. Les factories remplacent les fixtures statiques.
# spec/factories/users.rb
FactoryBot.define do
factory :user do
# Séquences pour l'unicité
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 }
# Traits pour les variations
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 héritée
factory :admin_user do
admin
with_profile
end
end
end# 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# Utilisation dans les tests
RSpec.describe OrderProcessor do
# build : instance non sauvegardée
let(:user) { build(:user) }
# create : instance sauvegardée en DB
let(:order) { create(:order, :with_items, user: user) }
# create_list : plusieurs instances
let(:products) { create_list(:product, 5) }
# Combinaison de traits
let(:admin) { create(:user, :admin, :with_profile) }
# Override d'attributs
let(:expensive_order) { create(:order, :with_items, items_count: 10) }
# build_stubbed : plus rapide, pour les tests unitaires
let(:stubbed_user) { build_stubbed(:user) }
endPréférer build ou build_stubbed à create quand la persistence n'est pas nécessaire - cela accélère significativement les tests.
Background Jobs
Question 12 : Comment utiliser Active Job et Sidekiq dans Rails ?
Active Job fournit une interface unifiée pour les background jobs, indépendamment du backend (Sidekiq, Resque, etc.). Sidekiq est le choix populaire pour sa performance avec Redis.
# app/jobs/process_order_job.rb
class ProcessOrderJob < ApplicationJob
queue_as :default
# Retry configuration
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10
discard_on ActiveJob::DeserializationError
# Sidekiq options (si Sidekiq backend)
sidekiq_options retry: 5, backtrace: true
def perform(order_id)
order = Order.find(order_id)
OrderProcessor.new(order).call
rescue ActiveRecord::RecordNotFound
# Order supprimée entre l'enqueue et l'exécution
Rails.logger.warn("Order #{order_id} not found, skipping job")
end
end# app/jobs/batch_email_job.rb
class BatchEmailJob < ApplicationJob
queue_as :mailers
# Rate limiting avec Sidekiq Enterprise ou gem throttle
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# Enqueuing jobs
# Immédiat
ProcessOrderJob.perform_later(order.id)
# Différé
ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id)
# À une heure spécifique
ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)
# Queue spécifique
ProcessOrderJob.set(queue: :critical).perform_later(order.id)
# Synchrone (pour les tests ou debugging)
ProcessOrderJob.perform_now(order.id)# config/sidekiq.yml
:concurrency: 10
:queues:
- [critical, 3] # Priorité haute, poids 3
- [default, 2] # Priorité moyenne, poids 2
- [mailers, 1] # Priorité basse, poids 1
- [low, 1]
:schedule:
cleanup_job:
cron: '0 3 * * *' # Tous les jours à 3h
class: CleanupJobActive Job abstrait le backend, mais accéder aux fonctionnalités spécifiques (batches, rate limiting) nécessite souvent de coupler au backend choisi.
Prêt à réussir tes entretiens Ruby on Rails ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
API Development
Question 13 : Comment construire une API RESTful avec Rails ?
Rails facilite la construction d'APIs JSON avec les Controllers API-only et les serializers. Une bonne API est versionnée, documentée et sécurisée.
# 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: 'Resource not found', details: exception.message },
status: :not_found
end
def unprocessable_entity(exception)
render json: { error: 'Validation failed', details: exception.record.errors },
status: :unprocessable_entity
end
def bad_request(exception)
render json: { error: 'Bad request', details: exception.message },
status: :bad_request
end
end
end
end# 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# app/serializers/product_serializer.rb
# Avec 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
endLes bonnes pratiques API : versionner via namespace, utiliser les codes HTTP appropriés, paginer les collections, et fournir des messages d'erreur clairs.
Question 14 : Comment implémenter l'authentification JWT dans Rails ?
JWT (JSON Web Tokens) est une méthode stateless d'authentification populaire pour les APIs. Le token encode l'identité de l'utilisateur et sa validité.
# 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, 'Token has expired'
rescue JWT::DecodeError
raise AuthenticationError, 'Invalid token'
end
end
end# 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: 'Invalid credentials' }, 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# 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, 'Missing token' 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: 'User not found' }, status: :unauthorized
end
def current_user
@current_user
end
endPour la production, considérer : refresh tokens, token blacklisting pour logout, et des durées de vie courtes. Des gems comme devise-jwt simplifient l'implémentation.
Caching et Performance
Question 15 : Comment implémenter le caching dans Rails ?
Rails offre plusieurs niveaux de caching : fragment caching, Russian Doll caching, low-level caching. Le choix dépend du cas d'usage.
# 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
}<%# app/views/products/index.html.erb %>
<%# Fragment caching avec cache key automatique %>
<% @products.each do |product| %>
<%# Cache basé sur updated_at du produit %>
<% cache product do %>
<%= render product %>
<% end %>
<% end %>
<%# Russian Doll caching - cache imbriqué %>
<% cache ['v1', @category] do %>
<h2><%= @category.name %></h2>
<% @category.products.each do |product| %>
<% cache ['v1', product] do %>
<%= render product %>
<% end %>
<% end %>
<% end %>
<%# Cache conditionnel %>
<% cache_if current_user.nil?, @product do %>
<%= render @product %>
<% end %># app/models/product.rb
class Product < ApplicationRecord
# Touch parent pour invalider le cache Russian Doll
belongs_to :category, touch: true
# Cache key personnalisé
def cache_key_with_version
"#{super}/#{reviews.maximum(:updated_at)&.to_i}"
end
end# Low-level caching dans les services
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
# Cache avec race condition protection
Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do
Product.bestsellers.limit(10).to_a
end
# Invalidation explicite
Rails.cache.delete('dashboard:stats')
Rails.cache.delete_matched('products:*')Le Russian Doll caching est efficace car seuls les fragments modifiés sont régénérés. Utiliser touch: true sur les associations pour propager l'invalidation.
Question 16 : Comment optimiser les performances d'une application Rails ?
L'optimisation Rails couvre plusieurs aspects : requêtes DB, caching, assets, et architecture. Une approche méthodique avec monitoring est essentielle.
# Database optimization
# config/database.yml
production:
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
prepared_statements: true
advisory_locks: true
# app/models/order.rb
class Order < ApplicationRecord
# Index composés pour les requêtes fréquentes
# add_index :orders, [:user_id, :status, :created_at]
# Select only needed columns
scope :summary, -> { select(:id, :status, :total, :created_at) }
# Batch processing pour les gros volumes
def self.process_pending
pending.find_each(batch_size: 1000) do |order|
ProcessOrderJob.perform_later(order.id)
end
end
# Éviter les calculs répétitifs
def self.revenue_by_month
completed
.group("DATE_TRUNC('month', created_at)")
.sum(:total)
end
end# Memory optimization
# 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# Profiling avec 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# Lazy loading et pagination
class ProductsController < ApplicationController
def index
@products = Product.active
.includes(:category, :primary_image)
.page(params[:page])
.per(24)
# Prefetch pour la page suivante
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
endOutils essentiels : rack-mini-profiler pour le profiling, bullet pour les N+1, New Relic ou Scout pour le monitoring production.
Security
Question 17 : Quelles sont les bonnes pratiques de sécurité dans Rails ?
Rails inclut des protections par défaut contre les vulnérabilités courantes. Comprendre et configurer correctement ces protections est crucial.
# CSRF Protection
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Activé par défaut, génère une exception si token invalide
protect_from_forgery with: :exception
# Pour les APIs, utiliser :null_session
# protect_from_forgery with: :null_session
end
# Dans les vues, le token est inclus automatiquement dans les forms
# <%= form_with ... %> inclut authenticity_token
# Pour les requêtes AJAX
# Ajouter le header X-CSRF-Token avec la valeur de csrf_meta_tags# SQL Injection Prevention
# ✅ Paramètres interpolés automatiquement échappés
User.where('email = ?', params[:email])
User.where(email: params[:email])
# ❌ DANGER - Interpolation directe
User.where("email = '#{params[:email]}'")
# ✅ Pour les clauses ORDER dynamiques
ALLOWED_SORTS = %w[name created_at price].freeze
sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name'
Product.order(sort_column)# XSS Protection
# Rails échappe automatiquement le HTML dans les vues
# ✅ Échappé automatiquement
<%= user.name %>
# ❌ Dangereux - contenu non échappé
<%== user.bio %>
<%= raw user.bio %>
<%= user.bio.html_safe %>
# ✅ Pour le HTML sûr, utiliser sanitize
<%= sanitize user.bio, tags: %w[p br strong em] %># Strong Parameters
class UsersController < ApplicationController
def update
@user.update!(user_params)
end
private
def user_params
# Whitelist explicite des attributs autorisés
params.require(:user).permit(:name, :email, :avatar)
# Pour les admins uniquement
if current_user.admin?
params.require(:user).permit(:name, :email, :role, :active)
else
params.require(:user).permit(:name, :email)
end
end
end# Secure Headers
# 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:'
endAuditer régulièrement avec brakeman (analyse statique de sécurité) et maintenir les gems à jour avec bundle audit.
Question 18 : Comment gérer l'authentification et l'autorisation dans Rails ?
L'authentification vérifie l'identité, l'autorisation contrôle les permissions. Devise gère l'auth, Pundit ou CanCanCan gèrent l'autorisation.
# Devise setup
# 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# Pundit policies
# 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
# Scope pour les collections
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# Controller avec 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: 'Article updated.'
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: 'Article published.'
end
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
flash[:alert] = "You are not authorized to perform this action."
redirect_back(fallback_location: root_path)
end
endPundit est plus explicite et testable que CanCanCan. Chaque action a une policy method correspondante, et les scopes filtrent les collections automatiquement.
Rails Avancé
Question 19 : Expliquez le pattern Repository dans Rails
Le pattern Repository isole la logique d'accès aux données du reste de l'application. Bien que Rails utilise Active Record (pattern différent), Repository peut être utile pour les cas complexes.
# 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# 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# Utilisation dans un service
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
# Facilite le testing avec des mocks
RSpec.describe ProductSearchService do
let(:repository) { instance_double(ProductRepository) }
let(:service) { described_class.new(repository: repository) }
it 'filters by category' 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
endLe Repository est optionnel en Rails car Active Record est déjà un excellent pattern. L'utiliser pour des requêtes complexes ou quand l'isolation du storage est importante.
Question 20 : Comment implémenter le pattern CQRS dans Rails ?
CQRS (Command Query Responsibility Segregation) sépare les opérations de lecture et d'écriture. En Rails, cela se traduit par des classes distinctes pour les queries et les commands.
# 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# 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, "Product #{item[:product_id]} not available")
end
end
end
end
end# 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# Controller utilisant 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: 'Order created!'
else
flash.now[:alert] = result.errors.join(', ')
render :new, status: :unprocessable_entity
end
end
endCQRS brille pour les applications complexes avec des besoins de lecture/écriture asymétriques. Pour des CRUD simples, c'est une sur-ingénierie.
Question 21 : Comment gérer les WebSockets avec Action Cable ?
Action Cable intègre WebSockets dans Rails pour la communication temps réel bidirectionnelle. Il utilise Redis pour la synchronisation entre serveurs.
# 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
# Via cookie de session
if verified_user = User.find_by(id: cookies.encrypted[:user_id])
verified_user
# Via JWT pour les APIs
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# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
@room = ChatRoom.find(params[:room_id])
# Vérifier les permissions
unless @room.accessible_by?(current_user)
reject
return
end
stream_for @room
# Notifier les autres de la présence
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']
)
# Broadcast à tous les abonnés
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
endimport 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 gère automatiquement les reconnexions et la synchronisation. En production, configurer Redis comme adapter et dimensionner selon le nombre de connexions concurrentes.
Question 22 : Comment implémenter le multi-tenancy dans Rails ?
Le multi-tenancy permet à une application de servir plusieurs clients (tenants) isolés. Trois approches principales : database-level, schema-level, ou row-level.
# Row-level multitenancy avec ActsAsTenant ou manual
# app/models/concerns/tenant_scoped.rb
module TenantScoped
extend ActiveSupport::Concern
included do
belongs_to :tenant
# Scope par défaut au tenant courant
default_scope -> { where(tenant: Current.tenant) if Current.tenant }
# Validation du 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# 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
# Via subdomain
if request.subdomain.present? && request.subdomain != 'www'
Tenant.find_by!(subdomain: request.subdomain)
# Via header (pour les APIs)
elsif request.headers['X-Tenant-ID'].present?
Tenant.find(request.headers['X-Tenant-ID'])
# Via user
elsif current_user
current_user.tenant
end
rescue ActiveRecord::RecordNotFound
redirect_to root_url(subdomain: 'www'), alert: 'Tenant not found'
end
end# 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
# Les admins peuvent appartenir à plusieurs tenants
has_many :tenant_memberships
has_many :accessible_tenants, through: :tenant_memberships, source: :tenant
end# Schema-level avec Apartment gem (PostgreSQL)
# config/initializers/apartment.rb
Apartment.configure do |config|
config.excluded_models = %w[Tenant User]
config.tenant_names = -> { Tenant.pluck(:subdomain) }
end
# Utilisation
Apartment::Tenant.switch('acme') do
# Toutes les requêtes dans ce bloc utilisent le schema 'acme'
Project.all # SELECT * FROM acme.projects
endLe row-level est le plus simple mais requiert une attention constante aux leaks. Le schema-level offre une meilleure isolation mais complexifie les migrations. Choisir selon les besoins de sécurité et de scalabilité.
Question 23 : Comment mettre en place une architecture Microservices avec Rails ?
Rails peut servir de base pour une architecture microservices avec une communication via HTTP/gRPC ou message queues. La clé est de bien définir les boundaries.
# Service client 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('Service unavailable', code: response.code)
end
rescue Net::OpenTimeout, Net::ReadTimeout
ServiceResult.failure('Service timeout')
end
end# Event-driven communication avec 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)# API Gateway pattern
# app/controllers/api/v1/gateway_controller.rb
module Api
module V1
class GatewayController < BaseController
# Agrégation de plusieurs services
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} unavailable", message: e.message }
end
end
end
endPour les microservices Rails : définir des contrats d'API clairs (OpenAPI), implémenter des circuit breakers (gem circuitbox), et utiliser le distributed tracing (gem opentelemetry).
Question 24 : Comment déployer une application Rails en production ?
Le déploiement Rails moderne utilise des containers ou des PaaS. Une configuration production robuste couvre les assets, la base de données, et le monitoring.
# config/environments/production.rb
Rails.application.configure do
config.cache_classes = true
config.eager_load = true
config.consider_all_requests_local = false
# Assets
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
config.assets.compile = false
config.assets.digest = true
# Logging
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) }
# Cache
config.cache_store = :redis_cache_store, {
url: ENV['REDIS_URL'],
expires_in: 1.day
}
# Force 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
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
# Production image
FROM ruby:3.3-alpine
RUN apk add --no-cache postgresql-client tzdata
WORKDIR /app
COPY /app /app
COPY /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"]# 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:Checklist production : SSL obligatoire, secrets via ENV, health checks, backups DB automatisés, monitoring (APM + logs + metrics), et alerting configuré.
Question 25 : Quelles sont les nouveautés de Rails 7+ à connaître ?
Rails 7+ apporte des changements significatifs : Hotwire par défaut, import maps, encrypted credentials améliorés, et de nombreuses optimisations.
# 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 "Load more", articles_path(page: @page + 1),
data: { turbo_frame: "articles" } %>
<% end %>
# Turbo Streams pour les updates temps réel
# 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 } %># Stimulus controllers
# 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()
}
}# Import Maps (sans bundler 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"
# Pins depuis CDN
pin "lodash-es", to: "https://ga.jspm.io/npm:lodash-es@4.17.21/lodash.js"# Active Record Encryption (Rails 7+)
# app/models/user.rb
class User < ApplicationRecord
encrypts :email, deterministic: true # Permet les recherches
encrypts :phone_number # Non-déterministe par défaut
encrypts :ssn, deterministic: true, downcase: true
end
# config/credentials.yml.enc
active_record_encryption:
primary_key: abc123...
deterministic_key: def456...
key_derivation_salt: ghi789...# Query interface improvements
# Rails 7.1+
# Async queries
users = User.where(active: true).load_async
# Continue processing while query runs
# Access results with 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')
# Automatic inverse_of detection
class Author < ApplicationRecord
has_many :books # inverse_of détecté automatiquement
end
# Strict loading par défaut (évite N+1)
class ApplicationRecord < ActiveRecord::Base
self.strict_loading_by_default = true
endRails 7+ privilégie la simplicité (pas de Webpack par défaut) et le HTML-over-the-wire avec Hotwire. Cette approche réduit la complexité JavaScript tout en offrant une expérience utilisateur moderne.
Conclusion
Les entretiens Ruby on Rails évaluent la maîtrise du framework complet et la compréhension de ses conventions. Les points clés à retenir :
✅ Fondamentaux : MVC, Active Record, migrations, validations et associations
✅ Architecture : Service Objects, Concerns, Query Objects, et patterns CQRS
✅ Performance : N+1 queries, caching (fragment, Russian Doll, low-level), eager loading
✅ Testing : RSpec, FactoryBot, request specs, et bonnes pratiques de test
✅ Sécurité : CSRF, SQL injection, XSS, Strong Parameters, et authentification/autorisation
✅ APIs : RESTful design, JWT, serializers, et versioning
✅ Production : Background jobs, WebSockets, déploiement, et monitoring
La philosophie Rails - Convention over Configuration, DRY, et Rails Way - guide l'ensemble des décisions architecturales. Maîtriser ces principes et savoir quand s'en écarter démontre une expertise solide.
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

ActiveRecord : Optimiser les requêtes N+1 dans Ruby on Rails
Guide complet pour détecter et corriger les problèmes de requêtes N+1 dans Rails avec ActiveRecord. Includes, preload, eager_load et outils de détection.

Ruby on Rails 7 : Hotwire et Turbo pour des applications réactives
Guide complet sur Hotwire et Turbo dans Rails 7. Apprenez à créer des applications réactives sans écrire de JavaScript avec Turbo Drive, Frames et Streams.

Action Cable et WebSockets dans Rails : Guide Complet pour les Entretiens Techniques
Action Cable integre les WebSockets directement dans Rails. Ce guide approfondi couvre l'architecture des connexions, les channels, Solid Cable, Turbo Streams, le scaling avec Redis et les patterns de test pour les entretiens techniques.