Wie schreibe ich eine Rails-Finder-Methode, bei der keines der has_many-Elemente ein Nicht-Null-Feld enthält?

8

Ich benutze Rails 5. Ich habe das folgende Modell ...

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

Das LineItem-Modell hat das Attribut "discount_applied". Ich möchte alle Bestellungen zurückgeben, bei denen es keine Instanzen einer Werbebuchung gibt, bei denen das Feld "discount_applied" nicht null ist. Wie schreibe ich eine solche Finder-Methode?

Dave
quelle
Welches RDBMS verwenden Sie? Ist die Verwendung von "rohem" SQL eine Option?
Sebastian Palma
Die Frage ist etwas verwirrend. Sie möchten also im Wesentlichen alle Bestellungen, bei denen die zugeordneten LineOrders einen Rabatt von null haben?
Bwalshy
@bwalshy, ich möchte alle Bestellungen, die keine Werbebuchungen haben, bei denen ein Feld discount_applied nicht null ist. Dies würde Bestellungen ohne Werbebuchungen, Bestellungen mit einer einzelnen Werbebuchung, bei denen discount_applied gleich Null ist, oder Bestellungen mit zwei Werbebuchungen, bei denen beide Felder discount_applied gleich Null sind, oder Bestellungen mit drei Werbebuchungen einschließen ... Ich denke, Sie haben die Idee.
Dave

Antworten:

0

Nicht effizient, aber ich dachte, es könnte Ihr Problem lösen:

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

Update :

Anstatt Bestellungen zu finden, für die alle Werbebuchungen keinen Rabatt haben, können wir alle Bestellungen mit Werbebuchungen mit einem Rabatt vom Ausgabeergebnis ausschließen. Dies kann mit einer Unterabfrage innerhalb der where-Klausel erfolgen:

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

Sie können sie in einer einzigen Finder-Methode kombinieren:

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

Hoffe das hilft

Mosaaleb
quelle
Vielen Dank für dieses @ Mosaaleb - die Logik scheint zu funktionieren, während ich es teste. Gibt es eine Möglichkeit, diese beiden Dinge in Bezug auf das von Ihnen vorgenommene Update in einer einzigen Finder-Methode zu kombinieren?
Dave
Hallo Dave. Ja sicher, dass Sie sie kombinieren können, ich habe meine Antwort aktualisiert.
Mosaaleb
2

