简体   繁体   中英

Tricky Rails 5 ActiveRecord “where” query involving multiple associated models and an “or” operator

I've got a PurchaseOrder model with a has_many relationship to a Line Item model. Both the PurchaseOrder and Line Item models have a has_many relationship to a Delivery model. The Delivery model has an "expected_arrival" attribute. I need to fetch all the Purchase Orders where either of the following conditions are true:

a) the order's delivery has an "expected_arrival" attribute of yesterday or earlier b) any of the order's line items have deliveries with an "expected_arrival" attribute of yesterday or earlier

I had planned to use the Rails 5 or operator, but apparently there is a known bug in Rails when combining or with joins or includes :

https://github.com/rails/rails/issues/24055

I then tried using group and having , like so:

PurchaseOrder.left_joins(:deliveries, {line_items: :deliveries})
  .group("purchase_orders.id")
  .having("MAX(line_items.deliveries.expected_arrival) <= ? 
    OR MAX(deliveries.expected_arrival) <= ?", 
  Time.now, 
  Time.now)`

This produced the following error:

ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR:  invalid reference to FROM-clause entry for table "deliveries"

LINE 1: ..." IS NULL GROUP BY purchase_orders.id HAVING (MAX(line_items... ^ HINT: There is an entry for table "deliveries", but it cannot be referenced from this part of the query. `

It's strange that Rails is telling me I can't access the deliveries table in this way, because each of the following queries works by themselves:

PurchaseOrder.left_joins(:deliveries).group("purchase_orders.id").having("MAX(deliveries.expected_arrival) <= ?", Time.now)`

PurchaseOrder.left_joins({line_items: :deliveries}).group("purchase_orders.id").having("MAX(deliveries.expected_arrival) <= ?", Time.now)`

I would like to avoid making to database calls, which is why I originally wanted to use the or query. As you can see, in both of the last two queries, I'm simply referencing deliveries.expected_arrival instead of line_items.deliveries.expected_arrival . I'm assuming this is where Rails has a problem, and it implies I need to alias line_items.deliveries somehow.

Is that a correct assumption? If so, how would I go about that? And if not, what's the correct way to structure this query?

From what I can tell Rails does not have a built in joins method capable of multiple joins to the same table, but sql does. From this answer we see that it can be done by adding a parameter to the joins statement to name the joins.

SELECT i.name as name, v1.value as value_1, v2.value as value_2 
  FROM item i
       INNER JOIN item_value iv ON iv.item = i.id
       INNER JOIN property p ON iv.property = p.id
       LEFT JOIN value v1 ON p.name = 'prop1' AND v1.id = iv.value
       LEFT JOIn value v2 ON p.name = 'prop2' AND v2.id = iv.value

Rails does allow you to put sql as part of the joins statement as seen in this answer

for this use case

PurchaseOrder
  .joins("LEFT JOIN deliveries_purchase_orders on purchase_orders.id = deliveries_purchase_orders.purchase_order_id)
  .joins("LEFT JOIN deliveries order_deliveries ON deliveries.id = deliveries_purchase_orders.delivery_id")
  .left_joins(:line_items)
  .joins("LEFT JOIN deliveries_line_items on line_items.id = deliveries_line_items.line_item_id)
  .joins("LEFT JOIN deliveries item_deliveries ON deliveries.id = deliveries_line_items.delivery_id")
  .group("purchase_orders.id")
  .having("MAX(item_deliveries.expected_arrival) <= ? 
    OR MAX(order_deliveries.expected_arrival) <= ?", 
  Time.now, 
  Time.now)`

Not as clean as it could be if there was a railsy way to do it, but it should get you moving forward.

EDIT:

This was all written in PostgreSQL and the formatting would be slightly different for other flavors of SQL

Alternate Solution.

Since this request is doing 5 joins requests it may be preferable to add a field to the PurchaseOrder and LineItem for Latest Expected Delivery Date, and use Rails after create/update filters to maintain them. Taking the processing load off of the read, and onto the write.

Try that.

PurchaseOrder.includes(:deliveries, line_items: :deliveries)
  .group("purchase_orders.id")
  .where("MAX(line_items.deliveries.expected_arrival) <= ? 
    OR MAX(deliveries.expected_arrival) <= ?", 
  Time.now, 
  Time.now)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM