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.