Consultas en Carriles, Parte 3

En esta última pieza, vamos a profundizar un poco más en las consultas y jugar con algunos escenarios más avanzados. Cubriremos un poco más las relaciones de los modelos de Active Record en este artículo, pero me mantendré alejado de ejemplos que podrían ser demasiado confusos para los novatos de programación. Antes de seguir adelante, cosas como el ejemplo a continuación no deberían causar confusión:

Mission.last.agents.where (nombre: 'James Bond')

Si no está familiarizado con las consultas de Registro Activo y SQL, le recomiendo que eche un vistazo a mis dos artículos anteriores antes de continuar. Este podría ser difícil de tragar sin el conocimiento de que estaba construyendo tan lejos. Depende de usted, por supuesto. Por otro lado, este artículo no será tan largo como los otros si solo desea ver estos casos de uso ligeramente avanzados. Vamos a profundizar en! 

Los temas

  • Ámbitos y asociaciones
  • Uniones más delgadas
  • Unir
  • tiene muchos
  • Uniones personalizadas

Ámbitos y asociaciones

Reiteremos. Podemos consultar los modelos de Active Record de inmediato, pero las asociaciones también son un juego justo para las consultas, y podemos encadenar todas estas cosas. Hasta ahora tan bueno. También podemos incluir buscadores en ámbitos nítidos y reutilizables en sus modelos, y mencioné brevemente su similitud con los métodos de clase..

Carriles

agente de clase < ActiveRecord::Base belongs_to :mission scope :find_bond, -> where (nombre: 'James Bond') ámbito: licenced_to_kill, -> where (licence_to_kill: true) ámbito: mujeriego, -> where (mujeriego: verdadero) ámbito: jugador, -> donde (jugador: true) end # => Agent.find_bond # => Agent.licenced_to_kill # => Agent.womanizer # => Agent.gambler # => Mission.last.agents.find_bond # => Mission.last.agents.licenced_to_kill # = > Mission.last.agents.womanizer # => Mission.last.agents.gambler # => Agent.licenced_to_kill.womanizer.gambler # => Mission.last.agents.womanizer.gambler.licenced_to_kill

Así que también puedes empaquetarlos en tus propios métodos de clase y terminar con ellos. Los ámbitos no son dudosos ni nada, creo que, aunque la gente los menciona como algo mágico aquí y allá, pero como los métodos de clase logran lo mismo, optaría por eso.

Carriles

agente de clase < ActiveRecord::Base belongs_to :mission def self.find_bond where(name: 'James Bond') end def self.licenced_to_kill where(licence_to_kill: true) end def self.womanizer where(womanizer: true) end def self.gambler where(gambler: true) end end # => Agent.find_bond # => Agent.licenced_to_kill # => Agent.womanizer # => Agent.gambler # => Mission.last.agents.find_bond # => Mission.last.agents.licenced_to_kill # => Mission.last.agents. womanizer # => Mission.last.agents.gambler # => Agent.licenced_to_kill.womanizer.gambler # => Mission.last.agents.womanizer.gambler.licenced_to_kill

Estos métodos de clase se leen igual, y no necesitas apuñalar a nadie con un lambda. Lo que funcione mejor para ti o tu equipo; depende de usted qué API desea utilizar. ¡Simplemente no los mezcle y los combine, quédese con una sola opción! Ambas versiones le permiten encadenar fácilmente estos métodos dentro de otro método de clase, por ejemplo:

Carriles

agente de clase < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> where (womanizer: true) def self.find_licenced_to_kill_womanizer womanizer.licenced_to_kill end # => Agent.find_licenced_to_kill_womanizer # => Mission.last.agents.find_licenced.ind_licenced.inchina.

Carriles

agente de clase < ActiveRecord::Base belongs_to :mission def self.licenced_to_kill where(licence_to_kill: true) end def self.womanizer where(womanizer: true) end def self.find_licenced_to_kill_womanizer womanizer.licenced_to_kill end end # => Agent.find_licenced_to_kill_womanizer # => Mission.last.agents.find_licenced_to_kill_womanizer

