Delegación de Lógica de Filtrado en Rails
En el desarrollo de aplicaciones Ruby on Rails, los controladores suelen saturarse con lógica condicional para manejar filtros de búsqueda, paginación y ordenamiento. La biblioteca has_scope resuelve este problema de arquitectura mapeando automáticamente los parámetros de las peticiones HTTP a los scopes (ámbitos) definidos en los modelos de Active Record. Esto elimina el código repetitivo y centraliza las reglas de consulta en la capa del modelo.
Configuración Inicial y Mapeo Básico
Para comenzar a utilizar esta herramienta, se declaran las reglas de mapeo directamente en el controlador mediante la macro has_scope. A continuación, se muestra un ejemplo utilizando un dominio de bienes raíces:
class PropertiesController < ApplicationController
has_scope :status
has_scope :has_pool, type: :boolean
has_scope :price_range, type: :hash, using: [:min, :max]
has_scope :amenities, type: :array
end
Una vez declarados, se invoca el método apply_scopes en la acción correspondiente para ejecutar la cadena de consultas:
def index
@properties = apply_scopes(Property).all
end
Con esta configuración, una petición a la URL /properties?status=available&has_pool=true&amenities[]=gym&amenities[]=parking se traducirá internamente en la siguiente ejecución de Active Recorrd:
Property.status('available').has_pool.amenities(['gym', 'parking'])
Configuraciones Avanzadas y Control de Flujo
1. Tipado de Parámetros
El mapeo inteligente requiere conocer la estructura de los datos entrantes. La opción :type define cómo se interpretan los parámetros:
- Booleanos: Evalúan la presencia del parámetro o valores de verdad, ideales para interruptores (toggles).
- Arrays: Agrupan múltiples valores con el mismo nombre de clave, útiles para filtros de categerías o etiquetas.
- Hashes: Desglosan objetos complejos. La opción
usingpermite extraer claves específicas y pasarlas como argumentos posicionales al scope.
2. Ejecución Condicional
Es posible restringir la aplicación de un scope basándose en el estado del controlador o de la petición utilizando :if y :unless:
has_scope :premium_only, type: :boolean,
if: ->(controller) { controller.current_user&.vip? },
unless: :internal_api_request?
3>Restricción por Acción
Para evitar que ciertos filtros se apliquen en contextos donde no tienen sentido, se utilizan las opciones :only y :except:
has_scope :include_archived, default: false, except: [:show, :edit]
has_scope :trending, default: true, only: :index
4. Alias de Parámetros
Si el nombre del parámetro en la API difiere del nombre del scope en el modelo, la opción :as permite crear un alias:
has_scope :property_type, as: :type
Esto permite que la URL use ?type=house mientras internamente se invoca el scope property_type.
Procesamiento Dinámico y Anidado
1. Lógica Personalizada con Bloques
Cuando la transformación del parámetro requiere lógica de negocio adicional antes de consultar la base de datos, se puede pasar un bloque al método:
has_scope :by_location do |controller, scope, value|
value.present? ? scope.near(value, 20) : scope
end
2. Parámetros Anidados
Para estructuras de parámetros más complejas, como las generadas por formularios de búsqueda avanzados, la opción :in extrae valores de un hash anidado:
has_scope :title, in: :search
has_scope :description, in: :search
Esto procesará correctamente peticiones como ?search[title]=villa&search[description]=ocean.
3. Valores por Defecto y Nulos
Se pueden establecer valores predeterminados estáticos o dinámicos (usando lambdas/procs) para garantizar que ciertos scopes siempre se ejecuten. Además, :allow_blank fuerza la ejecución del scope incluso si el parámetro está vacío:
has_scope :listed_after, default: -> { 1.month.ago }
has_scope :status, allow_blank: true
Consideraciones de Arquitectura y Rendimiento
- Consistencia de Nomenclatura: Mantenga una convención estricta entre los nombres de los parámetros de la API y los scopes del modelo para reducir la fricción cognitiva.
- Validación de Datos: Aunque la gema maneja el tipado básico, la validación de negocio y sanitización de entradas debe seguir realizándose en el modelo o mediante objetos de formulario (Form Objects).
- Optimización de Cnosultas: La combinación dinámica de múltiples scopes puede generar consultas SQL ineficientes. Asegúrese de que las columnas filtradas estén indexadas y considere el uso de
includesojoinsdentro de los scopes para evitar problemas de N+1. - Cobertura de Pruebas: Dado que la lógica de filtrado se mueve al controlador de forma implícita, es crucial escribir pruebas de integración (request specs) que verifiquen las combinaciones de parámetros de la URL y las consultas SQL resultantes.