# Domande colloquio Ruby on Rails: Top 25 nel 2026 > Le 25 domande più frequenti per i colloqui Ruby on Rails. Architettura MVC, Active Record, migration, testing RSpec, API REST con risposte dettagliate ed esempi di codice. - Published: 2026-02-03 - Updated: 2026-04-27 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, ruby, colloquio, active record, colloquio tecnico - Reading time: 22 min --- I colloqui Ruby on Rails valutano la padronanza del framework Ruby più popolare, la comprensione dell'architettura MVC, dell'ORM Active Record e la capacità di costruire applicazioni web robuste seguendo la filosofia "Convention over Configuration". Questa guida copre le 25 domande più frequenti, dai fondamenti di Rails ai pattern avanzati di produzione. > **Consiglio per il colloquio** > > I recruiter apprezzano i candidati che comprendono la filosofia Rails: "Convention over Configuration", DRY (Don't Repeat Yourself) e i pattern Rails Way. Spiegare perché Rails compie certe scelte architetturali fa la differenza. ## Fondamenti di Ruby on Rails ### Domanda 1: Spiegare il pattern MVC in Ruby on Rails Il pattern Model-View-Controller (MVC) è il cuore architetturale di Rails. Separa le responsabilità in tre livelli distinti per una migliore manutenibilità e testabilità del codice. ```ruby # app/models/article.rb # Il Model gestisce i dati e la logica di business class Article < ApplicationRecord # Validazioni dei dati validates :title, presence: true, length: { minimum: 5 } validates :body, presence: true # Associazioni con altri modelli belongs_to :author, class_name: 'User' has_many :comments, dependent: :destroy has_many :tags, through: :article_tags # Scope per query riutilizzabili scope :published, -> { where(published: true) } scope :recent, -> { order(created_at: :desc).limit(10) } # Callback del ciclo di vita before_save :generate_slug private def generate_slug self.slug = title.parameterize if title_changed? end end ``` ```ruby # app/controllers/articles_controller.rb # Il Controller riceve le richieste e orchestra la risposta 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: 'Articolo creato con successo.' 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 visualizza i dati in formato HTML %>

<%= @article.title %>

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