Zunächst hängt dies wirklich davon ab, ob Sie einen reinen Arel-Ansatz verwenden möchten oder ob die Verwendung von SQL in Ordnung ist. Ersteres ist IMO nur ratsam, wenn Sie beabsichtigen, eine Bibliothek zu erstellen, aber nicht erforderlich, wenn Sie eine App erstellen, bei der es in Wirklichkeit sehr unwahrscheinlich ist, dass Sie Ihr DBMS auf dem Weg ändern (und wenn Sie dies tun, eine Handvoll davon ändern Manuelle Abfragen sind wahrscheinlich das geringste Problem.

Vorausgesetzt, die Verwendung von SQL ist in Ordnung, ist die einfachste Lösung, die für nahezu alle Datenbanken funktionieren sollte, folgende:

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

Dies sollte auch so ziemlich überall funktionieren (und hat ein bisschen mehr Arel und weniger manuelles SQL):

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

Abhängig von Ihrem spezifischen DBMS (genauer gesagt dem entsprechenden Abfrageoptimierer) ist das eine oder andere möglicherweise leistungsfähiger.

Ich hoffe, das hilft.

Clemens Kofler
quelle
Ich verwende PostGres, aber gibt es eine Möglichkeit, dieses RDBMS neutral zu machen?
Dave
Lassen Sie mich meine vorherige Antwort aus Gründen der Klarheit umformulieren: Meine beiden Beispiele sollten in allen DBMS funktionieren, die SQL-kompatibel sind. Sie werden jedoch möglicherweise nicht optimal optimiert, da Arel manchmal magische Dinge unter der Haube ausführt, um Abfragen für einzelne Personen neu anzuordnen DBMS. Für Postgres spielt es meiner Erfahrung nach keine Rolle, da das Abfrageoptimierungsprogramm intelligent genug ist, um seine Aufgabe so oder so zu erledigen.
Clemens Kofler
Oh gotcha @ClemensKofler - ja, das SQL, das Sie haben, sieht für mich normal aus und die Leistung spielt in dieser Phase keine Rolle. Eine Frage: Wo berücksichtigen Sie in den oben genannten Lösungen die Einschränkung, dass alle Felder mit Rabattanwendung aus den LineItems Null sein müssen?
Dave
Ich habe die Bedingung zu beiden Geschmacksrichtungen hinzugefügt. Beachten Sie, dass ich dies nicht getestet habe und es nur aus dem Wissen heraus getippt wurde, wie es funktionieren sollte .
Clemens Kofler
0

Ein möglicher Code

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

Ich empfehle, sich mit der AR-Dokumentation für Abfragemethoden vertraut zu machen.

Aktualisieren

Dies scheint jedoch mehr interessiert zu sein als ich anfangs. Und komplizierter, so dass ich Ihnen keinen Arbeitscode geben kann. Aber ich würde eine Lösung mit untersuchen LineItem.group(order_id).having(discount_applied: nil), die Ihnen eine Sammlung von line_items geben und sie dann als Unterabfrage verwenden sollte, um verwandte Bestellungen zu finden.

adass
quelle
Danke, aber das funktioniert nicht. Es werden Ergebnisse zurückgegeben, bei denen eine Bestellung einige Werbebuchungen mit dem Feld discount_applied von null und nicht null enthält. Idealerweise sollten die einzigen Bestellungen, die zurückgegeben werden sollen, alle Werbebuchungen enthalten, wobei discount_applied gleich Null ist.
Dave
Entschuldigung, ich habe Ihre Frage falsch verstanden. Dieser Teil über null Instanzen war mir nicht klar.
Adass
Keine Sorge, danke, dass du es ausprobiert hast und das Update für meine weiteren Überlegungen dazu beigefügt hast.
Dave
0

Wenn Sie alle Datensätze möchten, bei denen discount_applied gleich Null ist, dann:

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

(Verwendung beinhaltet, um n + 1 Problem zu vermeiden) oder

Order.joins(:line_items).where.not(line_items: {discount_applied: nil})
Bhavna Garg
quelle
Hallo, dies war auch die Antwort von @adass, die jedoch oben fehlgeschlagen ist. Sie schlägt jedoch fehl, da Bestellungen mit mindestens einer Werbebuchung mit einem Rabatt von nicht null zurückgegeben werden, während ich nur nach Bestellungen suche, bei denen alle Werbebuchungen mit einem Rabatt von null nicht null sind (oder Bestellungen ohne LineItems).
Dave
0

Hier ist die Lösung für Ihr Problem

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

Erste Abfrage wird wieder Ide Ordersmit mindestens einer line_itemmit discount_applied. Die zweite Abfrage gibt alle zurück, bei ordersdenen es keine Instanzen von a line_itemmit dem gibt discount_applied.

Naveed
quelle
Danke, aber wie unterscheidet sich diese Lösung von der oben von Mosaaleb angebotenen?
Dave
Meine schlechte, ich habe diese Antwort verpasst, Mossalebs Antwort ist eine gute.
Naveed
0

Ich würde die NOT EXISTSFunktion von SQL verwenden, die zumindest in MySQL und PostgreSQL verfügbar ist

es sollte so aussehen

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

Wenn ich das richtig verstanden habe, möchten Sie alle Bestellungen erhalten, für die für keine Position (falls vorhanden) ein Rabatt gewährt wird.

Eine Möglichkeit, diese Bestellungen zu erhalten, ActiveRecordwäre die folgende:

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

Hier ist eine kurze Erklärung, wie das funktioniert:

  • Die Lösung verwendet left_outer_joins, vorausgesetzt, Sie greifen nicht bei jeder Bestellung auf die Werbebuchungen zu. Sie können auch left_joinseinen Alias ​​verwenden.
  • Wenn Sie die Werbebuchungen für jede OrderInstanz instanziieren müssen, fügen Sie .eager_load(:line_items)sie der Kette hinzu, um zu verhindern, dass für jede Bestellung (N + 1) eine zusätzliche Abfrage ausgeführt wird, dh order.line_items.eachin einer Ansicht.
  • Die Verwendung distinctist wichtig, um sicherzustellen, dass Bestellungen nur einmal im Ergebnis enthalten sind.

Aktualisieren

Meine vorherige Lösung bestand darin, dies nur discount_applied IS NULLfür mindestens eine Werbebuchung zu überprüfen , nicht für alle. Die folgende Abfrage sollte die von Ihnen benötigten Bestellungen zurückgeben.

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

Das ist was los ist:

  • Die Lösung muss weiterhin einen linken äußeren Join ( orders LEFT OUTER JOIN line_items) verwenden, damit Bestellungen ohne zugehörige Artikel enthalten sind.
  • Gruppiert die Werbebuchungen, um ein einzelnes OrderObjekt zu erhalten, unabhängig davon, wie viele Elemente es enthält ( GROUP BY recipes.id).
  • Es zählt die Anzahl der Werbebuchungen, denen für jede Bestellung ein Rabatt gewährt wurde, und wählt nur diejenigen aus, für deren Positionen keine Rabatte gelten ( HAVING (COUNT(line_items.discount_applied) = 0)).

Ich hoffe das hilft.

Sebastian Sogamoso
quelle
Was sollen "Schritte" sein? Das obige gibt den Fehler 'fehlender FROM-Klauseleintrag für Tabelle "Schritte"'
Dave
Es tut mir leid @ Dave, das war ein Tippfehler. Es ist jetzt behoben.
Sebastian Sogamoso
Np @SebastianSogamoso, aber das scheint nicht ganz zu funktionieren. Dies ist 'Order.distinct.left_outer_joins (: line_items) .where (line_items: {discount_applied: nil}). Wählen Sie {| o | o.line_items.any? {| li | li.discount_applied! = nil}}. count 'gibt eine Zahl größer als Null zurück. Wenn alle gefundenen Bestellungen nur Werbebuchungen hatten, bei denen discount_applied gleich Null ist, sollte meine Aussage meiner Meinung nach Null zurückgeben.
Dave
@ Dave das stimmt. Ich habe das anfangs verpasst und meine Antwort aktualisiert.
Sebastian Sogamoso
-1

Sie können dies mit einem klassischen Rails left_joins nicht effizient tun, aber SQL Left Join wurde erstellt, um diese Fälle zu behandeln

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

Ein einfacher innerer Join gibt alle Bestellungen zurück, die mit allen line_items verknüpft sind.
Wenn jedoch keine line_items für diese Bestellung vorhanden sind, wird die Reihenfolge ignoriert (wie ein falsches where)
. Wenn beim linken Join keine line_items gefunden wurden, verbindet sql sie mit einem leerer Eintrag, um ihn zu behalten

Also sind wir zu den line_items gegangen, die wir nicht wollen, und haben alle Bestellungen gefunden, die mit einem leeren line_items verbunden sind

Und vermeiden Sie jeglichen Code mit where(id: pluck(:id))oder having("COUNT(*) = 0"), an diesem Tag wird Ihre Datenbank zerstört

Gadi
quelle