# 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. - Published: 2026-02-03 - Updated: 2026-03-28 - Author: SharpSkill - Tags: ruby on rails, ruby, interview, active record, entretien technique - Reading time: 22 min --- 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. > **Conseil pour l'entretien** > > 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. ```ruby # 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 ``` ```ruby # 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 ``` ```erb <%# app/views/articles/show.html.erb %> <%# La View affiche les données au format HTML %>

<%= @article.title %>

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

<%= simple_format @article.body %>
<%# Partial pour les commentaires %> <%= render @comments %>
``` 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. ```ruby # 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 ``` ```ruby # 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) end ``` Active 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. ```ruby # 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 ``` ```ruby # 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 ``` ```bash # 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, seed ``` Les 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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é. ```ruby # 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 ``` ```ruby # 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. ## 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. ```ruby # 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 ``` ```bash # 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#drafts ``` Les 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. ```ruby # 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 ``` ```ruby # 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 end ``` Les 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. ```ruby # 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 ``` ```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: '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 end ``` Le 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Les 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. ```ruby # 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 ``` ```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 '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 ``` ```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 '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 end ``` Les 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. ```ruby # 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 ``` ```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 # 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) } end ``` Pré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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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) ``` ```ruby # 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: CleanupJob ``` Active Job abstrait le backend, mais accéder aux fonctionnalités spécifiques (batches, rate limiting) nécessite souvent de coupler au backend choisi. ## 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. ```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: '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 ``` ```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 # 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 end ``` Les 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é. ```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, 'Token has expired' rescue JWT::DecodeError raise AuthenticationError, 'Invalid token' 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: '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 ``` ```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, '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 end ``` Pour 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. ```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 %> <%# 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 %>

<%= @category.name %>

<% @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 %> ``` ```ruby # 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 ``` ```ruby # 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Outils 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. ```ruby # 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 ``` ```ruby # 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) ``` ```ruby # 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] %> ``` ```ruby # 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 ``` ```ruby # 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:' end ``` Auditer 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. ```ruby # 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 ``` ```ruby # 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 ``` ```ruby # 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 end ``` Pundit 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. ```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 # 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 end ``` Le 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. ```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, "Product #{item[:product_id]} not available") 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 # 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 end ``` CQRS 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. ```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 # 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 ``` ```ruby # 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 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 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. ```ruby # 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 ``` ```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 # 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 ``` ```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 # Les admins peuvent appartenir à plusieurs tenants has_many :tenant_memberships has_many :accessible_tenants, through: :tenant_memberships, source: :tenant end ``` ```ruby # 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 end ``` Le 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. ```ruby # 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 ``` ```ruby # 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) ``` ```ruby # 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 end ``` Pour 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. ```ruby # 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 # 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 --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: ``` 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. ```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 "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 } %> ``` ```ruby # 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() } } ``` ```ruby # 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" ``` ```ruby # 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... ``` ```ruby # 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 end ``` Rails 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/fr/blog/ruby-on-rails/questions-entretien-ruby-on-rails