Como eu escrevo um método localizador do Rails em que nenhum dos itens has_many possui um campo diferente de nulo?


8

Estou usando o Rails 5. Tenho o seguinte modelo ...

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

O modelo LineItem possui um atributo, "discount_applied". Gostaria de devolver todos os pedidos em que existem zero instâncias de um item de linha, com o campo "discount_applied" não sendo nulo. Como escrevo esse método localizador?


Qual RDBMS você está usando? Está usando SQL "bruto" uma opção?
Sebastian Palma

A questão é um pouco confusa. Então você quer essencialmente todos os pedidos em que os LineOrders associados tenham um desconto_aplicado nulo?
bwalshy 25/02

@bwalshy, quero todos os pedidos que não tenham itens de linha em que um campo de desconto_applicado não seja nulo. Isso inclui pedidos sem itens de linha, pedidos com um único item de linha em que desconto_applicado é nulo ou pedidos com dois itens de linha em que ambos os campos aplicados com desconto são nulos ou pedidos com três itens de linha ... Acho que você entendeu a idéia.
Dave

Respostas:


0

Não é eficiente, mas achei que poderia resolver o seu problema:

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

Atualização :

Em vez de encontrar pedidos com todos os seus itens de linha sem desconto, podemos excluir todos os pedidos com itens de linha com desconto aplicado no resultado da saída. Isso pode ser feito com subconsulta dentro da 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)

Você pode combiná-los em um único método localizador:

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

Espero que isto ajude


Obrigado por isso @Mosaaleb - a lógica parece funcionar enquanto eu a testo. Em relação à atualização que você fez, alguma maneira de combinar essas duas coisas em um único método localizador?
Dave

Olá @Dave. Sim, com certeza você pode combiná-los. Atualizei minha resposta.
Mosaaleb 28/02

2

Antes de tudo, isso realmente depende se você deseja ou não usar uma abordagem Arel pura ou se o SQL é bom. O primeiro é IMO somente recomendável se você pretende criar uma biblioteca, mas desnecessário se estiver criando um aplicativo em que, na realidade, é altamente improvável que você esteja alterando seu DBMS ao longo do caminho (e, se o fizer, alterando algumas consultas manuais provavelmente serão o menor dos seus problemas).

Supondo que o uso do SQL seja bom, a solução mais simples que deve funcionar em praticamente todos os bancos de dados é:

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

Isso também deve funcionar praticamente em qualquer lugar (e tem um pouco mais de Arel e menos SQL manual):

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

Dependendo do DBMS específico (mais especificamente: seu respectivo otimizador de consulta), um ou outro pode ter melhor desempenho.

Espero que ajude.


Estou usando o PostGres, mas existe alguma maneira de tornar este RDBMS neutro?
Dave

Permita-me reformular minha resposta anterior para maior clareza: Ambos os meus exemplos devem funcionar em todos os DBMS compatíveis com SQL - mas podem não ser executados da maneira mais otimizada, porque a Arel às vezes faz coisas mágicas sob o capô para reorganizar as consultas individuais. DBMS. Para o Postgres, na minha experiência, isso não importa, porque o otimizador de consultas é inteligente o suficiente para fazer seu trabalho de qualquer maneira.
Clemens Kofler

Oh, entendi @ ClemensKofler - sim, o SQL que você possui parece padrão para mim e o desempenho não importa nesta fase. Uma pergunta, nas soluções que você tem acima, onde você leva em conta a restrição de que todos os campos com desconto_aplicativos dos LineItems devem ser nulos?
Dave

Eu adicionei o condicional para ambos os sabores. Observe que eu não testei isso e é apenas digitado pelo conhecimento de como deve funcionar.
Clemens Kofler

0

Um código possível

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

Eu aconselho a se familiarizar com a documentação do AR para Métodos de Consulta.

Atualizar

Isso parece estar mais interessado do que eu pensava inicialmente. E mais complicado, então não poderei fornecer um código funcional. Mas eu procuraria uma solução usando LineItem.group(order_id).having(discount_applied: nil), que deve fornecer uma coleção de itens de linha e depois usá-la como subconsulta para encontrar pedidos relacionados.


Obrigado, mas isso não está funcionando. Ele retorna resultados em que um Pedido terá alguns itens de linha com um campo de desconto_applicado nulo e não nulo. Idealmente, os únicos pedidos que devem ser devolvidos devem ter todos os itens de linha com desconto_applicado sendo nulo.
Dave

