#239 ActiveRecord::Relation Walkthrough
Una de las funcionalidades más destacadas de Rails 3 es la nueva sintaxis de consultas de ActiveRecord. En el episodio 202 [verlo, leerlo] estudiamos esta nueva sintaxis con cierto detalle por lo que los lectores que no estén familiarizados con ella deberían leerse dicho episodio antes de empezar con este. Podría parecernos cuando utilicemos la nueva sintaxis que hay algo mágico funcionando por detrás, pero en este episodio veremos qué partes del código fuente de Rails entran en juego.
Descarga del código fuente
Si aún no tenemos a mano una copia del código fuente de Rails deberíamos descargar una copia para poder seguir este episodio. Lo único que tenemos que hacer es clonar el repositorio Git desde Github con la siguiente orden:
$ git clone git://github.com/rails/rails.git
Cuando el repositorio haya terminado de descargarse podemos cambiar a la versión que usaremos aquí haciendo un checkout de la rama apropiada.
$ git checkout v3.0.1
Nos interesará sobre todo el código de ActiveRecord, por lo que nos cambiaremos a dicho directorio.
$ cd activerecord/lib/active_record
El código de ActiveRecord es enorme y se encuentra repartido a lo largo de un número de archivos; en este episodio sólo veremos unos pocos.
$ ls -F aggregations.rb nested_attributes.rb association_preload.rb observer.rb associations/ persistence.rb associations.rb query_cache.rb attribute_methods/ railtie.rb attribute_methods.rb railties/ autosave_association.rb reflection.rb base.rb relation/ callbacks.rb relation.rb connection_adapters/ schema.rb counter_cache.rb schema_dumper.rb dynamic_finder_match.rb serialization.rb dynamic_scope_match.rb serializers/ errors.rb session_store.rb fixtures.rb test_case.rb locale/ timestamp.rb locking/ transactions.rb log_subscriber.rb validations/ migration.rb validations.rb named_scope.rb version.rb
Experimentos en la consola
Antes de sumergirnos en el código vamos a hacernos una idea de qué estamos buscando haciendo algunos sencillos experimentos con la consola de una aplicación Rails 3. Se trata de una sencilla aplicación de tareas pendientes que tiene varios modelos Task
. Podemos recuperar todas las tareas utilizando Task.all
.
> Task.all => [#<Task id: 1, project_id: 1, name: "paint fence", completed_at: nil, created_at: "2010-11-08 21:25:05", updated_at: "2010-11-08 21:32:21", priority: 2>, #<Task id: 2, project_id: 1, name: "weed garden", completed_at: nil, created_at: "2010-11-08 21:25:29", updated_at: "2010-11-08 21:27:04", priority: 3>, #<Task id: 3, project_id: 1, name: "mow lawn", completed_at: nil, created_at: "2010-11-08 21:25:37", updated_at: "2010-11-08 21:26:42", priority: 3>]
La nueva sintaxis de consultas en ActiveRecord hace que sea muy sencillo, por ejemplo, recuperar todas las tareas que tengan una prioridad de 3
.
> Task.where(:priority => 3) => [#<Task id: 2, project_id: 1, name: "weed garden", completed_at: nil, created_at: "2010-11-08 21:25:29", updated_at: "2010-11-08 21:27:04", priority: 3>, #<Task id: 3, project_id: 1, name: "mow lawn", completed_at: nil, created_at: "2010-11-08 21:25:37", updated_at: "2010-11-08 21:26:42", priority: 3>]
Lo que ha devuelto esta consulta tiene el aspecto de una lista de registros pero si invocamos class
resulta que es en realidad una instancia de ActiveRecord::Relation
.
> Task.where(:priority => 3).class => ActiveRecord::Relation
Si añadimos otra opción a la consulta e invocamos class
veremos que se trata de un objeto del mismo tipo.
> Task.where(:priority => 3).limit(2).class => ActiveRecord::Relation
La clase Relation
El que las consultas devuelvan un objeto ActiveRecord::Relation
es lo que nos permite encadenar consultas seguidas, y lógicamente la clase Relation
está en el centro de la nueva sintaxis. Aprenderemos más de esta clase buscando en el directorio de código fuente de ActiveRecord un fichero llamado relation.rb
.
Al comienzo de la clase se definen un número de constantes, una de las cuales es un Struct
(para los que no estén familiarizados con esta clase, se trata de una forma rápida de definir una clase dinámicamente pasándole una lista de atributos en el constructor).
require 'active_support/core_ext/object/blank' module ActiveRecord # = Active Record Relation class Relation JoinOperation = Struct.new(:relation, :join_class, :on) ASSOCIATION_METHODS = [:includes, :eager_load, :preload] MULTI_VALUE_METHODS = [:select, :group, :order, :joins, :where, :having] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :create_with, :from] include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches
Seguidamente la clase incluye varios módulos que suministran la mayor parte de la funcionalidad de la clase. Dichos módulos están en el directorio relation/
dentro del directorio active_record
. Vamos a echarle un vistazo a uno de estos módulos: query_methods.rb
.
Esta clase contiene los métodos que utilizaremos en la nueva sintaxis de consultas: includes
, select
, group
, order
, joins
, etcétera. Todos estos métodos se comportan de forma parecida, invocando a clone
. Lo que hace esto es clonar el objeto Relation
, devolviendo un nuevo objeto Relation
en lugar de modificar el ya existente. Después se invoca tap
en el objeto clonado, lo que devuelve el objeto tras ejecutar el bloque sobre él. En cada bloque añadimos los argumentos recibidos en el método al conjunto apropiado de valores del objeto Relation
.
def group(*args) clone.tap {|r| r.group_values += args.flatten if args.present? } end def order(*args) clone.tap {|r| r.order_values += args if args.present? } end def reorder(*args) clone.tap {|r| r.order_values = args if args.present? } end
Así que cuando antes en la consola invocamos Task.where:(priority =>3)
nos devolvió una instancia de Relation
, y cuando sobre dicho objeto invocamos limit(2)
, se invocó al método limit
del módulo QueryMethods
que nos devuelve una versión clonada del objeto Relation
. Pero, ¿qué pasó con la llamada inicial a where
? Sabemos que limit
está siendo invocado sobre un objeto Relation
pero la llamada a where
se lanza directamente sobre el modelo Task
y por tanto en ActiveRecord::Base
en lugar de Relation
. ¿Dónde se crea el objeto Relation
original?
Para encontrar la respuesta tenemos que indagar más en el código fuente de ActiveRecord. Si buscamos “def where
” sólo lo encontraremos en el módulo QueryMethods
que ya estamos viendo. Tampoco encontraremos “def self.where
”. Pero hay una forma distinta de definir métodos con delegate
así que buscaremos en el código esta expresión: “delegate.+ :where
” y encontraremos resultados sumamente interesantes.
El segundo resultado delega un gran número de métodos de consulta, así que parece que es lo que estábamos buscando.
delegate :select, :group, :order, :reorder, :limit, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped
En esta línea aparecen todos los métodos de consulta, que se delegan a scoped
. Y ¿qué hace scope
? Si buscamos otra vez en el proyecto veremos que este método aparece en el fichero named_scope.rb
.
El módulo NamedScope
se incluye en ActiveRecord::Base
, por lo que tenemos acceso a todos sus métodos. El método scoped
es bastante sencillo, pues sólo invoca a relation
para mezclar las opciones que haya recibido.
def scoped(options = nil) if options scoped.apply_finder_options(options) else current_scoped_methods ? relation.merge (current_scoped_methods) : relation.clone end end
Veamos el método relation
, que ha sido definido en ActiveRecord::Base
.
private def relation #:nodoc: @relation ||= Relation.new(self, arel_table) finder_needs_type_condition? ? @relation.where(type_condition) : @relation end
Aquí es donde se instancia el objeto Relation
. Se le pasa self
, que es una clase de modelo ActiveRecord y arel_table
, que es un objeto de tipo Arel::Table
, y devuelve dicha Relation
(la otra línea añade una condición where
extra para el caso de herencias de tabla única). El método arel_table
está definido en la misma clase y tan sólo crea un objeto de clase Arel::Table
.
def arel_table @arel_table ||= Arel::Table.new(table_name, arel_engine) end
Arel
Ahora la siguiente pregunta es: ¿qué es Arel? Es una dependencia externa, por lo que no la encontraremos en el código fuente de Rails, pero su código fuente se encuentra en Github. Arel es una librería que simplifica la generación de consultas complejas de SQL, y es para eso para lo que la utiliza ActiveRecord:
users.where(users[:name].eq('amy')) # => SELECT * FROM users WHERE users.name = 'amy'
Ahora que ya sabemos lo que es Arel::Table
podemos volver al método relation
. Devuelve un objeto Relation
, por lo que nos iremos a la definición de esta clase, cuyo inicializador tan sólo recibe la clase y tabla que se le pasan y los almacena en una variable de instancia.
Volviendo a la consola de Rails, ya sabemos qué es lo que ocurre cuando se invoca
Task.where(:priority => 3).limit(2).class
Al invocar where
sobre el modelo se crea un nuevo objeto Relation
sobre el cual volvemos a invocar limit
lo que hace que se clone la relación con argumentos adicionales en el objeto clonado. Cuando se invoca class
no se ejecuta la consulta, pero si quitamos .class
al final de la línea, la consulta será lanzada y veremos la lista de objetos devueltos.
> Task.where(:priority => 3).limit(2) => [#<Task id: 2, project_id: 1, name: "weed garden", completed_at: nil, created_at: "2010-11-08 21:25:29", updated_at: "2010-11-08 21:27:04", priority: 3>, #<Task id: 3, project_id: 1, name: "mow lawn", completed_at: nil, created_at: "2010-11-08 21:25:37", updated_at: "2010-11-08 21:26:42", priority: 3>]
Comprobamos que la consulta termina lanzándose de alguna manera: lo que ocurre entre bambalinas es que se invoca al método inspect
sobre la orden que se está ejecutando. Relation
redefine el método inspect
. Veamos qué aspecto tiene.
def inspect to_a.inspect end
Lo único que hace el método inspect
es invocar a to_a.inspect
sobre la relación. Rastreando el código en Relation
podemos ver que el método to_a
se define así:
def to_a return @records if loaded? @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql) preload = @preload_values preload += @includes_values unless eager_loading? preload.each {|associations| @klass.send(:preload_associations, @records, associations) } # @readonly_value is true only if set explicitly. @implicit_readonly is true if there # are JOINS and no explicit SELECT. readonly = @readonly_value.nil? ? @implicit_readonly : @readonly_value @records.each { |record| record.readonly! } if readonly @loaded = true @records end
Este método devuelve los registros si ya existen, de lo contrario los recupera y los devuelve. La parte más interesante de este método es la que recupera los métodos: @klass.find_by_sql(arel.to_sql)
. Este código ejecuta find_by_sql
en el modelo, en nuestro caso el modelo Task
, pasándole arel.to_sql
. El método arel
está definido el módulo QueryMethods
que vimos anteriormente, y lo único que hace es invocar a otro método llamado build_arel
y cachear los resultados en una variable de instancia:
def build_arel arel = table arel = build_joins(arel, @joins_values) unless ↵ @joins_values.empty? (@where_values - ['']).uniq.each do |where| case where when Arel::SqlLiteral arel = arel.where(where) else sql = where.is_a?(String) ? where : where.to_sql arel = arel.where(Arel::SqlLiteral.new("(#{sql})")) end end arel = arel.having(*@having_values.uniq.select{|h| h.present?}) unless @having_values.empty? arel = arel.take(@limit_value) if @limit_value arel = arel.skip(@offset_value) if @offset_value arel = arel.group(*@group_values.uniq.select{|g| g.present?}) unless @group_values.empty? arel = arel.order(*@order_values.uniq.select{|o| o.present?}) unless @order_values.empty? arel = build_select(arel, @select_values.uniq) arel = arel.from(@from_value) if @from_value arel = arel.lock(@lock_value) if @lock_value arel end
Este método recupera el objeto Arel::Table
que vimos anteriormente y luego construye la consulta, convirtiendo todos los datos que hemos ido arrastrando en el objeto Relation
en una consulta de Arel, la cual se termina devolviendo. Una vez en la clase Relation
, el método to_a
invoca to_sql
sobre esta consulta Arel para convertirla a SQL y luego invoca find_by_sql
sobre el modelo para que se devuelva el correspondiente array de registros.
Con este conocimiento básico de cómo funciona la clase ya podemos explicarnos cómo trabajan muchos otros métodos simplemente viendo el código de Relation
. Por ejemplo el método create
llama a otro método llamado scoping
y llama a create
sobre la @klass
. Esto creará una nueva instancia de un modelo y el método de ámbito se añadirá él solo a los métodos de ámbito de @klass
. Lo que esto quiere decir es que cualquier cosa que se ejecute sobre un bloque de ámbito tendrá su ámbito restringido como si se invocase directamente sobre dicho objeto de relación. Merece la pena explorar el resto de módulos, especialmente QueryMethods
. Hay algunos métodos que tal vez nos sean nuevos como por ejemplo reorder
, que reiniciará todos los argumentos de ordenación en lugar de añadir uno nuevo como hace order
.
def order(*args) clone.tap {|r| r.order_values += args if args.present? } end def reorder(*args) clone.tap {|r| r.order_values = args if args.present? } end
También hay un método reverse_order
que ordenará al revés el resultado de la cláusula order
.
El módulo Calculations
contiene métodos para realizar cálculos sobre los campos de un modelo tales como average
, minimum
y maximum
. El módulo SpawnMethods
es interesante porque permite manipular objetos Relation
diferentes (por ejemplo para mezclar dos relaciones) y existen también los métodos except
y only
con los cuales todavía no hemos tenido tiempo de experimentar, la mejor manera de hacerlo será lanzando una consola de aplicación Rails 3 y probar estos métodos para ver qué hacen. Se pueden descubrir muchas técnicas interesantes simplemente leyendo el código fuente y haciendo pruebas con los métodos que vayamos encontrando.
Con esto cerramos este episodio en el que hemos visto ActiveRecord::Relation
por dentro. Os aninamos a explorar el código fuente de Rails y experimentar con los métodos que os parezcan interesantes.