Vamos a dar un paso más, quédate conmigo. Podemos usar un lambda en asociaciones para definir un cierto alcance. Parece un poco extraño al principio, pero pueden ser bastante útiles. Eso hace posible llamar a estos lambdas directamente en sus asociaciones.. 

Esto es bastante bueno y altamente legible con métodos de encadenamiento más cortos en marcha. Tenga cuidado de acoplar estos modelos demasiado apretados, aunque.

Carriles

clase de misión < ActiveRecord::Base has_many :double_o_agents, -> where (licence_to_kill: true), class_name: "Agent" end # => Mission.double_o_agents

¡Dime que esto no es genial de alguna manera! No es para uso diario, pero supongo que dope. Asi que aqui Misión puede "solicitar" solo a los agentes que tienen la licencia para matar. 

Una palabra acerca de la sintaxis, ya que nos apartamos de las convenciones de nombres y usamos algo más expresivo como double_o_agents. Necesitamos mencionar el nombre de la clase para no confundir a Rails, que de otra manera esperaría buscar una clase DoubleOAgent. Usted puede, por supuesto, tener ambos Agente asociaciones en su lugar, la habitual y la personalizada, y Rails no se quejará.

Carriles

clase de misión < ActiveRecord::Base has_many :agents has_many :double_o__agents, -> where (licence_to_kill: true), class_name: "Agent" end # => Mission.agents # => Mission.double_o_agents

Uniones más delgadas

Cuando consulta la base de datos en busca de registros y no necesita todos los datos, puede elegir especificar exactamente qué desea que se devuelva. ¿Por qué? Debido a que los datos devueltos al registro activo eventualmente se integrarán en nuevos objetos de Ruby. Veamos una estrategia simple para evitar la acumulación de memoria en su aplicación Rails:

Carriles

clase de misión < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end

Carriles

Agent.all.joins (: mission)

SQL

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

Así que esta consulta devuelve una lista de agentes con una misión de la base de datos a Active Record, que a su vez se establece para construir objetos Ruby a partir de ella. los misión los datos están disponibles ya que los datos de estas filas se unen a las filas de los datos del agente. Eso significa que los datos unidos están disponibles durante la consulta, pero no se devolverán al registro activo. Así que tendrás estos datos para realizar cálculos, por ejemplo.. 

Es especialmente bueno porque puedes usar datos que no se envían a tu aplicación. Menos atributos que deben integrarse en los objetos de Ruby, que ocupan memoria, pueden ser una gran ganancia. En general, piense en enviar solo las filas y columnas necesarias absolutas que necesita. De esa forma puedes evitar inflar bastante..

Carriles

Agent.all.joins (: mission) .where (misiones: objetivo: "Salvando el mundo")

Un breve resumen de la sintaxis aquí: porque no estamos consultando el Agente mesa via dónde, pero el unido :misión tabla, tenemos que especificar que estamos buscando específicos misiones en nuestro DÓNDE cláusula.

SQL

SELECCIONE "agentes". * DE "agentes" INNER JOIN "misiones" ON "misiones". "Id" = "agentes". "Mission_id" DONDE "misiones". "Object" =? [["objetivo", "Salvando el mundo"]]

Utilizando incluye Aquí también se devolverían las misiones a Active Record para cargar con ansias y ocupar memoria construyendo objetos de Ruby..

Unir

UNA unir resulta útil, por ejemplo, cuando queremos combinar una consulta sobre los agentes y sus misiones asociadas que tienen un alcance específico definido por usted. Podemos tomar dos ActiveRecord :: Relación Objetos y fusionar sus condiciones. Claro no biggie pero unir es útil si desea usar un cierto alcance mientras usa una asociación. 

En otras palabras, lo que podemos hacer con unir Se filtra por un ámbito con nombre en el modelo unido. En uno de los ejemplos anteriores, usamos métodos de clase para definir dichos ámbitos con nombre nosotros mismos.

Carriles

clase de misión < ActiveRecord::Base has_many :agents def self.dangerous where(enemy: "Ernst Stavro Blofeld") end end class Agent < ActiveRecord::Base belongs_to :mission end

Carriles

Agent.joins (: mission) .merge (Mission.dangerous)

SQL

