Ya sabemos que mediante el uso de Associations podemos conseguir eliminar el problema de las n+1 selects cuando queremos cargar datos de una tabla y su tabla asociada. Esto es lo que se conoce como eager loading.
Siguiendo la documentación, está claro que se puede hacer include de n tablas de golpe haciendo :include =>[tabla1,tabla2], y obtendremos datos tanto de nuestra tabla original como de las tablas tabla1 y tabla2. Un punto a tener en cuenta es que el tipo de joins que se hacen son LEFT OUTER, por lo que obtendremos todos los registros de la tabla original y solamente los que tengan datos de las tablas relacionadas.
Hasta aquí, todo perfecto cuando quieres hacer una select con tablas que están relacionadas con tu modelo directamente; pero si quieres hacer de una tacada una query que incluya las tablas relacionadas y a su vez las tablas relacionadas de éstas, no parece que sea posible. No al menos mirando solamente la documentación.
Sin embargo, una de las cosas interesantes del open source es que le podemos echar un vistazo al código fuente y ver qué es lo que hace exactamente, teniendo de esta forma la mejor documentación posible.
Tras una investigación alrededor de ActiveRecord(*ver más abajo), encontramos que, cuando se definen asociaciones, a la hora de montar la query se diferencia entre tres casos diferentes: si la asociación es un Symbol/String, si es un Array o si es una Hash. Aquí es donde está la gracia. En la documentación sólo nos explican el caso de Symbol/String y de Array, pero… y si le paso una Hash?.
Si le paso una Hash, consigo justamente lo que quería, poder incluir en la query las tablas relacionadas y las relacionadas con ellas. La sintáxis ,no tan obvia, para esto es la siguiente:
:include=>[{:tabla1=>[:tabla1_A,:tabla1_B]}]
Esto me carga las relaciones de mi modelo con tabla1, y a su vez las de tabla1 con tabla1_A y con tabla1_B, ahorrándonos bastante carga contra la base de datos.
* A continuación muestro el extracto de la clase JoinDependency, definida en Associations.rb donde se lleva a cabo la magia de las asociaciones
def construct(parent, associations, joins, row)
case associations
when Symbol, String
while (join = joins.shift).reflection.name.to_s != associations.to_s
raise ConfigurationError, “Not Enough Associations” if joins.empty?
end
construct_association(parent, join, row)
when Array
associations.each do |association|
construct(parent, association, joins, row)
end
when Hash
associations.keys.sort{|a,b|a.to_sb.to_s}.each do |name|
association = construct_association(parent, joins.shift, row)
construct(association, associations[name], joins, row) if association
end
else
raise ConfigurationError, associations.inspect
end
end
def construct_association(record, join, row)
case join.reflection.macro
when :has_many, :has_and_belongs_to_many
collection = record.send(join.reflection.name)
collection.loaded
return nil if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
association = join.instantiate(row)
collection.target.push(association) unless collection.target.include?(association)
when :has_one, :belongs_to
return if record.id.to_s != join.parent.record_id(row).to_s or row[join.aliased_primary_key].nil?
association = join.instantiate(row)
record.send(“set_#{join.reflection.name}_target”, association)
else
raise ConfigurationError, “unknown macro: #{join.reflection.macro}”
end
return association
end
searchwords: include, nested include, chained include, eager loading, associations, JoinDependency