Consultas en Rails, Parte 2

En este segundo artículo, profundizaremos un poco más en las consultas de Registro Activo en Rails. En caso de que aún sea nuevo en SQL, agregaré ejemplos que sean lo suficientemente simples como para que pueda etiquetar y retomar la sintaxis a medida que avanzamos. 

Dicho esto, definitivamente ayudaría si ejecuta un tutorial rápido de SQL antes de volver a leer. De lo contrario, tómese su tiempo para comprender las consultas de SQL que usamos, y espero que para el final de esta serie ya no se sienta intimidante.. 

La mayoría es muy sencillo, pero la sintaxis es un poco rara si acabas de comenzar con la codificación, especialmente en Ruby. Aguanta ahí, no es ciencia espacial!

Los temas

  • Incluye & Eager Loading
  • Uniendo Mesas
  • Eager Loading
  • Alcances
  • Agregaciones
  • Buscadores dinámicos
  • Campos específicos
  • SQL personalizado

Incluye & Eager Loading

Estas consultas incluyen más de una tabla de base de datos para trabajar y podrían ser las más importantes para eliminar de este artículo. Todo se reduce a esto: en lugar de realizar múltiples consultas para obtener información que se distribuye en varias tablas, incluye trata de mantener estos al mínimo. El concepto clave detrás de esto se llama "carga impaciente" y significa que estamos cargando objetos asociados cuando hacemos un hallazgo.

Si lo hiciéramos iterando sobre una colección de objetos y luego tratando de acceder a sus registros asociados desde otra tabla, nos encontraríamos con un problema que se llama "problema de consulta N + 1". Por ejemplo, para cada agente.handler en una colección de agentes, haríamos consultas separadas para ambos agentes y sus manejadores. Eso es lo que debemos evitar, ya que esto no se escala en absoluto. En su lugar, hacemos lo siguiente:

Carriles

agents = Agent.includes (: handlers)

Si ahora repasamos esa colección de agentes con descuento, no hemos limitado el número de registros devueltos por ahora, terminaremos con dos consultas en lugar de posiblemente un billón.. 

SQL

SELECCIONE "agentes". * DE "agentes" SELECCIONE "manejadores". * DE "manejadores" DONDE "manejadores". "Id" IN (1, 2)

Este agente de la lista tiene dos manejadores, y cuando ahora le pedimos al gestor de objetos que maneja sus manejadores, no es necesario realizar consultas de base de datos adicionales. Por supuesto, podemos llevar esto un paso más allá y podemos cargar varios registros de tabla asociados. Si tuviéramos que cargar no solo los manejadores, sino también las misiones asociadas del agente por cualquier motivo, podríamos usar incluye Me gusta esto.

Carriles

agents = Agent.includes (: handlers,: mission)

¡Sencillo! Solo tenga cuidado al usar versiones en singular y plural para las inclusiones. Dependen de sus asociaciones modelo. UNA tiene muchos la asociación usa plural, mientras que una pertenece a o un Tiene uno Necesita la versión singular, por supuesto. Si lo necesita, también puede colocar un dónde cláusula para especificar condiciones adicionales, pero la forma preferida de especificar condiciones para tablas asociadas que están ansiosas de cargar es mediante el uso de se une en lugar. 

Una cosa que se debe tener en cuenta sobre la carga impaciente es que los datos que se agregarán se enviarán de nuevo a Active Record, que a su vez genera objetos Ruby que incluyen estos atributos. Esto contrasta con "simplemente" unir los datos, donde obtendrá un resultado virtual que puede utilizar para cálculos, por ejemplo, y tendrá menos consumo de memoria que lo que incluye.

Uniendo Mesas

Unir tablas es otra herramienta que le permite evitar enviar demasiadas consultas innecesarias en el proceso. Un escenario común es unir dos tablas con una sola consulta que devuelve algún tipo de registro combinado. se une es solo otro método de búsqueda de registro activo que le permite en términos SQL-UNIRSE mesas. Estas consultas pueden devolver registros combinados de varias tablas, y obtiene una tabla virtual que combina registros de estas tablas. Esto es bastante radical cuando lo comparas con disparar todo tipo de consultas para cada tabla en su lugar. Hay algunos tipos diferentes de superposición de datos que puede obtener con este enfoque. 

