# Preguntas de entrevista Ruby on Rails: Top 25 en 2026 > Las 25 preguntas de entrevista Ruby on Rails más solicitadas. Arquitectura MVC, Active Record, migraciones, testing RSpec, APIs REST con respuestas detalladas y ejemplos de código. - Published: 2026-02-03 - Updated: 2026-04-27 - Author: Anthony Fillion-Maillet - Tags: ruby on rails, ruby, entrevista, active record, entrevista técnica - Reading time: 22 min --- Las entrevistas de Ruby on Rails evalúan el dominio del framework Ruby más popular, la comprensión de la arquitectura MVC, el ORM Active Record y la capacidad de construir aplicaciones web robustas siguiendo la filosofía "Convention over Configuration". Esta guía cubre las 25 preguntas más solicitadas, desde los fundamentos de Rails hasta los patrones avanzados de producción. > **Consejo para la entrevista** > > Los reclutadores valoran a los candidatos que comprenden la filosofía Rails: "Convention over Configuration", DRY (Don't Repeat Yourself) y los patrones Rails Way. Explicar por qué Rails toma ciertas decisiones arquitectónicas marca la diferencia. ## Fundamentos de Ruby on Rails ### Pregunta 1: Explicar el patrón MVC en Ruby on Rails El patrón Model-View-Controller (MVC) es el núcleo arquitectónico de Rails. Separa las responsabilidades en tres capas distintas para una mejor mantenibilidad y testabilidad del código. ```ruby # app/models/article.rb # El Model gestiona los datos y la lógica de negocio class Article < ApplicationRecord # Validaciones de datos validates :title, presence: true, length: { minimum: 5 } validates :body, presence: true # Asociaciones con otros modelos belongs_to :author, class_name: 'User' has_many :comments, dependent: :destroy has_many :tags, through: :article_tags # Scopes para consultas reutilizables scope :published, -> { where(published: true) } scope :recent, -> { order(created_at: :desc).limit(10) } # Callbacks del ciclo de vida before_save :generate_slug private def generate_slug self.slug = title.parameterize if title_changed? end end ``` ```ruby # app/controllers/articles_controller.rb # El Controller recibe las peticiones y orquesta la respuesta 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: 'Artículo creado correctamente.' 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 muestra los datos en formato HTML %>

<%= @article.title %>

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

