Как мне написать метод поиска Rails, где ни один из элементов has_many не имеет ненулевого поля? - PullRequest
8 голосов
/ 25 февраля 2020

Я использую Rails 5. У меня следующая модель ...

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

Модель LineItem имеет атрибут "discount_applied". Я хотел бы вернуть все заказы, в которых есть ноль экземпляров позиции, у которой поле "discount_applied" не равно нулю. Как мне написать такой метод поиска?

Ответы [ 8 ]

2 голосов
/ 28 февраля 2020

Прежде всего, это действительно зависит от того, хотите ли вы использовать подход чистого Ареля или использовать SQL хорошо. Первый вариант рекомендуется только в том случае, если вы намереваетесь создать библиотеку, но не нужен, если вы создаете приложение, в котором, на самом деле, маловероятно, что вы меняете свою СУБД по ходу процесса (и если вы это делаете, меняете несколько ручные запросы, вероятно, будут наименьшей из ваших проблем).

При условии использования SQL все в порядке, самое простое решение, которое должно работать практически во всех базах данных:

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

Это также должен работать почти везде (и имеет немного больше Arel и меньше ручного SQL):

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

В зависимости от вашей конкретной СУБД c (точнее: соответствующего оптимизатора запросов), один или другой может быть более производительным.

Надеюсь, это поможет.

0 голосов
/ 05 марта 2020

Я бы использовал функцию NOT EXISTS из SQL, которая, по крайней мере, доступна как в MySQL, так и в PostgreSQL

, она должна выглядеть следующим образом

class Order
  has_many :line_items
  scope :without_discounts, -> {
    where("NOT EXISTS (?)", line_items.where("discount_applied is not null")
  }
end
0 голосов
/ 05 марта 2020

Вы не можете сделать это эффективно с классом c rails left_joins, но sql левое соединение было создано для обработки этих случаев

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")

Простое внутреннее соединение вернет все заказы, объединенные со всеми line_items,
, но если для этого заказа нет line_items, заказ игнорируется (как false, где)
При левом соединении, если не найдено line_items, sql присоединяет его к пустой записи в порядке чтобы сохранить его

Итак, мы присоединились к ненужным элементам line_items и нашли все ордера, объединенные с пустыми элементами line_items

И избегали всего кода с where(id: pluck(:id)) или having("COUNT(*) = 0") при день это убьет вашу базу данных

0 голосов
/ 29 февраля 2020

Вот решение вашей проблемы

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

Первый запрос вернет идентификаторы Orders, по крайней мере с одним line_item, имеющим discount_applied. Второй запрос вернет все orders, где есть ноль экземпляров line_item, имеющих discount_applied.

0 голосов
/ 28 февраля 2020

Если вы хотите, чтобы все записи, где discount_applied равно nil, тогда:

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

(используйте, чтобы избежать проблемы n + 1) или

Order.joins(:line_items).where.not(line_items: {discount_applied: nil})
0 голосов
/ 28 февраля 2020

Неэффективно, но я подумал, что это может решить вашу проблему:

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

Обновление :

Вместо того, чтобы находить заказы, на которые у всех его позиций нет скидки, мы можем исключить из итогового результата все заказы, в которых есть позиции со скидкой. Это можно сделать с помощью подзапроса внутри предложения 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)

Вы можете объединить их одним методом поиска:

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

Надеюсь, это поможет

0 голосов
/ 28 февраля 2020

Если я правильно понял, вы хотите получить все заказы, для которых ни одна позиция (если есть) не имеет скидки.

Один из способов получить эти заказы с использованием ActiveRecord будет следующим:

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

Вот краткое объяснение того, как это работает:

  • В решении используется left_outer_joins, при условии, что вы не будете получать доступ к позициям для каждого заказа. Вы также можете использовать left_joins, который является псевдонимом.
  • Если вам нужно создать отдельные элементы для каждого экземпляра Order, добавьте .eager_load(:line_items) в цепочку, что предотвратит выполнение дополнительного запроса для каждого order (N + 1), т. е. выполнение order.line_items.each в представлении.
  • Использование distinct необходимо для обеспечения того, чтобы заказы включались в результат только один раз.

Обновление

Мое предыдущее решение проверяло только discount_applied IS NULL как минимум для одной позиции, а не для всех. Следующий запрос должен вернуть нужные вам заказы.

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

Вот что происходит:

  • В решении все еще необходимо использовать левое внешнее соединение (orders LEFT OUTER JOIN line_items), поэтому что заказы без каких-либо связанных элементов включены.
  • Группирует позиции, чтобы получить один объект Order независимо от того, сколько у него элементов (GROUP BY recipes.id).
  • Подсчитывает количество позиций, которым была предоставлена ​​скидка на каждый заказ, выбирая только те, для которых применяются нулевые скидки (HAVING (COUNT(line_items.discount_applied) = 0)).

Надеюсь, это поможет.

0 голосов
/ 25 февраля 2020

Возможный код

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

Я советую ознакомиться с документацией AR для методов запросов.

Обновление

Это кажется более интересным, чем я изначально, хотя , И сложнее, поэтому я не смогу дать вам рабочий код. Но я бы посмотрел на решение с использованием LineItem.group(order_id).having(discount_applied: nil), которое должно дать вам набор line_items, а затем использовать его как подзапрос для поиска связанных заказов.

...