La unión interna es el modus operandi predeterminado para se une. Esto coincide con todos los resultados que coinciden con un determinado ID y su representación como una clave externa de otro objeto o tabla. En el siguiente ejemplo, en pocas palabras: dame todas las misiones en las que la misión esté carné de identidad aparece como misión_id en la mesa de un agente. "agentes". "mission_id" = "misiones". "id". Las uniones internas excluyen las relaciones que no existen.

Carriles

Mission.joins (: agentes)

SQL

SELECCIONE "misiones". * FROM "misiones" INNER JOIN "agentes" ON "agentes". "Mission_id" = "mission". "Id"

Así que estamos haciendo coincidir las misiones y sus agentes acompañantes, ¡en una sola consulta! Claro, podríamos obtener las misiones primero, repasarlas una por una y preguntar por sus agentes. Pero luego volveríamos a nuestro terrible "problema de consulta N + 1". No gracias! 

Lo bueno de este enfoque también es que no obtendremos ningún caso con uniones internas; solo obtenemos registros devueltos que coincidan con sus ID con claves externas en tablas asociadas. Si necesitamos encontrar misiones, por ejemplo, que carezcan de agentes, necesitaríamos una unión externa en su lugar. Dado que esto actualmente implica escribir tu propio ÚNETE EXTERNO SQL, vamos a ver esto en el último artículo. De vuelta a las combinaciones estándar, también puede unir varias tablas asociadas, por supuesto.

Carriles

Mission.joins (: agentes,: gastos,: manejadores)

Y usted puede agregar a estos algunos dónde cláusulas para especificar sus buscadores aún más. A continuación, solo buscamos misiones ejecutadas por James Bond y solo los agentes que pertenecen a la misión 'Moonraker' en el segundo ejemplo..

Mission.joins (: agents) .where (agents: name: 'James Bond')

SQL

SELECCIONE "misiones". * DE "misiones" INNER JOIN "agentes" ON "agentes". "Mission_id" = "misiones". "Id" DONDE "agentes". "Nombre" =? [["nombre", "James Bond"]]

Carriles

Agent.joins (: mission) .where (misiones: mission_name: 'Moonraker')

SQL

SELECCIONE "agentes". * DE "agentes" INNER JOIN "misiones" ON "misiones". "Id" = "agentes". "Mission_id" DONDE "misiones". "Mission_name" =? [["mission_name", "Moonraker"]]

Con se une, También debe prestar atención al uso singular y plural de sus asociaciones modelo. Porque mi Misión clase has_many: agentes, Podemos usar el plural. Por otro lado, para el Agente clase Pertenece a: misión, Solo la versión singular funciona sin explotar. Pequeño detalle importante: el dónde La parte es más simple. Ya que está buscando múltiples filas en la tabla que cumplan una determinada condición, la forma plural siempre tiene sentido.

Alcances

Los alcances son una forma útil de extraer las necesidades comunes de consulta en métodos bien nombrados propios. De esa manera, son un poco más fáciles de transmitir y posiblemente también más fáciles de entender si otros tienen que trabajar con su código o si necesita volver a consultar ciertas consultas en el futuro. Puede definirlos para modelos individuales, pero utilizarlos para sus asociaciones también.. 

El cielo es realmente el límite.-se une, incluye, y dónde son todos juegos justos! Dado que los alcances también vuelven ActiveRecord :: Relaciones Objetos, puede encadenarlos y llamar a otros ámbitos sobre ellos sin dudar. Extraer ámbitos como ese y encadenarlos para consultas más complejas es muy útil y hace que los más largos sean más legibles. Los ámbitos se definen mediante la sintaxis de "stabby lambda":

Carriles

clase de misión < ActiveRecord::Base has_many: agents scope :successful, -> where (mission_complete: true) finaliza Mission.successful
agente de clase < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) ámbito: mujeriego, -> where (mujeriego: verdadero) ámbito: jugador, -> donde (jugador: verdadero) fin # Agent.gambler # Agent.womanizer # Agent.licenced_to_kill # Agent.womanizer.gambler Agent.licenced_to_kill.womanizer.gambler

SQL

SELECCIONE "agentes". * FROM "agentes" DONDE "agentes". "Licence_to_kill" =? Y "agentes". "Mujeriego" =? Y "agentes". "Jugador" =? [["licence_to_kill", "t"], ["mujeriego", "t"], ["jugador", "t"]]

Como puede ver en el ejemplo anterior, encontrar a James Bond es mucho mejor cuando puede simplemente encadenar ámbitos. De esa manera, puede mezclar y combinar varias consultas y mantenerse SECO al mismo tiempo. Si necesita ámbitos a través de asociaciones, también están a su disposición:

Mission.last.agents.licenced_to_kill.womanizer.gambler
SELECCIONE "misiones". * DE "misiones" ORDENAR POR "misiones". "Id" DESC LIMIT 1 SELECCIONE "agentes". * DE "agentes" DONDE "agentes". "Mission_id" =? Y "agentes". "Licence_to_kill" =? Y "agentes". "Mujeriego" =? Y "agentes". "Jugador" =? [["mission_id", 33], ["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]]

También puedes redefinir el default_scope para cuando estas mirando algo como Misión.todos.

clase de misión < ActiveRecord::Base default_scope  where status: "In progress"  end Mission.all

SQL

 SELECCIONE "misiones". * DE "misiones" DONDE "misiones". "Estado" =? [["estado en progreso"]]

Agregaciones

Esta sección no está tan avanzada en términos de la comprensión involucrada, pero las necesitará con más frecuencia que en escenarios que se pueden considerar un poco más avanzados que su promedio de búsqueda. .todos, .primero, .find_by_id o lo que sea. El filtrado basado en cálculos básicos, por ejemplo, es probablemente algo con lo que los novatos no se ponen en contacto de inmediato. ¿Qué estamos viendo exactamente aquí??

  • suma
  • contar
  • mínimo
  • máximo
  • promedio

Fácil peasy, ¿verdad? Lo bueno es que, en lugar de recorrer en bucle una colección de objetos devueltos para realizar estos cálculos, podemos dejar que Active Record haga todo este trabajo por nosotros y devuelva estos resultados con las consultas, preferiblemente en una consulta. Bien eh?

  • contar

Carriles

Mission.count # => 24

SQL

SELECCIONAR CUENTA (*) DE "misiones"
  • promedio

Carriles

Agent.average (: number_of_gadgets) .to_f # => 3.5

SQL

SELECCIONE AVG ("agentes". "Número_de_gadgets") DE "agentes"

Ya que ahora sabemos cómo podemos hacer uso de se une, podemos ir un paso más allá y solo pedir el promedio de dispositivos que los agentes tienen en una misión en particular, por ejemplo.

Carriles

Agent.joins (: mission) .where (misiones: name: 'Moonraker'). Average (: number_of_gadgets) .to_f # => 3.4

SQL

SELECCIONE AVG ("agentes". "Número_de_gadgets") DE "agentes" INNER JOIN "misiones" ON "misiones". "Id" = "agentes". "Mission_id" DONDE "misiones". "Nombre" =? [["nombre", "Moonraker"]]

Agrupar este número promedio de dispositivos por nombre de misión se vuelve trivial en ese punto. Vea más sobre la agrupación a continuación:

Carriles

Agent.joins (: mission) .group ('missions.name'). Average (: number_of_gadgets)

SQL

SELECCIONE AVG ("agentes". "Número_de_gadgets") COMO número promedio_de_ajustes, misiones.nombre AS misiones_nombre DE "agentes" UNIENDO INTERNA "Misiones" ON ". Misiones". "Id" = "agentes". "Mission_id" GRUPO POR misiones.nombre
  • suma

Carriles

Agent.sum (: number_of_gadgets) Agent.where (licence_to_kill: true) .sum (: number_of_gadgets) Agent.where.not (licence_to_kill: true) .sum (: number_of_gadgets)

SQL

SELECT SUM ("agentes". "Number_of_gadgets") DE "agentes" SELECT SUM ("agentes". "Number_of_gadgets") DE "agentes" DONDE "agentes". "Licence_to_kill" =? [["licence_to_kill", "t"]] SELECT SUM ("agents". "number_of_gadgets") FROM "agents" WHERE ("agents". "licence_to_kill"! =?) [["licence_to_kill", "t"]]
  • máximo

Carriles

Agent.maximum (: number_of_gadgets) Agent.where (licence_to_kill: true) .maximum (: number_of_gadgets) 

SQL

SELECT MAX ("agentes". "Number_of_gadgets") DE "agentes" SELECT MAX ("agentes". "Number_of_gadgets") DE "agentes" DONDE "agentes". "Licence_to_kill" =? [["licence_to_kill", "t"]]
  • mínimo

Carriles

Agent.minimum (: iq) Agent.where (licence_to_kill: true) .minimum (: iq) 

SQL

SELECCIONAR MIN ("agentes". "Iq") DE "agentes" SELECCIONAR MIN ("agentes". "Iq") DE "agentes" DONDE "agentes". "Licence_to_kill" =? [["licence_to_kill", "t"]]

Atención!

Todos estos métodos de agregación no le permiten encadenar otras cosas, son terminales. El orden es importante para hacer cálculos. No conseguimos un ActiveRecord :: Relación Rechace el objeto de estas operaciones, lo que hace que la música se detenga en ese punto. En su lugar obtenemos un hash o números. Los ejemplos a continuación no funcionarán:

Carriles

Agent.maximum (: number_of_gadgets) .where (licence_to_kill: true) Agent.sum (: number_of_gadgets) .where.not (licence_to_kill: true) Agent.joins (: mission) .average (: number_of_gadgets) .group ('misiones.name ')

Agrupados

Si desea que los cálculos se desglosen y se clasifiquen en grupos lógicos, debe hacer uso de un GRUPO Cláusula y no hagas esto en ruby. Lo que quiero decir con esto es que debes evitar iterar sobre un grupo que produce potencialmente toneladas de consultas.

Carriles

Agent.joins (: mission) .group ('misiones.nombre'). Average (: number_of_gadgets) # => "Moonraker" => 4.4, "Octopussy" => 4.9

SQL

SELECCIONE AVG ("agentes". "Número_de_gadgets") COMO número promedio_de_ajustes, misiones.nombre AS misiones_nombre DE "agentes" UNIENDO INTERNA "Misiones" ON ". Misiones". "Id" = "agentes". "Mission_id" GRUPO POR misiones.nombre

¡Este ejemplo encuentra todos los agentes que se agrupan en una misión en particular y devuelve un hash con el número promedio calculado de gadgets como sus valores en una sola consulta! ¡Sip! Lo mismo ocurre con los otros cálculos también, por supuesto. En este caso, realmente tiene más sentido dejar que SQL haga el trabajo. La cantidad de consultas que hacemos para estas agregaciones es demasiado importante.

Buscadores dinámicos

Para cada atributo en sus modelos, diga nombre, dirección de correo electrónicofavorito_gadget y así sucesivamente, Active Record le permite utilizar métodos de búsqueda muy legibles que se crean dinámicamente para usted. Suena críptico, lo sé, pero no significa nada más que find_by_id o find_by_favorite_gadget. los encontrar por la parte es estándar, y Active Record solo incluye el nombre del atributo para usted. Incluso puedes llegar a añadir un ! si quieres que el buscador genere un error si no se encuentra nada. La parte enferma es que incluso puedes encadenar estos métodos de búsqueda dinámica. Al igual que esto:

Carriles

Agent.find_by_name ('James Bond') Agent.find_by_name_and_licence_to_kill ('James Bond', verdadero)

SQL

SELECCIONE "agentes". * FROM "agentes" DONDE "agentes". "Nombre" =? LÍMITE 1 [["nombre", "James Bond"]] SELECCIONE "agentes". * DE "agentes" DÓNDE "agentes". "Nombre" =? Y "agentes". "Licence_to_kill" =? LÍMITE 1 [["nombre", "James Bond"], ["licence_to_kill", "t"]]

Por supuesto, puedes volverte loco con esto, pero creo que pierde su encanto y utilidad si vas más allá de dos atributos:

Carriles

Agent.find_by_name_and_licence_to_kill_and_womanizer_and_gambler_and_number_of_gadgets ('James Bond', true, true, true, 3) 

SQL

SELECCIONE "agentes". * FROM "agentes" DONDE "agentes". "Nombre" =? Y "agentes". "Licence_to_kill" =? Y "agentes". "Mujeriego" =? Y "agentes". "Jugador" =? Y "agentes". "Número_de_gadgets" =? LÍMITE 1 [["nombre", "James Bond"], ["licence_to_kill", "t"], ["mujeriego", "t"], ["jugador", "t"], ["number_of_gadgets", 3 ]]

En este ejemplo, sin embargo, es agradable ver cómo funciona debajo del capó. Cada nuevo _y_ agrega un SQL Y Operador para vincular lógicamente los atributos juntos. Sin embargo, en general, el principal beneficio de los buscadores dinámicos es la facilidad de lectura, ya que muchos atributos dinámicos pierden esa ventaja rápidamente. Raramente uso esto, tal vez sobre todo cuando juego en la consola, pero definitivamente es bueno saber que Rails ofrece este pequeño truco..

Campos específicos

Active Record le da la opción de devolver objetos que están un poco más centrados en los atributos que llevan. Por lo general, si no se especifica lo contrario, la consulta solicitará todos los campos en una fila a través de * (SELECCIONE "agentes". *), y luego Active Record construye objetos Ruby con el conjunto completo de atributos. Sin embargo, usted puede seleccionar solo los campos específicos que deben ser devueltos por la consulta y limitan la cantidad de atributos que sus objetos de Ruby necesitan "llevar".

Carriles

Agent.select ("nombre") => #, #,…]>

SQL

SELECCIONE "agentes". "Nombre" DE "agentes"

Carriles

Agent.select ("number, favorite_gadget") => #, #,…]>

SQL

SELECCIONE "agentes". "Número", "agentes". "Favorite_gadget" DE "agentes"

Como puede ver, los objetos devueltos solo tendrán los atributos seleccionados, más sus identificadores, por supuesto, es decir, con cualquier objeto. No hace ninguna diferencia si usa cadenas, como se muestra arriba, o símbolos: la consulta será la misma.

Carriles

Agent.select (: number_of_kills) Agent.select (: name,: licence_to_kill)

Una advertencia: si intenta acceder a los atributos del objeto que no ha seleccionado en sus consultas, recibirá una MissingAttributeError. Desde el carné de identidad Se le proporcionará automáticamente de todos modos, puede solicitar el ID sin seleccionarlo, aunque.

SQL personalizado

Por último, pero no menos importante, puede escribir su propio SQL personalizado a través de find_by_sql. Si tiene la suficiente confianza en su propio SQL-Fu y necesita algunas llamadas personalizadas a la base de datos, este método puede ser muy útil a veces. Pero esta es otra historia. Simplemente no olvide verificar primero los métodos de envoltorio de registro activo y evite reinventar la rueda donde Rails intenta encontrarse con usted más de la mitad.

Carriles

Agent.find_by_sql ("SELECT * FROM agents") Agent.find_by_sql ("SELECT name, licence_to_kill FROM agents") 

Como era de esperar, esto resulta en:

SQL

SELECT * FROM agents SELECT name, licence_to_kill FROM agents

Dado que los ámbitos y sus propios métodos de clase se pueden usar indistintamente para sus necesidades de buscador personalizado, podemos dar un paso más para consultas de SQL más complejas.. 

Carriles

agente de clase < ActiveRecord::Base… def self.find_agent_names query = <<-SQL SELECT name FROM agents SQL self.find_by_sql(query) end end

Podemos escribir métodos de clase que encapsulan el SQL dentro de un documento Here. Esto nos permite escribir cadenas multilínea de una manera muy legible y luego almacenar esa cadena SQL dentro de una variable que podemos reutilizar y pasar a find_by_sql. De esa manera no pegamos toneladas de código de consulta dentro de la llamada al método. Si tiene más de un lugar para usar esta consulta, también está SECO.

Dado que se supone que esto es para novatos y no un tutorial de SQL per se, mantuve el ejemplo muy minimalista por una razón. Sin embargo, la técnica para consultas mucho más complejas es la misma. Es fácil imaginar tener una consulta SQL personalizada allí que se extiende más allá de diez líneas de código. 

Ir tan loco como sea necesario, razonablemente! Puede ser un salvavidas. Una palabra sobre la sintaxis aquí. los SQL parte es solo un identificador aquí para marcar el principio y el final de la cadena. Apuesto a que no necesitarás mucho este método, ¡esperemos! Definitivamente tiene su lugar, y Rails Land no sería lo mismo sin él, en los raros casos en los que querrás afinar tu propio SQL con él..

Pensamientos finales

Espero que tenga un poco más cómodo escribiendo preguntas y leyendo el temido SQL en bruto. La mayoría de los temas que cubrimos en este artículo son esenciales para escribir consultas que traten con lógica de negocios más compleja. Tómese su tiempo para entender esto y juegue un poco con consultas en la consola. 

Estoy bastante seguro de que cuando deje atrás el tutorial, tarde o temprano su credibilidad de Rails aumentará significativamente si trabaja en sus primeros proyectos de la vida real y necesita elaborar sus propias consultas personalizadas. Si todavía eres un poco tímido con el tema, diría que simplemente diviértete con él, realmente no es una ciencia de cohetes.!