<%= simple_format @article.body %>
<%# Partial per i commenti %> <%= render @comments %>
``` Il flusso tipico: la richiesta arriva al Router, che la indirizza al Controller appropriato. Il Controller interagisce con il Model per recuperare o modificare i dati, poi passa questi dati alla View per il rendering HTML. ### Domanda 2: Cos'è Active Record e come funziona l'ORM di Rails? Active Record è l'ORM (Object-Relational Mapping) di Rails che implementa il pattern Active Record. Ogni classe Model rappresenta una tabella del database, e ogni istanza rappresenta una riga. ```ruby # app/models/user.rb # Active Record mappa automaticamente le colonne agli attributi class User < ApplicationRecord # La tabella 'users' è associata automaticamente # Colonne: id, email, name, created_at, updated_at has_secure_password # BCrypt per la password has_many :articles, foreign_key: :author_id has_one :profile, dependent: :destroy has_and_belongs_to_many :roles # Validazioni validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP } # Callback before_save :normalize_email # Metodi di classe per le query def self.admins joins(:roles).where(roles: { name: 'admin' }) end private def normalize_email self.email = email.downcase.strip end end ``` ```ruby # Esempi di query Active Record # Console Rails o all'interno di un service # Creazione user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123') # Lettura con condizioni active_users = User.where(active: true).order(:name) user = User.find_by(email: 'dev@example.com') # Query concatenate (lazy evaluation) recent_admins = User.admins .where('created_at > ?', 1.month.ago) .includes(:profile) .limit(10) # Prevenzione N+1 con eager loading articles = Article.includes(:author, :comments).published # Aggiornamento user.update!(name: 'Alice Martin') # Transazioni User.transaction do user.debit_balance!(100) recipient.credit_balance!(100) Payment.create!(from: user, to: recipient, amount: 100) end ``` Active Record converte i metodi Ruby in query SQL ottimizzate. Metodi come `where`, `joins`, `includes` sono lazy: la query viene eseguita solo durante l'iterazione o alla chiamata di `to_a`. ### Domanda 3: Spiegare il sistema di migration di Rails Le migration permettono di versionare lo schema del database con Ruby. Sono reversibili e consentono un'evoluzione controllata della struttura dei dati. ```ruby # db/migrate/20260203100000_create_products.rb # Migration per creare una tabella 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 e updated_at automatici end # Indici per le performance add_index :products, :name add_index :products, [:category_id, :active] end end ``` ```ruby # db/migrate/20260203110000_add_slug_to_products.rb # Migration per modificare una tabella esistente class AddSlugToProducts < ActiveRecord::Migration[7.1] def change add_column :products, :slug, :string add_index :products, :slug, unique: true # Popolare gli slug esistenti reversible do |dir| dir.up do Product.find_each do |product| product.update_column(:slug, product.name.parameterize) end end end # Rendere non nullabile dopo il popolamento change_column_null :products, :slug, false end end ``` ```bash # Comandi essenziali per le migration rails db:migrate # Esegue le migration in sospeso rails db:rollback # Annulla l'ultima migration rails db:rollback STEP=3 # Annulla le ultime 3 migration rails db:migrate:status # Visualizza lo stato delle migration rails db:seed # Esegue db/seeds.rb rails db:reset # Drop, create, migrate, seed ``` Le migration devono essere reversibili. Il metodo `change` è intelligente e può invertire automaticamente le operazioni comuni. Per casi complessi, usare `up` e `down` separatamente. ## Active Record avanzato ### Domanda 4: Come ottimizzare le query N+1 in Rails? Il problema N+1 si verifica quando una query iniziale è seguita da N query aggiuntive per caricare le associazioni. Rails offre diversi metodi di eager loading per risolvere questo problema. ```ruby # app/controllers/orders_controller.rb class OrdersController < ApplicationController def index # ❌ PROBLEMA N+1: 1 query + N query per ordine # @orders = Order.all # Nella view: order.user.name genera una query per ordine # ✅ SOLUZIONE con includes (eager loading) @orders = Order.includes(:user, :items) .where(status: 'completed') .order(created_at: :desc) # Genera solo 3 query in totale end def show # includes: carica le associazioni separatamente (2-3 query) @order = Order.includes(items: :product).find(params[:id]) # preload: forza il caricamento separato @order = Order.preload(:items, :user).find(params[:id]) # eager_load: forza un LEFT OUTER JOIN (1 query) @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 con includes di default scope :with_details, -> { includes(:user, items: :product) } # Counter cache per evitare query COUNT # Richiede: add_column :users, :orders_count, :integer, default: 0 belongs_to :user, counter_cache: true end ``` ```ruby # Rilevamento N+1 con la gem Bullet (sviluppo) # config/environments/development.rb config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.rails_logger = true end # Bullet mostrerà avvisi quando: # - Viene rilevata una query N+1 # - È presente eager loading non necessario # - Dovrebbe essere usato un counter cache ``` La regola: usare `includes` di default (Rails sceglie la strategia ottimale), `preload` quando si vogliono forzare query separate, `eager_load` quando si filtra sulle associazioni. ### Domanda 5: Spiegare Scope e Query Object in Rails Gli scope incapsulano condizioni di query riutilizzabili. Per query complesse, i Query Object offrono migliore organizzazione e testabilità. ```ruby # app/models/product.rb class Product < ApplicationRecord # Scope semplici scope :active, -> { where(active: true) } scope :in_stock, -> { where('stock_quantity > 0') } scope :featured, -> { where(featured: true) } # Scope con parametri scope :cheaper_than, ->(price) { where('price < ?', price) } scope :in_category, ->(category) { where(category: category) } # Scope concatenabili scope :available, -> { active.in_stock } # Scope con joins scope :with_recent_orders, -> { joins(:order_items) .where('order_items.created_at > ?', 30.days.ago) .distinct } # Scope con subquery 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 per ricerche complesse 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 # Utilizzo nel controller @products = ProductsSearchQuery.new(Product.active).call(params) ``` Gli scope sono perfetti per condizioni semplici e riutilizzabili. I Query Object si adattano a ricerche complesse con più filtri opzionali e logica di composizione. ## Routing e Controller ### Domanda 6: Come funziona il routing RESTful in Rails? Rails incoraggia rotte RESTful che mappano i verbi HTTP alle azioni CRUD. Il router traduce gli URL in chiamate specifiche al controller. ```ruby # config/routes.rb Rails.application.routes.draw do # Rotte RESTful standard (7 azioni) resources :articles do # Rotte annidate resources :comments, only: [:create, :destroy] # Rotte member (agiscono su un'istanza) member do post :publish delete :archive end # Rotte collection (agiscono sulla collezione) collection do get :drafts get :search end end # Rotte API con namespace namespace :api do namespace :v1 do resources :products, only: [:index, :show, :create, :update] do resources :reviews, shallow: true end end end # Rotta personalizzata get 'dashboard', to: 'dashboard#index' # Vincoli sulle rotte constraints(SubdomainConstraint.new) do resources :admin_settings end # Rotta principale root 'home#index' end ``` ```bash # rails routes - Mostra tutte le rotte generate # # 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 ``` Gli helper di rotta generati (`article_path(@article)`, `new_article_path`) permettono di referenziare gli URL in modo dinamico e manutenibile. ### Domanda 7: Spiegare callback e filter nei controller I callback (before_action, after_action, around_action) permettono di eseguire codice prima, dopo o intorno alle azioni del controller. ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Protezione CSRF abilitata di default protect_from_forgery with: :exception # Callback globale per l'autenticazione before_action :authenticate_user! # Gestione globale degli errori rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::ParameterMissing, with: :bad_request private def not_found render json: { error: 'Risorsa non trovata' }, 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 # Callback con opzioni before_action :require_admin before_action :set_product, only: [:show, :edit, :update, :destroy] after_action :log_activity, only: [:create, :update, :destroy] # Callback condizionale before_action :check_stock, only: [:update], if: :stock_changed? def create @product = Product.new(product_params) if @product.save redirect_to [:admin, @product], notice: 'Prodotto creato.' else render :new, status: :unprocessable_entity end end def update if @product.update(product_params) redirect_to [:admin, @product], notice: 'Prodotto aggiornato.' 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 ``` I callback vengono eseguiti nell'ordine di dichiarazione. Usare `skip_before_action` nelle sottoclassi per disabilitare callback ereditati. Evitare callback con troppa logica di business: preferire i Service Object. ## Service e architettura ### Domanda 8: Come implementare i Service Object in Rails? I Service Object incapsulano logica di business complessa che non appartiene né ai Model né ai Controller. Migliorano la testabilità e seguono il principio di singola responsabilità. ```ruby # app/services/order_processor.rb # Service Object con interfaccia standardizzata class OrderProcessor def initialize(order, payment_method:) @order = order @payment_method = payment_method end def call return failure('Ordine già elaborato') 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("Pagamento fallito: #{e.message}") rescue InsufficientStockError => e failure("Stock insufficiente: #{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: "Ordine ##{@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: 'Ordine confermato!' 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 ``` Il pattern Service Object segue una convenzione semplice: una classe, una responsabilità, un metodo pubblico `call`. Restituire un oggetto Result permette una gestione pulita di successo e fallimento. ### Domanda 9: Spiegare i Concern in Rails I Concern permettono di estrarre e condividere codice tra Model o Controller. Utilizzano `ActiveSupport::Concern` per una sintassi di inclusione pulita. ```ruby # app/models/concerns/sluggable.rb # Concern riutilizzabile per generare slug module Sluggable extend ActiveSupport::Concern included do # Codice eseguito all'inclusione before_validation :generate_slug, if: :should_generate_slug? validates :slug, presence: true, uniqueness: true end # Metodi di 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 # Metodi di istanza 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 # Opzionale, :title di default end # app/models/product.rb class Product < ApplicationRecord include Sluggable sluggable_source :name end ``` ```ruby # app/controllers/concerns/pagination.rb # Concern per i controller 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 ``` I Concern sono utili per codice realmente condiviso. Evitare di creare Concern solo per "accorciare" un Model: ciò nasconde la complessità senza ridurla. ## Testing con RSpec ### Domanda 10: Come strutturare i test RSpec in Rails? RSpec è il framework di testing standard per Rails. Una buona struttura di test include Model spec, Controller spec, Service spec e test di integrazione. ```ruby # spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do # Factory con 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 'valida il formato dell’email' 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 'restituisce nome e cognome combinati' do user = build(:user, first_name: 'John', last_name: 'Doe') expect(user.full_name).to eq('John Doe') end it 'gestisce il cognome mancante' do user = build(:user, first_name: 'John', last_name: nil) expect(user.full_name).to eq('John') end end describe '.active' do it 'restituisce solo gli utenti attivi' 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 'quando l’ordine è valido' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: true, transaction_id: 'txn_123') ) end it 'elabora l’ordine con successo' do result = subject.call expect(result).to be_success expect(order.reload.status).to eq('completed') end it 'decrementa lo stock del prodotto' do expect { subject.call }.to change { product.reload.stock_quantity }.by(-2) end it 'invia l’email di conferma' do expect { subject.call } .to have_enqueued_mail(OrderMailer, :confirmation) .with(order) end end context 'quando il pagamento fallisce' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: false, error: 'Card declined') ) end it 'restituisce un risultato di fallimento' do result = subject.call expect(result).to be_failure expect(result.error).to include('Card declined') end it 'non aggiorna lo stato dell’ordine' 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 'restituisce la lista dei prodotti' do get '/api/v1/products', headers: headers expect(response).to have_http_status(:ok) expect(json_response['data'].size).to eq(3) end it 'filtra per categoria' 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: 'Nuovo Prodotto', price: 99.99, category_id: create(:category).id } } end it 'crea un nuovo prodotto' 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 ``` Buone pratiche: usare `let` per i dati, `describe` per metodi/contesti, `context` per le condizioni e `it` per le asserzioni specifiche. Un test dovrebbe testare una sola cosa. ### Domanda 11: Come usare le factory con FactoryBot? FactoryBot permette di creare dati di test in modo dichiarativo e manutenibile. Le factory sostituiscono le fixture statiche. ```ruby # spec/factories/users.rb FactoryBot.define do factory :user do # Sequenze per garantire 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 } # Trait per le variazioni 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 ereditata 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 # Utilizzo nei test RSpec.describe OrderProcessor do # build: istanza non persistita let(:user) { build(:user) } # create: persistita su DB let(:order) { create(:order, :with_items, user: user) } # create_list: più istanze let(:products) { create_list(:product, 5) } # Combinare i trait let(:admin) { create(:user, :admin, :with_profile) } # Sovrascrivere gli attributi let(:expensive_order) { create(:order, :with_items, items_count: 10) } # build_stubbed: più veloce, per i test unitari let(:stubbed_user) { build_stubbed(:user) } end ``` Preferire `build` o `build_stubbed` rispetto a `create` quando la persistenza non è necessaria: questo accelera significativamente i test. ## Background Job ### Domanda 12: Come usare Active Job e Sidekiq in Rails? Active Job offre un'interfaccia unificata per i job in background, indipendentemente dal backend (Sidekiq, Resque, ecc.). Sidekiq è la scelta più popolare per le sue performance con Redis. ```ruby # app/jobs/process_order_job.rb class ProcessOrderJob < ApplicationJob queue_as :default # Configurazione dei retry retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10 discard_on ActiveJob::DeserializationError # Opzioni Sidekiq (se backend Sidekiq) sidekiq_options retry: 5, backtrace: true def perform(order_id) order = Order.find(order_id) OrderProcessor.new(order).call rescue ActiveRecord::RecordNotFound # Ordine eliminato tra l'accodamento e l'esecuzione 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 # Limitazione del rate con Sidekiq Enterprise o 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 # Accodare i job # Immediato ProcessOrderJob.perform_later(order.id) # Ritardato ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id) # A un orario specifico ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id) # Coda specifica ProcessOrderJob.set(queue: :critical).perform_later(order.id) # Sincrono (per test o debug) ProcessOrderJob.perform_now(order.id) ``` ```ruby # config/sidekiq.yml :concurrency: 10 :queues: - [critical, 3] # Alta priorità, peso 3 - [default, 2] # Media priorità, peso 2 - [mailers, 1] # Bassa priorità, peso 1 - [low, 1] :schedule: cleanup_job: cron: '0 3 * * *' # Ogni giorno alle 3:00 class: CleanupJob ``` Active Job astrae il backend, ma accedere a funzionalità specifiche (batch, rate limiting) spesso richiede accoppiamento con il backend scelto. ## Sviluppo API ### Domanda 13: Come costruire un'API RESTful con Rails? Rails facilita la costruzione di API JSON con controller API-only e serializer. Una buona API è versionata, documentata e sicura. ```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: 'Risorsa non trovata', details: exception.message }, status: :not_found end def unprocessable_entity(exception) render json: { error: 'Validazione fallita', details: exception.record.errors }, status: :unprocessable_entity end def bad_request(exception) render json: { error: 'Richiesta non valida', 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 # Con la gem jsonapi-serializer 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 ``` Buone pratiche per le API: versionare tramite namespace, usare codici HTTP appropriati, paginare le collezioni e fornire messaggi di errore chiari. ### Domanda 14: Come implementare l'autenticazione JWT in Rails? JWT (JSON Web Tokens) è un metodo di autenticazione stateless popolare per le API. Il token codifica l'identità e la validità dell'utente. ```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 scaduto' rescue JWT::DecodeError raise AuthenticationError, 'Token non valido' 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: 'Credenziali non valide' }, 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, 'Token assente' 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: 'Utente non trovato' }, status: :unauthorized end def current_user @current_user end end ``` Per la produzione, considerare: refresh token, blacklisting dei token al logout e tempi di scadenza brevi. Gem come `devise-jwt` semplificano l'implementazione. ## Cache e performance ### Domanda 15: Come implementare il caching in Rails? Rails offre diversi livelli di caching: fragment caching, Russian Doll caching, low-level caching. La scelta dipende dal caso d'uso. ```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 con chiave di cache automatica %> <% @products.each do |product| %> <%# Cache basata su updated_at del prodotto %> <% cache product do %> <%= render product %> <% end %> <% end %> <%# Russian Doll caching - cache annidata %> <% cache ['v1', @category] do %>