<%= simple_format @article.body %>
<%# Partial para los comentarios %> <%= render @comments %>
``` El flujo típico: la petición llega al Router, que la despacha al Controller adecuado. El Controller interactúa con el Model para recuperar o modificar los datos, luego pasa esos datos a la View para el renderizado HTML. ### Pregunta 2: ¿Qué es Active Record y cómo funciona el ORM de Rails? Active Record es el ORM (Object-Relational Mapping) de Rails que implementa el patrón Active Record. Cada clase Model representa una tabla de la base de datos, y cada instancia representa una fila. ```ruby # app/models/user.rb # Active Record mapea automáticamente columnas a atributos class User < ApplicationRecord # La tabla 'users' se asocia automáticamente # Columnas: id, email, name, created_at, updated_at has_secure_password # BCrypt para la contraseña has_many :articles, foreign_key: :author_id has_one :profile, dependent: :destroy has_and_belongs_to_many :roles # Validaciones validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP } # Callbacks before_save :normalize_email # Métodos de clase para consultas def self.admins joins(:roles).where(roles: { name: 'admin' }) end private def normalize_email self.email = email.downcase.strip end end ``` ```ruby # Ejemplos de consultas Active Record # Consola Rails o dentro de un servicio # Creación user = User.create!(email: 'dev@example.com', name: 'Alice', password: 'secret123') # Lectura con condiciones active_users = User.where(active: true).order(:name) user = User.find_by(email: 'dev@example.com') # Consultas encadenadas (evaluación perezosa) recent_admins = User.admins .where('created_at > ?', 1.month.ago) .includes(:profile) .limit(10) # Prevención de N+1 con eager loading articles = Article.includes(:author, :comments).published # Actualización user.update!(name: 'Alice Martin') # Transacciones User.transaction do user.debit_balance!(100) recipient.credit_balance!(100) Payment.create!(from: user, to: recipient, amount: 100) end ``` Active Record convierte los métodos Ruby en consultas SQL optimizadas. Los métodos como `where`, `joins`, `includes` son perezosos: la consulta solo se ejecuta al iterar o al llamar `to_a`. ### Pregunta 3: Explicar el sistema de migraciones de Rails Las migraciones permiten versionar el esquema de la base de datos con Ruby. Son reversibles y permiten una evolución controlada de la estructura de datos. ```ruby # db/migrate/20260203100000_create_products.rb # Migración para crear una tabla 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 y updated_at automáticos end # Índices para el rendimiento add_index :products, :name add_index :products, [:category_id, :active] end end ``` ```ruby # db/migrate/20260203110000_add_slug_to_products.rb # Migración para modificar una tabla existente class AddSlugToProducts < ActiveRecord::Migration[7.1] def change add_column :products, :slug, :string add_index :products, :slug, unique: true # Rellenar slugs existentes reversible do |dir| dir.up do Product.find_each do |product| product.update_column(:slug, product.name.parameterize) end end end # Hacer no nulo después del relleno change_column_null :products, :slug, false end end ``` ```bash # Comandos esenciales de migración rails db:migrate # Ejecutar migraciones pendientes rails db:rollback # Deshacer la última migración rails db:rollback STEP=3 # Deshacer las últimas 3 migraciones rails db:migrate:status # Ver el estado de las migraciones rails db:seed # Ejecutar db/seeds.rb rails db:reset # Drop, create, migrate, seed ``` Las migraciones deben ser reversibles. El método `change` es inteligente y puede revertir automáticamente las operaciones comunes. Para casos complejos, usar `up` y `down` por separado. ## Active Record avanzado ### Pregunta 4: ¿Cómo optimizar consultas N+1 en Rails? El problema N+1 ocurre cuando una consulta inicial es seguida por N consultas adicionales para cargar las asociaciones. Rails ofrece varios métodos de eager loading para resolver este problema. ```ruby # app/controllers/orders_controller.rb class OrdersController < ApplicationController def index # ❌ PROBLEMA N+1: 1 consulta + N consultas por pedido # @orders = Order.all # En la vista: order.user.name genera una consulta por pedido # ✅ SOLUCIÓN con includes (eager loading) @orders = Order.includes(:user, :items) .where(status: 'completed') .order(created_at: :desc) # Genera solo 3 consultas en total end def show # includes: carga las asociaciones por separado (2-3 consultas) @order = Order.includes(items: :product).find(params[:id]) # preload: fuerza la carga separada @order = Order.preload(:items, :user).find(params[:id]) # eager_load: fuerza un LEFT OUTER JOIN (1 consulta) @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 por defecto scope :with_details, -> { includes(:user, items: :product) } # Counter cache para evitar consultas COUNT # Requiere: add_column :users, :orders_count, :integer, default: 0 belongs_to :user, counter_cache: true end ``` ```ruby # Detección de N+1 con la gem Bullet (desarrollo) # config/environments/development.rb config.after_initialize do Bullet.enable = true Bullet.alert = true Bullet.bullet_logger = true Bullet.rails_logger = true end # Bullet mostrará alertas cuando: # - Se detecte una consulta N+1 # - Haya eager loading innecesario # - Se debería usar un counter cache ``` La regla: usar `includes` por defecto (Rails elige la estrategia óptima), `preload` cuando se quiere forzar consultas separadas, `eager_load` cuando se filtra sobre las asociaciones. ### Pregunta 5: Explicar Scopes y Query Objects en Rails Los scopes encapsulan condiciones de consulta reutilizables. Para consultas complejas, los Query Objects ofrecen mejor organización y testabilidad. ```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 con parámetros scope :cheaper_than, ->(price) { where('price < ?', price) } scope :in_category, ->(category) { where(category: category) } # Scopes encadenables 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 subconsulta 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 para búsquedas complejas 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 # Uso en el controller @products = ProductsSearchQuery.new(Product.active).call(params) ``` Los scopes son perfectos para condiciones simples y reutilizables. Los Query Objects son adecuados para búsquedas complejas con múltiples filtros opcionales y lógica de composición. ## Routing y Controllers ### Pregunta 6: ¿Cómo funciona el routing RESTful en Rails? Rails fomenta rutas RESTful que mapean los verbos HTTP a las acciones CRUD. El router traduce las URL en llamadas específicas al controller. ```ruby # config/routes.rb Rails.application.routes.draw do # Rutas RESTful estándar (7 acciones) resources :articles do # Rutas anidadas resources :comments, only: [:create, :destroy] # Rutas de miembro (actúan sobre una instancia) member do post :publish delete :archive end # Rutas de colección (actúan sobre la colección) collection do get :drafts get :search end end # Rutas de API con namespace namespace :api do namespace :v1 do resources :products, only: [:index, :show, :create, :update] do resources :reviews, shallow: true end end end # Ruta personalizada get 'dashboard', to: 'dashboard#index' # Restricciones de ruta constraints(SubdomainConstraint.new) do resources :admin_settings end # Ruta raíz root 'home#index' end ``` ```bash # rails routes - Muestra todas las rutas generadas # # 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 ``` Los helpers de ruta generados (`article_path(@article)`, `new_article_path`) permiten referenciar las URL de forma dinámica y mantenible. ### Pregunta 7: Explicar callbacks y filtros en los controllers Los callbacks (before_action, after_action, around_action) permiten ejecutar código antes, después o alrededor de las acciones del controller. ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Protección CSRF habilitada por defecto protect_from_forgery with: :exception # Callback global para autenticación before_action :authenticate_user! # Manejo global de errores rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from ActionController::ParameterMissing, with: :bad_request private def not_found render json: { error: 'Recurso no encontrado' }, 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 con opciones before_action :require_admin before_action :set_product, only: [:show, :edit, :update, :destroy] after_action :log_activity, only: [:create, :update, :destroy] # Callback condicional before_action :check_stock, only: [:update], if: :stock_changed? def create @product = Product.new(product_params) if @product.save redirect_to [:admin, @product], notice: 'Producto creado.' else render :new, status: :unprocessable_entity end end def update if @product.update(product_params) redirect_to [:admin, @product], notice: 'Producto actualizado.' 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 ``` Los callbacks se ejecutan en orden de declaración. Usar `skip_before_action` en las subclases para deshabilitar callbacks heredados. Evitar callbacks con demasiada lógica de negocio: preferir Service Objects. ## Servicios y arquitectura ### Pregunta 8: ¿Cómo implementar Service Objects en Rails? Los Service Objects encapsulan lógica de negocio compleja que no pertenece ni a los Models ni a los Controllers. Mejoran la testabilidad y siguen el principio de responsabilidad única. ```ruby # app/services/order_processor.rb # Service Object con interfaz estandarizada class OrderProcessor def initialize(order, payment_method:) @order = order @payment_method = payment_method end def call return failure('Pedido ya procesado') 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("Fallo en el pago: #{e.message}") rescue InsufficientStockError => e failure("Stock insuficiente: #{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: "Pedido ##{@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: '¡Pedido confirmado!' 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 ``` El patrón Service Object sigue una convención simple: una clase, una responsabilidad, un método público `call`. Devolver un objeto Result permite manejar limpiamente éxito y fallo. ### Pregunta 9: Explicar Concerns en Rails Los Concerns permiten extraer y compartir código entre Models o Controllers. Utilizan `ActiveSupport::Concern` para una sintaxis de inclusión limpia. ```ruby # app/models/concerns/sluggable.rb # Concern reutilizable para generar slugs module Sluggable extend ActiveSupport::Concern included do # Código ejecutado al incluir before_validation :generate_slug, if: :should_generate_slug? validates :slug, presence: true, uniqueness: true end # Métodos de clase 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étodos de instancia 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 # Opcional, :title por defecto end # app/models/product.rb class Product < ApplicationRecord include Sluggable sluggable_source :name end ``` ```ruby # app/controllers/concerns/pagination.rb # Concern para 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 ``` Los Concerns son útiles para código verdaderamente compartido. Evitar crear Concerns solo para "acortar" un Model: eso oculta la complejidad sin reducirla. ## Testing con RSpec ### Pregunta 10: ¿Cómo estructurar los tests RSpec en Rails? RSpec es el framework de testing estándar para Rails. Una buena estructura de tests incluye Model specs, Controller specs, Service specs y tests de integración. ```ruby # spec/models/user_spec.rb require 'rails_helper' RSpec.describe User, type: :model do # Factories 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 el formato del 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 'devuelve el nombre y apellido combinados' do user = build(:user, first_name: 'John', last_name: 'Doe') expect(user.full_name).to eq('John Doe') end it 'maneja la ausencia de apellido' do user = build(:user, first_name: 'John', last_name: nil) expect(user.full_name).to eq('John') end end describe '.active' do it 'devuelve solo los usuarios activos' 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 'cuando el pedido es válido' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: true, transaction_id: 'txn_123') ) end it 'procesa el pedido correctamente' do result = subject.call expect(result).to be_success expect(order.reload.status).to eq('completed') end it 'decrementa el stock del producto' do expect { subject.call }.to change { product.reload.stock_quantity }.by(-2) end it 'envía el correo de confirmación' do expect { subject.call } .to have_enqueued_mail(OrderMailer, :confirmation) .with(order) end end context 'cuando falla el pago' do before do allow(PaymentGateway).to receive(:charge).and_return( OpenStruct.new(success?: false, error: 'Card declined') ) end it 'devuelve un resultado de fallo' do result = subject.call expect(result).to be_failure expect(result.error).to include('Card declined') end it 'no actualiza el estado del pedido' 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 'devuelve la lista de productos' 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 por categoría' 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: 'Producto Nuevo', price: 99.99, category_id: create(:category).id } } end it 'crea un producto nuevo' 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 ``` Buenas prácticas: usar `let` para los datos, `describe` para métodos/contextos, `context` para condiciones, e `it` para aserciones específicas. Cada test debe probar una sola cosa. ### Pregunta 11: ¿Cómo usar factories con FactoryBot? FactoryBot permite crear datos de prueba de forma declarativa y mantenible. Las factories reemplazan a las fixtures estáticas. ```ruby # spec/factories/users.rb FactoryBot.define do factory :user do # Secuencias para garantizar unicidad 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 para variaciones 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 heredada 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 # Uso en tests RSpec.describe OrderProcessor do # build: instancia no persistida let(:user) { build(:user) } # create: persistida en BD let(:order) { create(:order, :with_items, user: user) } # create_list: múltiples instancias let(:products) { create_list(:product, 5) } # Combinar traits let(:admin) { create(:user, :admin, :with_profile) } # Sobrescribir atributos let(:expensive_order) { create(:order, :with_items, items_count: 10) } # build_stubbed: más rápido, para tests unitarios let(:stubbed_user) { build_stubbed(:user) } end ``` Preferir `build` o `build_stubbed` sobre `create` cuando la persistencia no es necesaria: esto acelera significativamente los tests. ## Background Jobs ### Pregunta 12: ¿Cómo usar Active Job y Sidekiq en Rails? Active Job ofrece una interfaz unificada para trabajos en segundo plano, sin importar el backend (Sidekiq, Resque, etc.). Sidekiq es la opción popular por su rendimiento con Redis. ```ruby # app/jobs/process_order_job.rb class ProcessOrderJob < ApplicationJob queue_as :default # Configuración de reintentos retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3 retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 10 discard_on ActiveJob::DeserializationError # Opciones de Sidekiq (si el backend es Sidekiq) sidekiq_options retry: 5, backtrace: true def perform(order_id) order = Order.find(order_id) OrderProcessor.new(order).call rescue ActiveRecord::RecordNotFound # Pedido eliminado entre el encolado y la ejecución 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 # Limitación de tasa 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 # Encolar trabajos # Inmediato ProcessOrderJob.perform_later(order.id) # Diferido ProcessOrderJob.set(wait: 5.minutes).perform_later(order.id) # A una hora específica ProcessOrderJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id) # Cola específica ProcessOrderJob.set(queue: :critical).perform_later(order.id) # Síncrono (para tests o depuración) ProcessOrderJob.perform_now(order.id) ``` ```ruby # config/sidekiq.yml :concurrency: 10 :queues: - [critical, 3] # Alta prioridad, peso 3 - [default, 2] # Prioridad media, peso 2 - [mailers, 1] # Prioridad baja, peso 1 - [low, 1] :schedule: cleanup_job: cron: '0 3 * * *' # Cada día a las 3am class: CleanupJob ``` Active Job abstrae el backend, pero acceder a funcionalidades específicas (batches, rate limiting) suele requerir acoplamiento con el backend elegido. ## Desarrollo de API ### Pregunta 13: ¿Cómo construir una API RESTful con Rails? Rails facilita la construcción de APIs JSON con Controllers API-only y serializadores. Una buena API está versionada, documentada y es segura. ```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: 'Recurso no encontrado', details: exception.message }, status: :not_found end def unprocessable_entity(exception) render json: { error: 'Validación fallida', details: exception.record.errors }, status: :unprocessable_entity end def bad_request(exception) render json: { error: 'Petición incorrecta', 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 ``` Buenas prácticas de API: versionar mediante namespace, usar códigos HTTP apropiados, paginar las colecciones y proporcionar mensajes de error claros. ### Pregunta 14: ¿Cómo implementar autenticación JWT en Rails? JWT (JSON Web Tokens) es un método de autenticación stateless popular para APIs. El token codifica la identidad y validez del usuario. ```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 expirado' rescue JWT::DecodeError raise AuthenticationError, 'Token inválido' 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: 'Credenciales inválidas' }, 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 ausente' 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: 'Usuario no encontrado' }, status: :unauthorized end def current_user @current_user end end ``` Para producción, considerar: refresh tokens, blacklisting de tokens al cerrar sesión y tiempos de expiración cortos. Gems como `devise-jwt` simplifican la implementación. ## Caché y rendimiento ### Pregunta 15: ¿Cómo implementar caché en Rails? Rails ofrece varios niveles de caché: fragment caching, Russian Doll caching, low-level caching. La elección depende del caso de 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 clave de caché automática %> <% @products.each do |product| %> <%# Caché basada en updated_at del producto %> <% cache product do %> <%= render product %> <% end %> <% end %> <%# Russian Doll caching - caché anidada %> <% cache ['v1', @category] do %>