SELECCIONE "agentes". * DE "agentes" UNIENDO INTERIOR "misiones" ON "misiones". "Id" = "agentes". "Mission_id" DONDE "misiones". "Enemigo" =? [["enemigo", "Ernst Stavro Blofeld"]]

Cuando encapsulamos lo que una peligroso la misión está dentro de la Misión modelo, podemos meterlo en una unirse vía unir de esta manera. Por lo tanto, trasladar la lógica de tales condiciones al modelo relevante al que pertenece es, por un lado, una buena técnica para lograr un acoplamiento más flexible. No queremos que nuestros modelos de Registro Activo sepan muchos detalles entre sí, y por el otro. mano, te da una buena API en tus uniones sin explotar en tu cara. El siguiente ejemplo sin combinación no funcionaría sin un error:

Carriles

Agent.all.merge (Mission.dangerous)

SQL

SELECCIONE "agentes". * DE "agentes" DÓNDE "misiones". "Enemigo" =? [["enemigo", "Ernst Stavro Blofeld"]]

Cuando ahora fusionamos un ActiveRecord :: Relación Para nuestras misiones en nuestros agentes, la base de datos no sabe de qué misiones estamos hablando. Necesitamos aclarar qué asociación necesitamos y unirnos a los datos de la misión primero o el SQL se confunde. Una última guinda en la parte superior. Podemos encapsular esto incluso mejor involucrando a los agentes también: 

Carriles

clase de misión < ActiveRecord::Base has_many :agents def self.dangerous where(enemy: "Ernst Stavro Blofeld") end end class Agent < ActiveRecord::Base belongs_to :mission def self.double_o_engagements joins(:mission).merge(Mission.dangerous) end end

Carriles

Agent.double_o_engagements

SQL

SELECCIONE "agentes". * DE "agentes" UNIENDO INTERIOR "misiones" ON "misiones". "Id" = "agentes". "Mission_id" DONDE "misiones". "Enemigo" =? [["enemigo", "Ernst Stavro Blofeld"]]

Eso es un poco de cereza dulce en mi libro. Encapsulación, OOP adecuada y gran legibilidad. Bote!

tiene muchos

Arriba hemos visto el pertenece a Asociación en acción mucho. Echemos un vistazo a esto desde otra perspectiva y traigamos secciones de servicios secretos a la mezcla:

Carriles

seccion de clase < ActiveRecord::Base has_many :agents end class Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission belongs_to :section end

Así que en este escenario, los agentes no solo tendrían un misión_id pero también un section_id. Hasta ahora tan bueno. Encontremos todas las secciones con agentes con una misión específica, así que las secciones que tienen algún tipo de misión en curso.

Carriles

Section.joins (: agentes)

SQL

SELECCIONA "secciones". * DE "secciones" INNER JOIN "agentes" ON "agentes". "Section_id" = "secciones." Id "

¿Notaste algo? Un pequeño detalle es diferente. Las llaves foráneas son volteadas. Aquí estamos solicitando una lista de secciones, pero usamos claves externas como esta: "agentes". "section_id" = "secciones." id ". En otras palabras, estamos buscando una clave externa de una tabla a la que nos unimos.

Carriles

Agent.joins (: mission)

SQL

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

Anteriormente nuestras uniones a través de un pertenece a La asociación se veía así: las claves foráneas estaban reflejadas ("misiones". "id" = "agentes". "id_misión") y estábamos buscando la clave externa de la tabla que estamos empezando.

Volviendo a tu tiene muchos En este caso, ahora obtendríamos una lista de secciones que se repiten porque tienen varios agentes en cada sección, por supuesto. Por lo tanto, para cada columna de agente que se une, obtenemos una fila para esta sección o sección_id-en breve, estamos duplicando filas básicamente. Para hacer que esto sea aún más vertiginoso, vamos a traer misiones a la mezcla también.

Carriles

Section.joins (agentes:: misión)

SQL

SELECCIONA "secciones". * DE "secciones" INNER JOIN "agentes" ON "agentes". "Section_id" = "secciones". "Id" INNER JOIN "misiones" ON "misiones". "Id" = "agentes". " mission_id "