<%= @category.name %>

<% @category.products.each do |product| %> <% cache ['v1', product] do %> <%= render product %> <% end %> <% end %> <% end %> <%# Cache condizionale %> <% cache_if current_user.nil?, @product do %> <%= render @product %> <% end %> ``` ```ruby # app/models/product.rb class Product < ApplicationRecord # Touch sul padre per invalidare la cache Russian Doll belongs_to :category, touch: true # Chiave di cache personalizzata def cache_key_with_version "#{super}/#{reviews.maximum(:updated_at)&.to_i}" end end ``` ```ruby # Low-level caching nei service 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 con protezione contro race condition Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do Product.bestsellers.limit(10).to_a end # Invalidazione esplicita Rails.cache.delete('dashboard:stats') Rails.cache.delete_matched('products:*') ``` Il Russian Doll caching è efficace perché solo i frammenti modificati vengono rigenerati. Usare `touch: true` sulle associazioni per propagare l'invalidazione. ### Domanda 16: Come ottimizzare le performance di un'applicazione Rails? L'ottimizzazione di Rails copre diversi aspetti: query DB, cache, asset e architettura. Un approccio metodico con monitoraggio è essenziale. ```ruby # Ottimizzazione del database # config/database.yml production: pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> prepared_statements: true advisory_locks: true # app/models/order.rb class Order < ApplicationRecord # Indici composti per query frequenti # add_index :orders, [:user_id, :status, :created_at] # Selezionare solo le colonne necessarie scope :summary, -> { select(:id, :status, :total, :created_at) } # Elaborazione batch per grandi volumi def self.process_pending pending.find_each(batch_size: 1000) do |order| ProcessOrderJob.perform_later(order.id) end end # Evitare calcoli ripetitivi def self.revenue_by_month completed .group("DATE_TRUNC('month', created_at)") .sum(:total) end end ``` ```ruby # Ottimizzazione della memoria # 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 con 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 e paginazione class ProductsController < ApplicationController def index @products = Product.active .includes(:category, :primary_image) .page(params[:page]) .per(24) # Prefetch per la pagina successiva 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 ``` Strumenti essenziali: `rack-mini-profiler` per il profiling, `bullet` per la rilevazione di N+1, `New Relic` o `Scout` per il monitoraggio in produzione. ## Sicurezza ### Domanda 17: Quali sono le best practice di sicurezza in Rails? Rails include protezioni di default contro le vulnerabilità più comuni. Comprendere e configurare correttamente queste protezioni è cruciale. ```ruby # Protezione CSRF # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Abilitata di default, solleva un'eccezione se il token non è valido protect_from_forgery with: :exception # Per le API, usare :null_session # protect_from_forgery with: :null_session end # Nelle view, il token è incluso automaticamente nei form # <%= form_with ... %> include authenticity_token # Per le richieste AJAX # Aggiungere l'header X-CSRF-Token con il valore di csrf_meta_tags ``` ```ruby # Prevenzione SQL Injection # ✅ Parametri interpolati con escape automatico User.where('email = ?', params[:email]) User.where(email: params[:email]) # ❌ PERICOLO - Interpolazione diretta User.where("email = '#{params[:email]}'") # ✅ Per clausole ORDER dinamiche ALLOWED_SORTS = %w[name created_at price].freeze sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name' Product.order(sort_column) ``` ```ruby # Protezione XSS # Rails fa l'escape dell'HTML nelle view automaticamente # ✅ Escape automatico <%= user.name %> # ❌ Pericoloso - contenuto senza escape <%== user.bio %> <%= raw user.bio %> <%= user.bio.html_safe %> # ✅ Per HTML sicuro, usare 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 esplicita degli attributi consentiti params.require(:user).permit(:name, :email, :avatar) # Solo per amministratori if current_user.admin? params.require(:user).permit(:name, :email, :role, :active) else params.require(:user).permit(:name, :email) end end end ``` ```ruby # Header di sicurezza # 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 ``` Auditare regolarmente con `brakeman` (analisi statica di sicurezza) e mantenere aggiornate le gem con `bundle audit`. ### Domanda 18: Come gestire autenticazione e autorizzazione in Rails? L'autenticazione verifica l'identità, l'autorizzazione controlla i permessi. Devise gestisce l'auth, Pundit o CanCanCan gestiscono l'autorizzazione. ```ruby # Setup di Devise # app/models/user.rb class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmable, :lockable, :trackable enum role: { user: 0, moderator: 1, admin: 2 } def admin? role == 'admin' end end ``` ```ruby # Policy Pundit # app/policies/article_policy.rb class ArticlePolicy < ApplicationPolicy def index? true end def show? record.published? || owner_or_admin? end def create? user.present? end def update? owner_or_admin? end def destroy? owner_or_admin? end def publish? user&.admin? || user&.moderator? end # Scope per le collezioni 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 con 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: 'Articolo aggiornato.' 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: 'Articolo pubblicato.' end rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "Non sei autorizzato a eseguire questa azione." redirect_back(fallback_location: root_path) end end ``` Pundit è più esplicito e testabile rispetto a CanCanCan. Ogni azione ha un metodo di policy corrispondente, e gli scope filtrano automaticamente le collezioni. ## Rails avanzato ### Domanda 19: Spiegare il pattern Repository in Rails Il pattern Repository isola la logica di accesso ai dati dal resto dell'applicazione. Sebbene Rails usi Active Record (un pattern diverso), Repository può essere utile per casi complessi. ```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 # Utilizzo in 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 # Facilita il testing con i mock RSpec.describe ProductSearchService do let(:repository) { instance_double(ProductRepository) } let(:service) { described_class.new(repository: repository) } it 'filtra per categoria' 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 ``` Repository è opzionale in Rails poiché Active Record è già un eccellente pattern. Usarlo per query complesse o quando l'isolamento dello storage è importante. ### Domanda 20: Come implementare il pattern CQRS in Rails? CQRS (Command Query Responsibility Segregation) separa le operazioni di lettura e scrittura. In Rails, ciò si traduce in classi distinte per query e command. ```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, "Prodotto #{item[:product_id]} non disponibile") 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 che usa 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: 'Ordine creato!' else flash.now[:alert] = result.errors.join(', ') render :new, status: :unprocessable_entity end end end ``` CQRS brilla in applicazioni complesse con esigenze asimmetriche di lettura/scrittura. Per CRUD semplici, è over-engineering. ### Domanda 21: Come gestire i WebSocket con Action Cable? Action Cable integra i WebSocket in Rails per la comunicazione bidirezionale in tempo reale. Utilizza Redis per la sincronizzazione tra server. ```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 # Tramite cookie di sessione if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user # Tramite JWT per le API 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]) # Verificare i permessi unless @room.accessible_by?(current_user) reject return end stream_for @room # Notificare gli altri della presenza 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'] ) # Trasmettere a tutti gli iscritti 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 gestisce automaticamente le riconnessioni e la sincronizzazione. In produzione, configurare Redis come adapter e scalare in base alle connessioni concorrenti. ### Domanda 22: Come implementare la multi-tenancy in Rails? La multi-tenancy permette a un'applicazione di servire più client (tenant) isolati. Tre approcci principali: a livello di database, a livello di schema o a livello di riga. ```ruby # Multitenancy a livello di riga con ActsAsTenant o manuale # app/models/concerns/tenant_scoped.rb module TenantScoped extend ActiveSupport::Concern included do belongs_to :tenant # Scope di default sul tenant corrente default_scope -> { where(tenant: Current.tenant) if Current.tenant } # Validazione del 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 # Tramite sottodominio if request.subdomain.present? && request.subdomain != 'www' Tenant.find_by!(subdomain: request.subdomain) # Tramite header (per le API) elsif request.headers['X-Tenant-ID'].present? Tenant.find(request.headers['X-Tenant-ID']) # Tramite utente elsif current_user current_user.tenant end rescue ActiveRecord::RecordNotFound redirect_to root_url(subdomain: 'www'), alert: 'Tenant non trovato' 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 # Gli amministratori possono appartenere a più tenant has_many :tenant_memberships has_many :accessible_tenants, through: :tenant_memberships, source: :tenant end ``` ```ruby # A livello di schema con la gem Apartment (PostgreSQL) # config/initializers/apartment.rb Apartment.configure do |config| config.excluded_models = %w[Tenant User] config.tenant_names = -> { Tenant.pluck(:subdomain) } end # Utilizzo Apartment::Tenant.switch('acme') do # Tutte le query in questo blocco usano lo schema 'acme' Project.all # SELECT * FROM acme.projects end ``` Il livello di riga è il più semplice ma richiede attenzione costante alle perdite. Il livello di schema offre un isolamento migliore ma complica le migration. Scegliere in base alle esigenze di sicurezza e scalabilità. ### Domanda 23: Come configurare un'architettura a microservizi con Rails? Rails può servire come base per un'architettura a microservizi con comunicazione tramite HTTP/gRPC o code di messaggi. La chiave è definire bene i confini. ```ruby # Client di servizio 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('Servizio non disponibile', code: response.code) end rescue Net::OpenTimeout, Net::ReadTimeout ServiceResult.failure('Timeout del servizio') end end ``` ```ruby # Comunicazione event-driven con 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 # Pattern API Gateway # app/controllers/api/v1/gateway_controller.rb module Api module V1 class GatewayController < BaseController # Aggregare più servizi 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} non disponibile", message: e.message } end end end end ``` Per i microservizi Rails: definire contratti API chiari (OpenAPI), implementare circuit breaker (gem `circuitbox`) e usare il tracing distribuito (gem `opentelemetry`). ### Domanda 24: Come fare il deploy di un'applicazione Rails in produzione? Il deploy moderno di Rails utilizza container o PaaS. Una configurazione di produzione robusta copre asset, database e monitoraggio. ```ruby # config/environments/production.rb Rails.application.configure do config.cache_classes = true config.eager_load = true config.consider_all_requests_local = false # Asset 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 } # Forzare 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 # Immagine di produzione 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 di produzione: SSL obbligatorio, segreti tramite ENV, health check, backup automatici del DB, monitoraggio (APM + log + metriche) e alerting configurato. ### Domanda 25: Quali sono le novità di Rails 7+ da conoscere? Rails 7+ porta cambiamenti significativi: Hotwire di default, import map, credenziali criptate migliorate e numerose ottimizzazioni. ```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 "Carica altri", articles_path(page: @page + 1), data: { turbo_frame: "articles" } %> <% end %> # Turbo Streams per gli aggiornamenti in tempo reale # 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 # Controller Stimulus # app/javascript/controllers/search_controller.js import { Controller } from "@hotwired/stimulus" import { debounce } from "lodash-es" export default class extends Controller { static targets = ["input", "results"] static values = { url: String } connect() { this.search = debounce(this.search.bind(this), 300) } async search() { const query = this.inputTarget.value if (query.length < 2) return const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`) this.resultsTarget.innerHTML = await response.text() } } ``` ```ruby # Import Maps (senza 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" # Pin da 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 # Permette le ricerche encrypts :phone_number # Non deterministico di default 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 # Miglioramenti all'interfaccia di query # Rails 7.1+ # Query asincrone users = User.where(active: true).load_async # Continuare a elaborare mentre la query è in esecuzione # Accedere ai risultati con 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') # Rilevamento automatico di inverse_of class Author < ApplicationRecord has_many :books # inverse_of rilevato automaticamente end # Strict loading di default (evita N+1) class ApplicationRecord < ActiveRecord::Base self.strict_loading_by_default = true end ``` Rails 7+ favorisce la semplicità (niente Webpack di default) e l'HTML-over-the-wire con Hotwire. Questo approccio riduce la complessità di JavaScript offrendo al contempo un'esperienza utente moderna. ## Conclusione I colloqui Ruby on Rails valutano la padronanza dell'intero framework e la comprensione delle sue convenzioni. Punti chiave da ricordare: ✅ **Fondamenti**: MVC, Active Record, migration, validazioni e associazioni ✅ **Architettura**: Service Object, Concern, Query Object e pattern CQRS ✅ **Performance**: query N+1, caching (fragment, Russian Doll, low-level), eager loading ✅ **Testing**: RSpec, FactoryBot, request spec e best practice di testing ✅ **Sicurezza**: CSRF, SQL injection, XSS, Strong Parameters e autenticazione/autorizzazione ✅ **API**: design RESTful, JWT, serializer e versionamento ✅ **Produzione**: background job, WebSocket, deploy e monitoraggio La filosofia Rails (Convention over Configuration, DRY e Rails Way) guida tutte le decisioni architetturali. Padroneggiare questi principi e sapere quando deviarne dimostra una solida competenza. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/ruby-on-rails/ruby-on-rails-interview-questions