¿Puede ActiveRecord::Relation recibir mensajes de métodos de clase? Sí

Ruby on Rails es una criatura enorme. Ofrece muchas funcionalidades y elementos que hacen fácil la vida de quien usa el framework bajo el nombre de magia. Magia que muchas veces no tenemos ni idea de cómo funciona.

Algunos podrán decir que esa magia es muy mala al ocultar cosas a quien desarrolla. Otros dirán que es buena justamente por lo mismo. En todo caso, siempre está la posibilidad de examinar el código fuente y ver cómo toda esa magia trabaja bajo cuerda.

Hace algunos días me topé con una de esas magias que sin querer sabía cómo funcionaba pero no había analizado tanto.

Métodos de Clase y Scopes

Los scopes en Rails son una buena forma de crear métodos para hacer consultas sin necesidad de definir un método de clase. Su sintaxis es más sencilla y en muchas ocasiones corta.

Uno de los beneficios explícitos de usarlos es poder encadenarlos para hacer una cadena de invocación:

class Algo < ActiveRecord::Base
  scope :default, -> { where(default: true }
  scope :available, -> { where(available: true }
end

Los scopes default y available pueden encadenarse uno tras otro:

Algo.default.available

Algo que con métodos de clase no es posible.

ActiveRecord::Relation

Bueno, resulta que en un proyecto en el que trabajo estaba este método de clase que parecía no tener uso.

class Taker < ActiveRecord::Base
  def self.to_csv
    # implementación
  end
end

Cuando en Sublime Text o Atom se hacía una búsqueda de Taker.to_csv no aparecía nada. Pensé que nada lo usaba. Permití que se borrara su test y esa implementación en el modelo.

Al par de días, un error aparece. ¿Qué será? Inicia el nuevo sprint, me asignan dicho bug y procedo a revisar qué ocurre. Como le habíamos hecho refactor a ese modelo, pensé que algo podría haberse movido a donde no correspondía así que empecé por lo obvio, comparar lo actual con un commit viejo pero no, no era por ahí.

Luego de analizar por un momento la situación, pensé en traer de nuevo ese viejo método borrado y probar. Y esa era la solución. Por alguna razón, ese método de clase era lo que faltaba.

Pero, ¿por qué un método de clase podría llamarse en una colección? Resulta que lo que llamaba al método era el resultado de una consulta con un .where. Uno creería que para usar métodos de clase habría que siempre usar el nombre de la clase y pasar el mensaje, es decir, el método. Resulta que no. Esa magia oculta de Rails lo hace posible.

La explicación está dada en Stack Overflow:

  1. By definition, model scopes return ActiveRecord::Relation objects
  2. By definition, scopes have access to class methods
  3. Therefore, ActiveRecord::Relation objects have access to class methods

Español:

  1. Por definición, los scopes retornan instancias de ActiveRecord::Relation
  2. Por definición, los scopes tienen acceso a los métodos de clase
  3. Por lo tanto, instancias ActiveRecord::Relation tienen acceso a métodos de clase

Ta da! Estuvo siempre sobre mis narices solo que no eran tan obvio.

El código culpable tenía la línea:

ts = filtered_ts.to_csv

En primera medida creíamos que ese .to_csv era algún método de Rails como tal y no el que estaba definido en el modelo. Cuando aparece el bug comprobamos que no era así y está dado por la explicación anterior.

Esta situación nos tomó por sorpresa. El bug no fue algo mayor y tuvimos la fortuna de aprender algo más de la mucha magia que hay en el framework.

Autor: cesc1989

Ingeniero de Sistemas que le gusta escribir y compartir sobre recursos que considera útiles, además que le gusta leer manga y ver anime.

Deja un comentario

Este sitio utiliza Akismet para reducir el spam. Conoce cómo se procesan los datos de tus comentarios.