¿Cómo escribo un método de buscador de Rails donde ninguno de los elementos has_many tiene un campo no nulo?

8

Estoy usando Rails 5. Tengo el siguiente modelo ...

class Order < ApplicationRecord
    ...
    has_many :line_items, :dependent => :destroy

El modelo LineItem tiene un atributo, "discount_applied". Me gustaría devolver todos los pedidos donde hay cero instancias de una línea de pedido cuyo campo "discount_applied" no sea nulo. ¿Cómo escribo un método de búsqueda de este tipo?

Dave
fuente
¿Qué RDBMS estás usando? ¿Está usando SQL "en bruto" una opción?
Sebastián Palma
La pregunta es un poco confusa. Entonces, ¿desea esencialmente todos los pedidos en los que los pedidos de línea asociados tienen un descuento aplicado de cero?
bwalshy
@bwalshy, quiero todos los pedidos que no tengan líneas de pedido en las que un campo discount_applied no sea nulo. Esto incluiría pedidos sin líneas de pedido, pedidos con una sola línea de pedido donde discount_applied es nulo, o pedidos con dos líneas de pedido donde ambos campos discount_applied son nulos, o pedidos con tres líneas de pedido ... Creo que se entiende la idea.
Dave

Respuestas:

0

No es eficiente, pero pensé que podría resolver su problema:

orders = Order.includes(:line_items).select do |order|
  order.line_items.all? { |line_item| line_item.discount_applied.nil? }
end

Actualización :

En lugar de encontrar pedidos en los que todas sus líneas de pedido no tienen descuento, podemos excluir todos los pedidos que tienen líneas de pedido con un descuento aplicado del resultado de salida. Esto se puede hacer con la subconsulta dentro de la cláusula where:

# Find all ids of orders which have line items with a discount applied:
excluded_ids = LineItem.select(:order_id)
                       .where.not(discount_applied: nil)
                       .distinct.map(&:order_id)

# exclude those ids from all orders:
Order.where.not(id: excluded_ids)

Puedes combinarlos en un único método de búsqueda:

Order.where.not(id: LineItem
                    .select(:order_id)
                    .where.not(discount_applied: nil))

Espero que esto ayude

Mosaaleb
fuente
Gracias por esto @Mosaaleb: la lógica parece funcionar mientras lo pruebo. Con respecto a la actualización que realizó, ¿alguna forma de combinar esas dos cosas en un solo método de búsqueda?
Dave
Hola Dave. Sí, seguro que puedes combinarlos, he actualizado mi respuesta.
Mosaaleb
2

En primer lugar, esto realmente depende de si desea utilizar un enfoque Arel puro o si está bien usar SQL. El primero es IMO solo aconsejable si tiene la intención de construir una biblioteca, pero innecesario si está creando una aplicación donde, en realidad, es muy poco probable que cambie su DBMS en el camino (y si lo hace, cambie un puñado de las consultas manuales probablemente serán el menor de sus problemas).

Suponiendo que usar SQL está bien, la solución más simple que debería funcionar en casi todas las bases de datos es esta:

Order.where("(SELECT COUNT(*) FROM line_items WHERE line_items.order_id = orders.id AND line_items.discount_applied IS NULL) = 0")

Esto también debería funcionar prácticamente en todas partes (y tiene un poco más de Arel y menos SQL manual):

Order.left_joins(:line_items).where(line_items: { discount_applied: nil }).group("orders.id").having("COUNT(line_items.id) = 0")

Dependiendo de su DBMS específico (más específicamente: su respectivo optimizador de consultas), uno u otro podría ser más eficiente.

Espero que ayude.

Clemens Kofler
fuente
Estoy usando PostGres, pero ¿hay alguna manera de hacer que este RDBMS sea neutral?
Dave
Permítanme reformular mi respuesta anterior para mayor claridad: mis dos ejemplos deberían funcionar en todos los DBMS que son compatibles con SQL, pero es posible que no se ejecuten de la manera más optimizada porque Arel a veces hace cosas mágicas para reorganizar consultas individuales DBMS. Para Postgres, en mi experiencia no importa porque su optimizador de consultas es lo suficientemente inteligente como para hacer su trabajo de cualquier manera.
Clemens Kofler
Oh, tengo @ClemensKofler: sí, el SQL que tienes me parece estándar y el rendimiento no importa en esta etapa. Una pregunta, en las soluciones que tiene arriba, ¿dónde tiene en cuenta la restricción de que todos los campos aplicados con descuento de LineItems deben ser nulos?
Dave
He agregado el condicional a ambos sabores. Tenga en cuenta que no he probado esto y solo está escrito a partir del conocimiento de cómo debería funcionar.
Clemens Kofler
0

Un posible código

Order.includes(:line_items).where.not(line_items: {discount_applied: nil})

Aconsejo familiarizarse con la documentación de AR para los métodos de consulta.

Actualizar

Sin embargo, esto parece estar más interesado que yo inicialmente. Y más complicado, así que no podré darte un código de trabajo. Pero buscaría una solución usando LineItem.group(order_id).having(discount_applied: nil), que debería darle una colección de line_items y luego usarla como subconsulta para encontrar órdenes relacionadas.