Echa un vistazo a los dos UNIR INTERNAMENTE partes. ¿Aún conmigo? Estamos "llegando" a través de agentes a sus misiones desde la sección del agente. Sí, cosas por diversión dolores de cabeza, lo sé. Lo que obtenemos son misiones que se asocian indirectamente con una determinada sección.. 

Como resultado, obtenemos nuevas columnas unidas, pero el número de filas sigue siendo el mismo que devuelve esta consulta. Lo que se vuelve a enviar a Active Record, que resulta en la construcción de nuevos objetos de Ruby, también sigue siendo la lista de secciones. Entonces, cuando tenemos múltiples misiones en curso con múltiples agentes, obtendríamos filas duplicadas para nuestra sección nuevamente. Vamos a filtrar esto un poco más:

Carriles

Section.joins (agentes:: misión) .where (misiones: enemigo: "Ernst Stavro Blofeld") 

SQL

SELECCIONA "secciones". * DE "secciones" INNER JOIN "agentes" ON "agentes". "Section_id" = "secciones". "Id" INNER JOIN "misiones" ON "misiones". "Id" = "agentes". " mission_id "DONDE" misiones "." enemigo "= 'Ernst Stavro Blofeld'

Ahora solo recibimos las secciones devueltas que están involucradas en misiones donde Ernst Stavro Blofeld es el enemigo involucrado. Cosmopolitas, como algunos súper villanos podrían pensar en sí mismos, podrían operar en más de una sección; por ejemplo, las secciones A y C, los Estados Unidos y Canadá, respectivamente.. 

Si tenemos varios agentes en una sección determinada que están trabajando en la misma misión para detener a Blofeld o lo que sea, tendríamos que repetir las filas en Active Record. Seamos un poco más claros al respecto:

Carriles

Section.joins (agentes:: misión) .where (misiones: enemigo: "Ernst Stavro Blofeld"). Distinto

SQL

SELECCIONAR DISTINTO "secciones". * DE "secciones" INNER JOIN "agentes" ON "agentes". "Section_id" = "secciones". "Id" INNER JOIN "misiones" ON "misiones". "Id" = "agentes". "mission_id" DONDE "misiones". "enemigo" = 'Ernst Stavro Blofeld'

Lo que esto nos da es la cantidad de secciones con las que opera Blofeld, que son conocidos, y que tiene agentes activos en misiones con él como enemigo. Como paso final, volvamos a hacer un poco de refactorización. Extraemos esto en un bonito "pequeño" método de clase en seccion de clase:

Carriles

seccion de clase < ActiveRecord::Base has_many :agents def self.critical joins(agents: :mission).where(missions:  enemy: "Ernst Stavro Blofeld" ).distinct end end class Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission belongs_to :section end

Puede refactorizar esto aún más y dividir las responsabilidades para lograr un acoplamiento más flexible, pero avancemos por ahora.

Uniones personalizadas 

La mayoría de las veces puede confiar en que Active Record escriba el SQL que desea para usted. Eso significa que te quedas en Ruby land y no tienes que preocuparte demasiado por los detalles de la base de datos. Pero a veces necesitas hacer un agujero en la tierra SQL y hacer lo tuyo. Por ejemplo, si necesita usar un IZQUIERDA unirse y salir del comportamiento habitual de Active Record de hacer una INTERIOR unirse por defecto. se une Es una pequeña ventana para escribir su propio SQL personalizado si es necesario. Lo abre, conecta su código de consulta personalizado, cierra la "ventana" y puede continuar agregando métodos de consulta de Registro Activo.

Vamos a demostrar esto con un ejemplo que involucra gadgets. Digamos que un agente típico usualmente tiene muchos gadgets, y queremos encontrar agentes que no estén equipados con ningún gadget de lujo para ayudarles en el campo. Una combinación habitual no daría buenos resultados ya que estamos realmente interesados ​​en nulo-o nulo En SQL, los valores hablados de estos juguetes espía..

Carriles

clase de misión < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission has_many :gadgets end class Gadget < ActiveRecord::Base belongs_to :agent end

