La gem activerecord-multi-tenant facilita la implementación de arquitecturas multi-tenant en aplicaciones Ruby on Rails, especialmente cuando se utiliza PostgreSQL con la extensión Citus. A continuación, se detallan técnicas avanzadas para gestionar el contexto del tanant en controladores, integrar tareas en segundo plano con Sidekiq y ejecutar migraciones de datos progresivas mediante el modo de solo escritura.
Gestión del Contexto en Controladores
Para asegurar que cada solicitud HTTP opere dentro del aislamiento correcto, es fundamental configurar el tenant actual a nivel de controlador. Esto se logra mediante filtros que interceptan la petición y establecen el contexto global para ActiveRecord.
class BaseController < ActionController::Base
set_current_tenant_through_filter
before_action :resolve_and_set_organization
private
def resolve_and_set_organization
# Extraer el identificador de la sesión o del subdominio
org_id = session[:active_org_id] || extract_from_subdomain
organization = Organization.find(org_id)
set_current_tenant(organization)
end
end
Al invocar set_current_tenant, la gema almacena la referencia en una variable de hilo (MultiTenant.current_tenant). A partir de ese momento, cualquier consulta generada por ActiveRecord inyectará automáticamente la cláusula WHERE tenant_id = ?.
En paneles de administración donde se requiere acceso transversal, el contexto puede alternarse dinámicamente:
class SuperAdmin::DashboardController < BaseController
skip_before_action :resolve_and_set_organization
before_action :impersonate_tenant
private
def impersonate_tenant
if params[:target_tenant_id].present?
target = Organization.find(params[:target_tenant_id])
set_current_tenant(target)
end
end
end
Propagación de Contexto en Sidekiq
Las tareas asíncronas se ejecutan fuera del ciclo de vida de la solicitud HTTP, por lo que el contexto del tenant se pierde. Para solucionarlo, activerecord-multi-tenant proporciona middlewares para Sidekiq que serializan y deserializan el identificador del tenant en la cola de Redis.
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |cfg|
cfg.server_middleware do |chain|
chain.add Sidekiq::Middleware::MultiTenant::Server
end
cfg.client_middleware do |chain|
chain.add Sidekiq::Middleware::MultiTenant::Client
end
end
Sidekiq.configure_client do |cfg|
cfg.client_middleware do |chain|
chain.add Sidekiq::Middleware::MultiTenant::Client
end
end
Con esta configuración, los trabajos encolados heredarán el tenant activo:
class DataExportWorker
include Sidekiq::Worker
def perform(record_id)
# El contexto del tenant ya está restaurado automáticamente
record = FinancialRecord.find(record_id)
ExportService.new(record).process
end
end
# Encolar el trabajo manteniendo el aislamiento
MultiTenant.with(current_org) do
DataExportWorker.perform_async(target_record.id)
end
Para operaciones masivas que involucran múltiples tenants, se puede utilizar la API de inserción por lotes:
bulk_payload = {
'class' => 'DataExportWorker',
'jobs' => [
{ 'args' => [101], 'tenant_id' => 50 },
{ 'args' => [102], 'tenant_id' => 51 },
{ 'args' => [103], 'tenant_id' => 52 }
]
}
Sidekiq::Client.push_bulk_with_tenants(bulk_payload)
Migraciones Progresivas con el Modo de Solo Escritura
Adoptar una arquitectura multi-tenant en una base de datos existente requiere una transición cuidadosa. El modo de solo escritura (write-only mode) permite que las nuevas insecriones incluyan el tenant_id, mientras que las lecturas y actualizaciones ignoran temporalmente este filtro, evitando romper la aplicación durante el backfill de datos.
# config/initializers/multi_tenant_setup.rb
MultiTenant.enable_write_only_mode
Comportamiento comparativo:
- Modo Estándar: Inyecta
tenant_idenSELECT,INSERT,UPDATEyDELETE. - Modo Solo Escritura: Solo inyecta
tenant_iden operacionesINSERT. Las lecturas y modificaciones operan sobre todo el conjunto de datos.
Flujo de migración recomendado:
- Activar el modo de solo escritura en producción.
- Ejecutar un script de backfill para asignar el
tenant_ida los registros históricos. - Validar que no existan registros huérfanos (con
tenant_idnulo). - Desactivar el modo de solo escritura y aplicar restricciones de base de datos (
NOT NULL).
class HistoricalDataBackfill
def self.execute
Organization.find_each do |org|
MultiTenant.with(org) do
# Actualizar registros legacy sin disparar callbacks
LegacyTransaction.where(tenant_id: nil).update_all(tenant_id: org.id)
AuditLog.where(tenant_id: nil).update_all(tenant_id: org.id)
end
end
end
end
Optimización de Rendimiento y Monitoreo
La inyección automática de cláusulas WHERE puede afectar los planes de ejecución si no se indexa correctamente. Es obligatorio crear índices compuestos que prioricen el identificador del tenant.
class AddTenantIndexes < ActiveRecord::Migration[7.0]
def change
# El tenant_id debe ser la primera columna en el índice compuesto
add_index :financial_records, [:tenant_id, :status, :created_at]
add_index :audit_logs, [:tenant_id, :user_id]
end
end
Para diagnosticar consultas que no están siendo filtradas correctamente o para auditar el comportamiento de la gema, se puede habilitar el registro de consultas:
# Activar en entornos de desarrollo o staging
MultiTenant.enable_query_monitor = true
# Inspeccionar las consultas interceptadas
Rails.logger.info MultiTenant.current_tenant_query_log
Adicionalmente, integrar estrategias de caché a nivel de modelo reduce la carga en la base de datos distribuida:
class Workspace < ApplicationRecord
multi_tenant :organization
def active_users_count
Rails.cache.fetch("ws_#{id}_active_users", expires_in: 10.minutes) do
users.where(status: 'active').count
end
end
end