Desculpe, eu não entendi sua pergunta. Essa parte sobre zero instâncias não estava clara para mim.
adass 25/02

Não se preocupe, obrigado por tentar e incluir a atualização para pensar melhor sobre isso.
Dave

0

Se você deseja que todos os registros onde discount_applied seja nulo, então:

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

(o uso inclui para evitar o problema n + 1) ou

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

Olá, esta também foi a resposta enviada acima por @adass, mas falha porque retorna pedidos que têm pelo menos um item de linha com um desconto nulo igual a zero, enquanto eu só estou procurando por pedidos com todos os itens de linha com desconto aplicado não iguais a zero (ou pedidos sem itens de linha).
Dave

0

Aqui está a solução para o seu problema

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

A primeira consulta retornará IDs de Orderspelo menos um line_itemtendo discount_applied. A segunda consulta retornará tudo ordersonde houver zero instâncias de um line_itemtendo o discount_applied.


Obrigado, mas como esta solução é diferente da oferecida pela Mosaaleb acima?
Dave

Meu mal, eu perdi essa resposta, a resposta de Mossaleb é boa.
Naveed

0

Eu usaria o NOT EXISTSrecurso do SQL, que está disponível pelo menos no MySQL e no PostgreSQL

deve ficar assim

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

0

Se entendi corretamente, você deseja obter todos os pedidos para os quais nenhum item de linha (se houver) tem um desconto aplicado.

Uma maneira de obter esses pedidos ActiveRecordseria o seguinte:

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

Aqui está uma breve explicação de como isso funciona:

  • A solução usa left_outer_joins, desde que você não acesse os itens de linha de cada pedido. Você também pode usar left_joins, que é um alias.
  • Se você precisar instanciar os itens de linha para cada Orderinstância, adicione .eager_load(:line_items)à cadeia que impedirá a realização de uma consulta adicional para cada pedido (N + 1), ou seja, order.line_items.eachem uma exibição.
  • O uso distincté essencial para garantir que os pedidos sejam incluídos apenas uma vez no resultado.

Atualizar

Minha solução anterior foi verificar apenas discount_applied IS NULLpelo menos um item de linha, não todos. A consulta a seguir deve retornar os pedidos necessários.

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

Isto é o que está acontecendo:

  • A solução ainda precisa usar uma junção externa esquerda ( orders LEFT OUTER JOIN line_items) para incluir pedidos sem itens associados.
  • Agrupa os itens de linha para obter um único Orderobjeto, independentemente de quantos itens ele possui ( GROUP BY recipes.id).
  • Ele conta o número de itens de linha que receberam um desconto para cada pedido, selecionando apenas aqueles cujos itens têm descontos zero aplicados ( HAVING (COUNT(line_items.discount_applied) = 0)).

Espero que ajude.


O que devem ser os "passos"? O exemplo acima fornece o erro 'falta da entrada da cláusula FROM para a tabela "etapas"'
Dave

Lamento @Dave que foi um erro de digitação. Está consertado agora.
Sebastian Sogamoso

Np @SebastianSogamoso, mas isso não parece funcionar muito bem. Este, 'Order.distinct.left_outer_joins (: line_items) .where (line_items: {discount_applied: nil}). Selecione {| o | o.line_items.any? {| li | li.discount_applied! = nil}}. count 'retorna um número maior que zero. Se todos os pedidos encontrados tiverem apenas itens de linha com desconto_applicado nulo, acredito que minha declaração retorne zero.
Dave

@ Dave isso mesmo. Eu perdi isso inicialmente, então atualizei minha resposta.
Sebastian Sogamoso

-1

Você não pode fazer isso de forma eficiente com os trilhos clássicos left_joins, mas a junção esquerda do sql foi criada para lidar com esses 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")

Uma junção interna simples retornará todos os pedidos, juntamente com todos os itens de linha,
mas se não houver itens de linha para esse pedido, a ordem será ignorada (como um falso onde)
Com a junção esquerda, se nenhum item de linha for encontrado, o sql o unirá a um entrada vazia para mantê-lo

Então, partimos para os line_items que não queremos e localizamos todos os pedidos com um line_items vazio

E evite todo o código com where(id: pluck(:id))ou having("COUNT(*) = 0"), no dia isso matará seu banco de dados

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.