Cuando hacemos un se une operación, obtendremos solo agentes devueltos que ya están equipados con dispositivos porque el agent_id En estos gadgets no es nulo. Este es el comportamiento esperado de una combinación interna predeterminada. La unión interna se basa en una coincidencia en ambos lados y solo devuelve filas de datos que coinciden con esta condición. Un gadget inexistente con una nulo El valor para un agente que no lleva ningún gadget no coincide con ese criterio.. 

Carriles

Agent.joins (: gadgets)

SQL

SELECCIONE "agentes". * FROM "agentes" INNER JOIN "gadgets" ON "gadgets". "Agent_id" = "agents". "Id"

Por otro lado, estamos buscando agentes de mierda que necesitan un poco de amor por parte del intendente. Su primera suposición podría haber sido como la siguiente:

Carriles

Agent.joins (: gadgets) .where (gadgets: agent_id: nil) 

SQL

SELECCIONE "agentes". * FROM "agentes" INNER JOIN "gadgets" ON "gadgets". "Agent_id" = "agents". "Id" DONDE "gadgets". "Agent_id" ES NULA

No está mal, pero como se puede ver en la salida de SQL, no se reproduce y sigue insistiendo en el valor predeterminado UNIR INTERNAMENTE. Ese es un escenario donde necesitamos un EXTERIOR únete, porque falta un lado de nuestra "ecuación", por así decirlo. Estamos buscando resultados para gadgets que no existen, más precisamente, para agentes sin gadgets. 

Hasta ahora, cuando pasamos un símbolo a Active Record en una combinación, se esperaba una asociación. Con una cadena pasada, por otro lado, espera que sea un fragmento real de código SQL, una parte de su consulta.

Carriles

Agent.joins ("LEFT OUTER JOIN gadgets ON gadgets.agent_id = agents.id"). Where (gadgets: agent_id: nil)

SQL

SELECCIONE "agentes". * DE "agentes" IZQUIERDA COMENZAR A EXTREMO los gadgets EN gadgets.agent_id = agents.id DONDE "gadgets". "Agent_id" IS NULL

O, si tiene curiosidad acerca de los agentes perezosos sin misiones, posiblemente en Barbados o donde sea, nuestra unión personalizada se vería así:

Carriles

Agent.joins ("LEFT OUTER JOIN misiones ON misiones.id = agents.mission_id"). Donde (misiones: id: nil)

SQL

SELECCIONE "agentes". * DE "agentes" IZQUIERDA COMBINACIÓN EXTERNA misiones ON. Misiones.id = agents.mission_id DONDE "misiones". "Id" ES NULA

La unión externa es la versión de unión más inclusiva, ya que coincidirá con todos los registros de las tablas unidas, incluso si algunas de estas relaciones aún no existen. Debido a que este enfoque no es tan exclusivo como las uniones internas, obtendrá un montón de niles aquí y allá. Esto puede ser informativo en algunos casos, por supuesto, pero las uniones internas son, sin embargo, lo que estamos buscando. Rails 5 nos permitirá utilizar un método especializado llamado left_outer_joins en cambio para tales casos. Finalmente! 

Una pequeña cosa para el camino: mantenga estos agujeros que miran furtivamente en el terreno SQL lo más pequeño posible, si puede. Harás todo el mundo, incluido tu futuro yo, un tremendo favor..

Pensamientos finales

Obtener Active Record para escribir SQL eficiente para usted es una de las principales habilidades que debe quitar de esta miniserie para principiantes. De esa manera, también obtendrá un código compatible con cualquier base de datos que admita, lo que significa que las consultas serán estables en todas las bases de datos. Es necesario que entienda no solo cómo jugar con Active Record sino también el SQL subyacente, que es de igual importancia.. 

Sí, SQL puede ser aburrido, tedioso de leer y no tener un aspecto elegante, pero no olvide que Rails envuelve Active Record en torno a SQL y no debe dejar de entender esta pieza vital de tecnología, solo porque Rails hace que sea muy fácil no preocuparse por nada del tiempo. La eficiencia es crucial para las consultas de la base de datos, especialmente si creas algo para audiencias más grandes con mucho tráfico. 

Ahora, vaya a Internet y encuentre más material sobre SQL para sacarlo de su sistema de una vez por todas.!