adass
fuente
Gracias pero esto no funciona. Devuelve resultados en los que un pedido tendrá algunas líneas de pedido con un campo discount_applied de nil y non-nil. Idealmente, los únicos pedidos que deben devolverse deberían tener todas las líneas de pedido con discount_applied siendo nulo.
Dave
Lo siento, no entendí tu pregunta. Esa parte sobre cero instancias no me quedó clara.
Adass
No te preocupes, gracias por intentarlo e incluir la actualización para seguir pensando en esto.
Dave
0

Si desea todos los registros donde discount_applied es nulo, entonces:

Order.includes(:line_items).where.not(line_items: {discount_applied: nil})

(el uso incluye para evitar el problema n + 1) o

Order.joins(:line_items).where.not(line_items: {discount_applied: nil})
bhavna garg
fuente
Hola, esta también fue la respuesta presentada anteriormente por @adass, sin embargo, falla porque devuelve pedidos que tienen al menos una línea de pedido con un cero de descuento aplicado, mientras que solo estoy buscando pedidos con todos los artículos de línea que tienen descuento no aplicado. (o pedidos sin LineItems en absoluto).
Dave
0

Aquí está la solución a su problema.

order_ids = Order.joins(:line_items).where.not(line_items: {discount_applied: nil}).pluck(:id)
orders = Order.where.not(id: order_ids)

Primera consulta devolverá los identificadores de Ordersal menos una line_itemque tenga discount_applied. La segunda consulta devolverá todos los orderscasos donde haya cero instancias de line_itemtener el discount_applied.

Naveed
fuente
Gracias, pero ¿cómo es esta solución diferente a la ofrecida por Mosaaleb arriba?
Dave
Mi mal, me perdí esa respuesta, la respuesta de Mossaleb es buena.
Naveed
0

Usaría la NOT EXISTSfunción de SQL, que al menos está disponible en MySQL y PostgreSQL

Debe tener un aspecto como este

class Order
  has_many :line_items
  scope :without_discounts, -> {
    where("NOT EXISTS (?)", line_items.where("discount_applied is not null")
  }
end
Mathieu J.
fuente
0

Si entendí correctamente, desea obtener todos los pedidos para los que ninguna línea de pedido (si la hubiera) tiene un descuento aplicado.

Una forma de obtener esas órdenes usando ActiveRecordsería la siguiente:

Order.distinct.left_outer_joins(:line_items).where(line_items: { discount_applied: nil })

Aquí hay una breve explicación de cómo funciona:

  • La solución utiliza left_outer_joins, suponiendo que no accederá a las líneas de pedido para cada pedido. También puedes usar left_joins, que es un alias.
  • Si necesita crear una instancia de las líneas de pedido para cada Orderinstancia, agregue .eager_load(:line_items)a la cadena que evitará hacer una consulta adicional para cada pedido (N + 1), es decir, hacerlo order.line_items.eachen una vista.
  • El uso distinctes esencial para asegurarse de que los pedidos solo se incluyan una vez en el resultado.

Actualizar

Mi solución anterior solo estaba comprobando eso discount_applied IS NULLpara al menos una línea de pedido, no para todos. La siguiente consulta debería devolver los pedidos que necesita.

Order.left_joins(:line_items).group(:id).having("COUNT(line_items.discount_applied) = ?", 0)

Esto es lo que está pasando:

  • La solución aún necesita usar una combinación externa izquierda ( orders LEFT OUTER JOIN line_items) para que se incluyan los pedidos sin ningún elemento asociado.
  • Agrupa las líneas de pedido para obtener un solo Orderobjeto, independientemente de cuántos elementos tenga ( GROUP BY recipes.id).
  • Cuenta el número de artículos de línea que recibieron un descuento por cada pedido, solo seleccionando aquellos cuyos artículos tienen cero descuentos aplicados ( HAVING (COUNT(line_items.discount_applied) = 0)).

Espero que eso ayude.

Sebastian Sogamoso
fuente
¿Qué se supone que son los "pasos"? Lo anterior da el error 'falta la entrada de la cláusula FROM para la tabla "pasos"'
Dave
Lo siento @Dave que fue un error tipográfico. Ya está arreglado.
Sebastian Sogamoso
Np @SebastianSogamoso, pero esto no parece estar funcionando. Esto, 'Order.distinct.left_outer_joins (: line_items) .where (line_items: {discount_applied: nil}). Select {| o | o.line_items.any? {| li | li.discount_applied! = nil}}. count 'devuelve un número mayor que cero. Si todos los pedidos encontrados solo tenían líneas de pedido con discount_aplicado siendo nulo, creo que mi estado de cuenta debería devolver cero.
Dave
@Dave eso es correcto. Inicialmente me perdí eso, así que actualicé mi respuesta.
Sebastian Sogamoso
-1

No puede hacer esto de manera eficiente con un clásico rails left_joins, pero sql left join fue construido para manejar estos casos

Order.joins("LEFT JOIN line_items AS li ON li.order_id = orders.id 
                                       AND li.discount_applied IS NOT NULL")
     .where("li.id IS NULL")

Una combinación interna simple devolverá todas las órdenes, unidas con todos los elementos de línea,
pero si no hay elementos de línea para esta orden, la orden se ignora (como un falso donde)
Con la combinación izquierda, si no se encontraron elementos de línea, sql la unirá a un entrada vacía para mantenerla

Así que nos fuimos unidos a los line_items que no queremos, y encontramos todos los pedidos unidos con un line_items vacío

Y evite todo el código con where(id: pluck(:id))o having("COUNT(*) = 0"), el día esto matará su base de datos

Gadi
fuente