Chris Hudson
Chris Hudson

Reputation: 37

In Laravel Eloquent, how do I reference primary query in subquery

I have a model User that has many Orders. Orders have many products, with the pivot table order-product. I don't want to preload and iterate through the orders if at all possible.

I need to return users where

  1. signed_date === true on User
  2. order_date on Order is after signed_date on User
  3. order-product shows product hasn't been paid

I am failing on number 2. In the following code, the first query within whereHas is wrong. I don't know how to reference the signed date of the user from within the where has. If I was iterating through users in a collection I could do something like ($query) use $user, but how do I do this without preloading all the users?

return User::whereNotNull('signed_date')
           ->whereHas('orders', function ($query) {
               $query->where('order_date', '<=', 'user.signed_date');
               $query->whereHas('products', function ($q) {
                   $q->where('paid', false);
               });
           })
           ->get(['id','fname','lname', 'title', 'signed_date']);

I would like to use eloquent if possible. If that is not possible, I would be happy for tips in solving this problem using the query builder/sql.

Upvotes: 1

Views: 1895

Answers (1)

Namoshek
Namoshek

Reputation: 6544

The Eloquent query builder has a special function called whereColumn('a', '<=', 'b') to compare columns instead of a column against a value. Using this function instead of a normal where() is necessary because of the way the query builder builds the actual query. You need to let the query builder know that you are going to pass a column name instead of a value for proper escaping and formatting of the query string.

Anyway, it seems you can also pass column names prefixed with a table name to the function, allowing you to compare columns across tables:

$query->whereColumn('orders.order_date', '<=', 'users.signed_date')

This works because you are using whereHas() in your query. Your query basically gets translated to:

SELECT id, fname, lname, title, signed_date
FROM users
WHERE signed_date NOT NULL
  AND EXISTS (
    SELECT 1
    FROM orders
    WHERE orders.order_date <= users.signed_date
      AND EXISTS (
        SELECT 1
        FROM products
        WHERE paid = 0
      )
  )

It might actually be not necessary at all to use the table name together with the column name in whereColumn(). But in case you'll ever add a column named the same on another table, the query might break - so IMHO it is good practice to use the table name in custom queries.


By the way, the reason this will not work together with with('relationship') is that this function results in an additional query and you obviously cannot compare columns across queries. Imagine the following:

Order::with('user')->take(5)->get();

It will be translated into the following:

SELECT *
FROM orders
LIMIT 5

SELECT *
FROM users
WHERE id IN (?, ?, ?, ?, ?)

where the five ? will be the user_ids of the orders. If the first query returns multiple rows with the same user_id, the amount of rows fetched from the users table gets reduced of course.

Note: All the queries are only examples. Might be that the query builder builds different queries based on the database type and/or escapes them differently (i.e. column names in backticks).

Upvotes: 0

Related Questions