<%= @category.name %>

<% @category.products.each do |product| %> <% cache ['v1', product] do %> <%= render product %> <% end %> <% end %> <% end %> <%# Caché condicional %> <% cache_if current_user.nil?, @product do %> <%= render @product %> <% end %> ``` ```ruby # app/models/product.rb class Product < ApplicationRecord # Touch al padre para invalidar la caché Russian Doll belongs_to :category, touch: true # Clave de caché personalizada def cache_key_with_version "#{super}/#{reviews.maximum(:updated_at)&.to_i}" end end ``` ```ruby # Low-level caching en servicios 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 # Caché con protección contra race conditions Rails.cache.fetch('popular_products', expires_in: 1.hour, race_condition_ttl: 10.seconds) do Product.bestsellers.limit(10).to_a end # Invalidación explícita Rails.cache.delete('dashboard:stats') Rails.cache.delete_matched('products:*') ``` Russian Doll caching es eficaz porque solo se regeneran los fragmentos modificados. Usar `touch: true` en las asociaciones para propagar la invalidación. ### Pregunta 16: ¿Cómo optimizar el rendimiento de una aplicación Rails? La optimización Rails cubre múltiples aspectos: consultas BD, caché, assets y arquitectura. Un enfoque metódico con monitorización es esencial. ```ruby # Optimización de la base de datos # config/database.yml production: pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> prepared_statements: true advisory_locks: true # app/models/order.rb class Order < ApplicationRecord # Índices compuestos para consultas frecuentes # add_index :orders, [:user_id, :status, :created_at] # Seleccionar solo las columnas necesarias scope :summary, -> { select(:id, :status, :total, :created_at) } # Procesamiento por lotes para grandes volúmenes def self.process_pending pending.find_each(batch_size: 1000) do |order| ProcessOrderJob.perform_later(order.id) end end # Evitar cálculos repetitivos def self.revenue_by_month completed .group("DATE_TRUNC('month', created_at)") .sum(:total) end end ``` ```ruby # Optimización de 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 y paginación class ProductsController < ApplicationController def index @products = Product.active .includes(:category, :primary_image) .page(params[:page]) .per(24) # Prefetch para la siguiente página 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 ``` Herramientas esenciales: `rack-mini-profiler` para profiling, `bullet` para detección de N+1, `New Relic` o `Scout` para monitorización en producción. ## Seguridad ### Pregunta 17: ¿Cuáles son las mejores prácticas de seguridad en Rails? Rails incluye protecciones por defecto contra vulnerabilidades comunes. Comprender y configurar correctamente estas protecciones es crucial. ```ruby # Protección CSRF # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Habilitada por defecto, lanza una excepción si el token es inválido protect_from_forgery with: :exception # Para APIs, usar :null_session # protect_from_forgery with: :null_session end # En las vistas, el token se incluye automáticamente en los formularios # <%= form_with ... %> incluye authenticity_token # Para peticiones AJAX # Añadir el header X-CSRF-Token con el valor de csrf_meta_tags ``` ```ruby # Prevención de inyección SQL # ✅ Parámetros interpolados escapados automáticamente User.where('email = ?', params[:email]) User.where(email: params[:email]) # ❌ PELIGRO - Interpolación directa User.where("email = '#{params[:email]}'") # ✅ Para cláusulas ORDER dinámicas ALLOWED_SORTS = %w[name created_at price].freeze sort_column = ALLOWED_SORTS.include?(params[:sort]) ? params[:sort] : 'name' Product.order(sort_column) ``` ```ruby # Protección XSS # Rails escapa automáticamente el HTML en las vistas # ✅ Escapado automático <%= user.name %> # ❌ Peligroso - contenido sin escapar <%== user.bio %> <%= raw user.bio %> <%= user.bio.html_safe %> # ✅ Para HTML seguro, usar 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 explícita de atributos permitidos params.require(:user).permit(:name, :email, :avatar) # Solo para administradores if current_user.admin? params.require(:user).permit(:name, :email, :role, :active) else params.require(:user).permit(:name, :email) end end end ``` ```ruby # Cabeceras de seguridad # 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 ``` Auditar regularmente con `brakeman` (análisis estático de seguridad) y mantener las gems actualizadas con `bundle audit`. ### Pregunta 18: ¿Cómo manejar autenticación y autorización en Rails? La autenticación verifica la identidad, la autorización controla los permisos. Devise gestiona la auth, Pundit o CanCanCan gestionan la autorización. ```ruby # Configuración de 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 # Políticas 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 para colecciones 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: 'Artículo actualizado.' 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: 'Artículo publicado.' end rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized flash[:alert] = "No estás autorizado para realizar esta acción." redirect_back(fallback_location: root_path) end end ``` Pundit es más explícito y testeable que CanCanCan. Cada acción tiene un método de política correspondiente, y los scopes filtran automáticamente las colecciones. ## Rails avanzado ### Pregunta 19: Explicar el patrón Repository en Rails El patrón Repository aísla la lógica de acceso a datos del resto de la aplicación. Aunque Rails usa Active Record (un patrón distinto), Repository puede ser útil para casos complejos. ```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 # Uso en un servicio 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 el testing con mocks RSpec.describe ProductSearchService do let(:repository) { instance_double(ProductRepository) } let(:service) { described_class.new(repository: repository) } it 'filtra por categoría' 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 es opcional en Rails ya que Active Record es un excelente patrón. Usarlo para consultas complejas o cuando el aislamiento del almacenamiento sea importante. ### Pregunta 20: ¿Cómo implementar el patrón CQRS en Rails? CQRS (Command Query Responsibility Segregation) separa las operaciones de lectura y escritura. En Rails, esto se traduce en clases distintas para queries y 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, "Producto #{item[:product_id]} no disponible") 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 usando 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: '¡Pedido creado!' else flash.now[:alert] = result.errors.join(', ') render :new, status: :unprocessable_entity end end end ``` CQRS brilla en aplicaciones complejas con necesidades asimétricas de lectura/escritura. Para CRUD simple, es sobre-ingeniería. ### Pregunta 21: ¿Cómo manejar WebSockets con Action Cable? Action Cable integra WebSockets en Rails para comunicación bidireccional en tiempo real. Usa Redis para la sincronización entre servidores. ```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 # Vía cookie de sesión if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user # Vía JWT para 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]) # Verificar permisos unless @room.accessible_by?(current_user) reject return end stream_for @room # Notificar a los demás de la presencia 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'] ) # Difundir a todos los suscriptores 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 maneja automáticamente las reconexiones y la sincronización. En producción, configurar Redis como adaptador y escalar según las conexiones concurrentes. ### Pregunta 22: ¿Cómo implementar multi-tenancy en Rails? La multi-tenancy permite que una aplicación sirva a múltiples clientes (tenants) aislados. Tres enfoques principales: a nivel de base de datos, a nivel de schema o a nivel de fila. ```ruby # Multitenancy a nivel de fila con ActsAsTenant o manual # app/models/concerns/tenant_scoped.rb module TenantScoped extend ActiveSupport::Concern included do belongs_to :tenant # Scope por defecto al tenant actual default_scope -> { where(tenant: Current.tenant) if Current.tenant } # Validación 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 # Vía subdominio if request.subdomain.present? && request.subdomain != 'www' Tenant.find_by!(subdomain: request.subdomain) # Vía header (para APIs) elsif request.headers['X-Tenant-ID'].present? Tenant.find(request.headers['X-Tenant-ID']) # Vía usuario elsif current_user current_user.tenant end rescue ActiveRecord::RecordNotFound redirect_to root_url(subdomain: 'www'), alert: 'Tenant no encontrado' 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 # Los administradores pueden pertenecer a varios tenants has_many :tenant_memberships has_many :accessible_tenants, through: :tenant_memberships, source: :tenant end ``` ```ruby # A nivel de 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 # Uso Apartment::Tenant.switch('acme') do # Todas las queries en este bloque usan el schema 'acme' Project.all # SELECT * FROM acme.projects end ``` A nivel de fila es lo más simple pero requiere atención constante a las fugas. A nivel de schema ofrece mejor aislamiento pero complica las migraciones. Elegir según las necesidades de seguridad y escalabilidad. ### Pregunta 23: ¿Cómo configurar una arquitectura de microservicios con Rails? Rails puede servir como base para una arquitectura de microservicios con comunicación vía HTTP/gRPC o colas de mensajes. La clave es definir bien los límites. ```ruby # Cliente de servicio 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('Servicio no disponible', code: response.code) end rescue Net::OpenTimeout, Net::ReadTimeout ServiceResult.failure('Timeout del servicio') end end ``` ```ruby # Comunicación dirigida por eventos 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 # Patrón API Gateway # app/controllers/api/v1/gateway_controller.rb module Api module V1 class GatewayController < BaseController # Agregar múltiples servicios 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} no disponible", message: e.message } end end end end ``` Para microservicios Rails: definir contratos de API claros (OpenAPI), implementar circuit breakers (gem `circuitbox`) y usar tracing distribuido (gem `opentelemetry`). ### Pregunta 24: ¿Cómo desplegar una aplicación Rails en producción? El despliegue moderno de Rails utiliza contenedores o PaaS. Una configuración robusta de producción cubre assets, base de datos y monitorización. ```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) } # Caché config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'], expires_in: 1.day } # Forzar 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 # Imagen de producción 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 de producción: SSL obligatorio, secretos vía ENV, health checks, backups automatizados de BD, monitorización (APM + logs + métricas) y alertas configuradas. ### Pregunta 25: ¿Cuáles son las novedades de Rails 7+ que hay que conocer? Rails 7+ trae cambios significativos: Hotwire por defecto, import maps, credenciales encriptadas mejoradas y muchas optimizaciones. ```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 "Cargar más", articles_path(page: @page + 1), data: { turbo_frame: "articles" } %> <% end %> # Turbo Streams para actualizaciones en tiempo real # 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 # Controllers 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 (sin bundler de 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 desde 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 # Permite búsquedas encrypts :phone_number # No determinista por defecto 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 # Mejoras en la interfaz de queries # Rails 7.1+ # Queries asíncronas users = User.where(active: true).load_async # Continuar procesando mientras se ejecuta la query # Acceder a los resultados 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') # Detección automática de inverse_of class Author < ApplicationRecord has_many :books # inverse_of detectado automáticamente end # Strict loading por defecto (evita N+1) class ApplicationRecord < ActiveRecord::Base self.strict_loading_by_default = true end ``` Rails 7+ favorece la simplicidad (sin Webpack por defecto) y HTML-over-the-wire con Hotwire. Este enfoque reduce la complejidad de JavaScript ofreciendo una experiencia de usuario moderna. ## Conclusión Las entrevistas de Ruby on Rails evalúan el dominio del framework completo y la comprensión de sus convenciones. Puntos clave a recordar: ✅ **Fundamentos**: MVC, Active Record, migraciones, validaciones y asociaciones ✅ **Arquitectura**: Service Objects, Concerns, Query Objects y patrones CQRS ✅ **Rendimiento**: queries N+1, caching (fragment, Russian Doll, low-level), eager loading ✅ **Testing**: RSpec, FactoryBot, request specs y buenas prácticas de testing ✅ **Seguridad**: CSRF, inyección SQL, XSS, Strong Parameters y autenticación/autorización ✅ **APIs**: diseño RESTful, JWT, serializadores y versionado ✅ **Producción**: background jobs, WebSockets, despliegue y monitorización La filosofía Rails (Convention over Configuration, DRY y Rails Way) guía todas las decisiones arquitectónicas. Dominar estos principios y saber cuándo apartarse de ellos demuestra una experiencia sólida. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/ruby-on-rails/ruby-on-rails-interview-questions