Consultas en Rails, parte 1
Spanish (Español) translation by Esther (you can also view the original English article)
En este artículo aprenderás los fundamentos de las consultas de Active Record y, de paso, aprenderás algunos aspectos básicos de SQL. Está dirigido a principiantes que quieran empezar a aprender más sobre consultas a bases de datos en Ruby on Rails.
Temas
- Objetos individuales
- Objetos múltiples
- Condiciones
- Pedidos
- Límites
- Grupo y Tener
Active Record se utiliza para consultar la base de datos. Puede utilizarse con SQL, PostgresSQL y SQLite. Para recuperar registros de tu base de datos, tienes varios métodos de búsqueda a tu disposición. Lo mejor de ellos es que puedes ahorrarte la molestia de escribir SQL en bruto.
¿Qué hace realmente un método de búsqueda? Básicamente tres cosas: las opciones proporcionadas se convierten en una consulta SQL. Luego la consulta SQL se ejecuta y recupera los datos de la base de datos. Además, por cada fila de esa lista de resultados, obtenemos objetos Ruby recién instanciados del modelo que se corresponde con la consulta.
Si no has jugado con SQL antes, haré lo posible por mantener las cosas simples y presentarte lo más básico. Sigue los ejemplos de SQL e intenta dar sentido a estas sencillas consultas. En realidad, SQL no es una ciencia espacial, solo hay que acostumbrarse a su sintaxis. Esperamos que esto te abra el apetito para buscar algunos tutoriales útiles que completen los espacios en blanco.
Veamos algunos métodos que están a tu disposición:
findfirstlastfind_byallfind_eachfind_in_batcheswhereorderlimitoffsetgrouphaving
Todas ellas devolverán una instancia de ActiveRecord::Relation. ¿Una qué? Es una clase que tiene un espacio de nombres dentro del módulo ActiveRecord, y nos permite llamar a múltiples métodos de consulta y encadenarlos. Este objeto es el corazón de la sintaxis de consulta utilizada en Rails. Comprobemos la clase de dicho objeto y veamos por nosotros mismos:
Rieles
1 |
Agent.where(name: 'James Bond').class |
2 |
|
3 |
# => ActiveRecord::Relation
|
Objetos individuales
find
Este método te permite proporcionar el id primario de un objeto y recupera ese único objeto para ti. Si proporcionas un array de ids, también puedes recuperar múltiples objetos.
Rieles
1 |
bond = Agent.find(7) |
SQL
1 |
SELECT "agents".* FROM "agents" WHERE "agents"."id" = ? LIMIT 1 [["id", 7]] |
Esta línea de SQL indica que quieres seleccionar todos los atributos (*) de la tabla de agentes y "filtrar" solo el registro con el id 7. Un límite hace que devuelva solo un registro de la base de datos.
-
first,last
Como es lógico, te proporcionarán el primer y el último registro que se puede identificar por su clave primaria. Sin embargo, lo interesante es que puedes proporcionar un número opcional que te devuelva el primero o el último de ese número de registros.
Rieles
1 |
enemy_agents = SpectreAgent.first(10) |
2 |
|
3 |
enemy_agents = SpectreAgent.last(10) |
Bajo el capó, está proporcionando un nuevo límite para el número que proporciona y ordenándolo de forma ascendente o descendente.
SQL
1 |
SELECT "spectreagents".* FROM "spectreagents" ORDER BY "spectreagents"."id" ASC LIMIT 10 |
2 |
|
3 |
SELECT "spectreagents".* FROM "spectreagents" ORDER BY "spectreagents"."id" DESC LIMIT 10 |
find_by
Este buscador devuelve el primer objeto que coincida con la condición que tú le indiques.
Rieles
1 |
bond = Agent.find_by(last_name: 'Bond') |
SQL
1 |
SELECT "agents".* FROM "agents" WHERE "agents"."last_name" = ? LIMIT 1 [["last_name", "Bond"]] |
Objetos múltiples
Obviamente, a menudo necesitamos iterar sobre una colección de objetos con alguna agenda. Recuperar un solo objeto o unos pocos seleccionados a mano está bien, pero la mayoría de las veces queremos que Active Record recupere objetos en lotes.
Mostrar a los usuarios todo tipo de listas es el pan de cada día para la mayoría de las aplicaciones Rails. Lo que necesitamos es una herramienta potente con una API práctica que recoja estos objetos por nosotros, a ser posible de una manera que nos permita evitar escribir nosotros mismos el SQL implicado la mayor parte del tiempo.
all
Rieles
1 |
mi6_agents = Agents.all |
SQL
1 |
SELECT "agents".* FROM "agents" |
Este método es útil para colecciones de objetos relativamente pequeñas. Intente imaginar que hace esto con una colección de todos los usuarios de Twitter. No, no es una buena idea. Lo que queremos en cambio es un enfoque más afinado para tamaños de tabla más grandes.
La búsqueda de toda la tabla no va a escalar. ¿Por qué? Porque no solo pediríamos un montón de objetos, sino que tendríamos que construir un objeto por cada fila de esta tabla y ponerlos en un array en memoria. Espero que esto no parezca una buena idea. ¿Cuál es la solución para esto? Los lotes. Estamos dividiendo estas colecciones en lotes que son más fáciles de procesar en la memoria. ¡Woohoo!
Echemos un vistazo a find_each y find_in_batches. Ambos son similares, pero se comportan de forma diferente en cuanto a la forma en la que ceden los objetos en bloques. Aceptan una opción para regular el tamaño del lote. El valor por defecto es 1.000.
find_each
Rieles
1 |
NewRecruit.find_each do |recruit| |
2 |
recruit.start_hellweek |
3 |
end |
SQL
1 |
SELECT "newrecruits".* FROM "newrecruits" ORDER BY "newrecruits"."id" ASC LIMIT 1000 |
En este caso, recuperamos un lote por defecto de 1.000 nuevos reclutas, los cedemos al bloque y los enviamos al infierno semana a semana. Dado que los lotes trocean las colecciones, también podemos decirles dónde empezar a través del inicio. Digamos que queremos procesar 3.000 posibles reclutas de una sola vez y queremos empezar por 4.000.
Rieles
1 |
NewRecruit.find_each(start: 4000, batch_size: 3000) do |recruit| |
2 |
recruit.start_hellweek |
3 |
end |
SQL
1 |
SELECT "newrecruits".* FROM "newrecruits" WHERE ("newrecruits"."id" >= 4000) ORDER BY "newrecruits"."id" ASC LIMIT 3000 |
Para reiterar, primero recuperamos un lote de 3.000 objetos Ruby y luego los enviamos al bloque. start nos permite especificar el id de los registros donde queremos empezar a recuperar este lote.
find_in_batches
Este cede su lote como un array al bloque, lo pasa a otro objeto que prefiere tratar con colecciones. El SQL es el mismo aquí.
Rieles
1 |
NewRecruit.find_in_batches(start: 2700, batch_size: 1350) do |recruits| |
2 |
field_kitchen.prepare_food(recruits) |
3 |
end |
Condiciones
where
Tenemos que repasar el "where" antes de continuar. Esto nos permite especificar condiciones que limitan el número de registros devueltos por nuestras consultas: un filtro para "where" recuperar registros en la base de datos. Si has jugado con las cláusulas WHERE de SQL, puede que te sientas como en casa: lo mismo ocurre con esta envoltura de Ruby.
En SQL, esto nos permite especificar a qué fila de la tabla queremos afectar, básicamente cuando cumple algún tipo de criterio. Esta es una cláusula opcional, por cierto. En el SQL en bruto de abajo, seleccionamos solo los reclutas que son huérfanos a través de WHERE.
Seleccionar una fila específica de una tabla.
SQL
1 |
SELECT * FROM Recruits |
2 |
WHERE FamilyStatus = 'Orphan'; |
A través de where, puedes especificar condiciones con cadenas, hashes o arrays. Con todo esto, Active Record te permite filtrar por condiciones como ésta:
Rieles
1 |
promising_candidates = Recruit.where("family_status = 'orphan'")
|
SQL
1 |
SELECT "recruits".* FROM "recruits" WHERE (family_status = 'orphan') |
Muy bonito, ¿verdad? Quiero mencionar que esto sigue siendo una operación de búsqueda, simplemente especificamos cómo queremos filtrar esta lista de inmediato. De la lista de todos los reclutas, esto devolverá una lista filtrada de candidatos huérfanos. Este ejemplo es una condición de cadena. Mantente alejado de las condiciones de cadena puras ya que no se consideran seguras debido a su vulnerabilidad a las inyecciones SQL.
Seguridad argumental
En el ejemplo anterior, ponemos la variable huérfana en la cadena con las condiciones. Esto se considera una mala práctica porque es insegura. Necesitamos escapar la variable para evitar esta vulnerabilidad de seguridad. Deberías leer sobre la inyección SQL si esto es totalmente nuevo para ti, tu base de datos podría depender de ello.
Rieles
1 |
promising_candidates = Recruit.where("family_status = ?", 'orphan'")
|
El ? será sustituido como valor de la condición por el siguiente valor de la lista de argumentos. Así que el signo de interrogación es básicamente un marcador de posición. También puedes especificar múltiples condiciones con múltiples ? y encadenarlas. En un escenario de la vida real, usaríamos un hash de parámetros como este:
1 |
promising_candidates = Recruit.where("family_status = ?", params[:recruits])
|
Si tienes un gran número de condiciones variables, debes utilizar condiciones de marcador de posición de clave/valor.
Rieles
1 |
promising_candidates = Recruit.where( |
2 |
"family_status = :preferred_status |
3 |
AND iq >= :required_iq |
4 |
AND charming = :lady_killer", |
5 |
{ preferred_status: 'orphan',
|
6 |
required_iq: 140, |
7 |
lady_killer: true |
8 |
} |
9 |
) |
SQL
1 |
SELECT "recruits".* FROM "recruits" WHERE (family_status = 'orphan' AND iq >= 140 AND lady_killer = true) |
El ejemplo anterior es una tontería, por supuesto, pero muestra claramente las ventajas de la notación de marcador de posición. La notación hash, en general, es definitivamente la más legible.
Rieles
1 |
promising_candidates = Recruit.where(family_status: 'orphan') |
2 |
|
3 |
promising_candidates = Recruit.where('charming': true)
|
Como puedes ver, puedes ir con símbolos o con cadenas, depende de ti. Vamos a cerrar esta sección con rangos y condiciones negativas a través de NOT.
Rieles
1 |
promising_candidates = Recruit.where(birthday: ('1994-01-01'..'2000-01-01'))
|
Dos puntos y puedes establecer cualquier rango que necesites.
1 |
promising_candidates = Recruit.where.not(character: 'coward') |
Puedes meter el not en el where para filtrar todos los cobardes y obtener solo los resultados que no tengan ese atributo específico y no deseado. Bajo el capó, un != niega el "filtro" WHERE.
SQL
1 |
SELECT "recruits".* FROM "recruits" WHERE ("recruits"."character" != ?) [["character", "coward"]] |
Pedidos
order
Para no aburrirte hasta la saciedad, vamos a hacer esto rápido.
1 |
candidates = Recruit.order(:date_of_birth) |
1 |
candidates = Recruit.order(:date_of_birth, :desc) |
Aplica :asc o :desc para ordenarlo como corresponde. Eso es básicamente todo, ¡así que sigamos!
Límites
limit
Puedes reducir el número de registros devueltos a un número determinado. Como ya se ha mencionado, la mayoría de las veces no necesitarás todos los registros devueltos. El ejemplo siguiente te dará los cinco primeros reclutas de la base de datos, los cinco primeros identificadores.
Rieles
1 |
five_candidates = Recruit.limit(5) |
2 |
SQL
1 |
SELECT "recruits".* FROM "recruits" LIMIT 5 |
offset
Si alguna vez te has preguntado cómo funciona la paginación bajo el capó, limit y offset, en conjunto, hacen el trabajo duro. limit puede valerse por sí mismo, pero offset depende del primero.
Establecer un desplazamiento es sobre todo útil para la paginación y te permite saltar el número deseado de filas en la base de datos. La página dos de una lista de candidatos podría buscarse así:
Rieles
1 |
Recruit.limit(20).offset(20) |
El SQL tendría el siguiente aspecto:
1 |
SELECT "recruits".* FROM "recruits" LIMIT 20 OFFSET 20 |
De nuevo, estamos seleccionando todas las columnas del modelo de base de datos Recruit, limitando los registros devueltos a 20 objetos Ruby de la clase Recruit y saltando sobre los 20 primeros.
Grupo y Tener
Digamos que queremos una lista de reclutas agrupados por su coeficiente intelectual. En SQL, esto podría ser algo así.
1 |
SELECT "recruits".* FROM "recruits" GROUP BY "recruits"."iq" |
De este modo se obtendría una lista en la que se vería qué posibles reclutas tienen un coeficiente intelectual de, digamos, 120, y luego otro grupo de, digamos, 140, y así sucesivamente, cualquiera que sea su coeficiente intelectual y cuántos estarían por debajo de un número específico. Así, cuando dos reclutas tienen el mismo coeficiente intelectual de 130, se agruparían.
Otra lista podría agruparse por posibles candidatos que sufran claustrofobia, miedo a las alturas o que no sean médicamente aptos para bucear. La consulta del registro activo sería simplemente así:
group
Rieles
1 |
Candidate.group(:iq) |
Cuando contamos el número de candidatos, obtenemos un hash muy útil.
Rieles
1 |
Candidate.group(:iq).count |
2 |
|
3 |
# => { 130=>7, 134=>4, 135=>3, 138=>2, 140=>1, 141=>1 }
|
Ya está: tenemos siete posibles reclutas con un CI de 130 y solo uno con 141. El SQL resultante sería así:
SQL
1 |
SELECT COUNT(*) AS count_all, iq AS iq FROM "candidates" GROUP BY "candidates"."iq" |
Lo importante es la parte de GROUP BY. Como puedes ver, utilizamos la tabla de candidatos para obtener sus ids. Lo que también puedes observar en este sencillo ejemplo es la mayor comodidad de lectura y escritura de las versiones de Active Record. Imagina hacer esto a mano en ejemplos más extravagantes. Claro, a veces hay que hacerlo, pero todo el tiempo es claramente una molestia que podemos evitar con gusto.
having
Podemos especificar aún más este grupo utilizando HAVING, una especie de filtro para el group. En ese sentido, having es una especie de cláusula WHERE para GROUP. En otras palabras, having depende del uso de group.
Rieles
1 |
Recruit.having('iq > ?', 134).group(:iq)
|
SQL
1 |
SELECT "recruits".* FROM "recruits" GROUP BY "recruits"."iq" HAVING iq > '134' |
Ahora hemos agrupado a nuestros candidatos en listas de personas que tienen un coeficiente intelectual mínimo de 135. Vamos a contarlos para obtener algunas estadísticas:
Rieles
1 |
Recruit.having('iq > ?', 134).group(:iq).count
|
2 |
|
3 |
# => { 135=>3, 138=>2, 140=>1, 141=>1 }
|
SQL
1 |
SELECT COUNT(*) AS count_all, iq AS iq FROM "recruits" GROUP BY "recruits"."iq" HAVING iq > '134' |
También podríamos mezclarlas y ver, por ejemplo, qué candidatos con un coeficiente intelectual superior a 140 tienen relaciones o no.
Rieles
1 |
Recruit.having('iq > ?', 140).group(:family_status) |
SQL
1 |
SELECT "recruits".* FROM "recruits" GROUP BY "recruits"."family_status" HAVING iq > '140' |
Contar estos grupos es ahora demasiado fácil:
Rieles
1 |
Recruit.having('iq > ?', 140).group(:family_status).count
|
2 |
|
3 |
# => { "married"=>2, "single"=>1 }
|
SQL
1 |
SELECT COUNT(*) AS count_all, family_status AS family_status FROM "recruits" GROUP BY "recruits"."family_status" HAVING iq > '140' |
Reflexiones finales
Espero que este haya sido un primer vistazo útil a lo que Active Record tiene que ofrecer para hacer que tus esfuerzos de consulta sean tan legibles y convenientes como sea posible. En general, diría que es un excelente envoltorio que te evita escribir SQL a mano la mayor parte del tiempo.
En el próximo artículo, analizaremos un par de buscadores más implicados y ampliaremos lo aprendido hasta ahora.



