Why eloquent separates queries in eager loading? - laravel

For example I have two queries:
return $this->model->where('closed_at', null)
->whereHas('users', function ($query) {
$query->where('id', '=', auth()->user()->id);
})
->with(['product:id,title', 'in_works' => function ($query) {
$query->where('user_id', '=', auth()->user()->id);
}])
->get();
its work good and return 1 query with join(eager loading):
select * from `order` where `closed_at` is null and exists (select * from `user` inner join `user_order` on `user`.`id` = `user_order`.`user_id` where `order`.`id` = `user_order`.`order_id` and `id` = ?)
but similar query
return $this->model->where('closed_at', null)
->with(['product:id,title', 'in_works' => function ($query) {
$query->where('closed_at', '=', null);
}])->get();
return 3 queries, why?
select * from `order` where `closed_at` is null
select `id`, `title` from `product` where `product`.`id` in (1, 2)
select * from `in_work` where `in_work`.`order_id` in (1, 2) and `closed_at` is null

Each relationship added to a model via eager loading (i.e. using with) is queried via an additional query.
Here's what you have:
Model query
select * from `order` where `closed_at` is null -- Model
with product:id,title query
select `id`, `title` from `product` where `product`.`id` in (1, 2)
with in_works query
select * from `in_work` where `in_work`.`order_id` in (1, 2) and `closed_at` is null
Note that in cases (2) and (3) (1,2) corresponds to the identifiers of order that were obtained from the first query.
whereHas will further refine the original model query and therefore affect the total results returned. In your case the following is the result of the whereHas('users',...) query:
select * from `order` where `closed_at` is null and exists (select * from `user` inner join `user_order` on `user`.`id` = `user_order`.`user_id` where `order`.`id` = `user_order`.`order_id` and `id` = ?)
When a model query does not actually yield any results then no relationship queries are actually performed which is what is happening in this case.

Related

Convert DB::Select to Query Builder

i has raw query in laravel like this
public function getPopularBook(){
$book = DB::select("
with totalReview as(
SELECT r.book_id , count(r.id)
FROM review r
GROUP BY r.book_id
)
SELECT *
from totalReview x
left JOIN (
SELECT b.*,
case when ((now() >= d.discount_start_date and now() <= d.discount_end_date) or (now() >= d.discount_start_date and d.discount_end_date is null)) then (b.book_price-d.discount_price)
ELSE b.book_price
end as final_price
FROM discount d
right JOIN book b
on d.book_id = b.id
) as y
on x.book_id = y.id
ORDER BY x.count DESC, y.final_price ASC
LIMIT 8"
);
return $book;
}
so when i want to return a paginate, it doesn't work so can i convert this to query build to use paginate
This is a very un-optimized raw query in itself. You are performing too many Join in Subquery just to sort by price
i'm assuming the database table:
books[ id, name, price ]
reviews[ id, book_id ]
discounts[ id, book_id, start_date, end_date, discount_price]
Look how easy it is if you just use Eloquent:
Book::withCount('reviews')->orderBy('reviews_count')->get();
this will give you all the Books order by number of reviews
now with the final price, this can be a bit tricky, let's take a look at a query when we don't consider discount time
Book::withCount('reviews')
->withSum('discounts', 'discount_price') //i'm assuming a book can have many discount at the same time, so i just sum them all
->addSelect(
DB::raw('final_price AS (books.price - discounts_sum_discount_price)')
)
->orderBy('reviews_count', 'asc') // =you can specify ascending or descending
->orderBy('final_price', 'desc') //in laravel chaining multiple orderBy to order multiple column
->get();
I dont even need to use Subquery!! But how do we actually only add the "active" discount?, just need to modify the withSum a bit:
Book::withCount('reviews')
->withSum(
[
'discounts' => function($query) {
$query->where('start_date', '<=', Carbon::now())
->where('end_date', '>=', Carbon::now())
}
],
'discount_price'
)
->addSelect(
DB::raw('final_price AS (books.price - discounts_sum_discount_price)')
)
->orderBy('reviews_count', 'asc') // =you can specify ascending or descending
->orderBy('final_price', 'desc') //in laravel chaining multiple orderBy to order multiple column
->get();
and it is done
What about pagination? just replace the get() method with paginate():
Book::withCount('reviews')
->withSum(['discounts' => function($query) {
$query->where('start_date', '<=', Carbon::now())->where('end_date', '>=', Carbon::now())
}],'discount_price')
->addSelect(DB::raw('final_price AS (books.price - discounts_sum_discount_price)')) //just format to be a bit cleaner, nothing had changed
->orderBy('reviews_count', 'asc')
->orderBy('final_price', 'desc')
->paginate(10); //10 books per page
WARNING: this is written with ELoquent ORM, not QueryBuilder, so you must define your relationship first

Nested Select in Eloquent

I want to recreate below SQL in Eloquent (Laravel 6 LTS)
I want to avoid DB::raw as I have logics behind the PartnerPrice::class (model)
SELECT *
FROM (SELECT *,
ROW_NUMBER ()
OVER (PARTITION BY group, TYPE
ORDER BY effective_at DESC, created_at DESC)
r
FROM partner_prices
WHERE group = 'premier'
and partner_id = 8
AND TYPE = 'premium'
AND effective_at <= '2020-10-31') a
WHERE r = 1
ORDER BY group;
Here's my working inner query.
I just need help wrapping this with another select and add a where('r', 1)
$sub = PartnerPrice::select('*')
->selectRaw('ROW_NUMBER () OVER (PARTITION BY mccmnc, TYPE ORDER BY effective_at DESC, created_at DESC) r')
->where('type', $type)
->where('partner_id', $partnerId)
->where('group', $group)
->where('effective_at', '<=', now()->subMonth()->lastOfMonth())
->get();
You could use the toSql method with mergeBindings like this
$queryBuilder = PartnerPrice::select('*')
->selectRaw('ROW_NUMBER () OVER (PARTITION BY mccmnc, TYPE ORDER BY effective_at DESC, created_at DESC) r')
->where('type', $type)
->where('partner_id', $partnerId)
->where('group', $group)
->where('effective_at', '<=', now()->subMonth()->lastOfMonth());
$result = DB::table(DB::raw('(' . $queryBuilder->toSql() . ') as a'))
->mergeBindings($queryBuilder->getQuery())
->where('a.r', 1)
->get();
Note that I omitted the get() on the first builder
Also I think you can use
DB::select('*')
->fromSub($queryBuilder, 'a')
->where('a.r', 1)
->get();
But never used it. try this too.

subquery inside orWhere in laravel

I need assistance to build up the query like below in laravel:
SELECT *
FROM table t
WHERE t.a = 1
OR (t.a=0
AND t.id IN (
SELECT o.a_id
FROM other_table o
WHERE o.x > 3
)
);
You could try to build your exact current query, and in fact it might even be the most efficient to write it. But, if we rephrase your query using a left join, it becomes somewhat easier to express in Laravel code.
SELECT *
FROM your_table t
LEFT JOIN other_table o
ON t.id = o.a_id AND o.x > 3
WHERE
t.a = 1 OR
(t.a = 0 AND o.a_id IS NOT NULL);
This would translate to the following Laravel code:
$result = DB::table('your_table t')
->leftJoin('other_table o', function($join) {
$join->on('t.id', '=', 'o.a_id');
$join->on('o.x', '>', '3');
})
->where('t.a', '=', '1')
->orWhere(function($query) {
return $query->where('t.a', '=', '0')
->whereNotNull('o.a_id')
})
->get();

Query builder isn't execute where = on another column

Sorry, my question title isn't very clear but I couldn't figure a way to word this question.
I have the following query builder code.
return self::select(DB::raw('sum(user_points.points) AS points, users.id AS user_id, users.username, users.avatar, users.firstname, users.lastname'))
->join('users', 'users.id', '=', 'user_points.user_id')
$query->where('user_points.artist_id', 0)->orWhere('user_points.artist_id', 'users.favourite_artist_id');
})
->where('user_points.created_at', '>=', $startDate)
->where('user_points.created_at', '<=', $endDate)
->groupBy('users.id')
->orderBy('points', 'DESC')
->orderBy('user_id', 'ASC')
->simplePaginate(100);
It runs ok but is ignore the inner where query, specifically it's ignoring part of this;
$query->where('user_points.artist_id', 0)->orWhere('user_points.artist_id', 'users.favourite_artist_id');
})
It's matching 'user_points.artist_id = 0', but it's not matching the 'user_points.artist_id = users.favourite_artist_id', presumable it's got something to do with the way it's handling the bindings? But I can't seem to find a way to get it to work.
The complete query should end up like this;
SELECT SUM(user_points.points) AS points, users.id AS user_id, users.username, users.avatar, users.firstname, users.lastname
FROM user_points
INNER JOIN users ON users.id = user_points.user_id
WHERE (user_points.artist_id = 0 OR user_points.artist_id = users.favourite_artist_id)
AND user_points.created_at >= '$startDate' AND user_points.created_at <= '$endDate'
GROUP BY user_id
ORDER BY points DESC, user_id ASC
I updated the query builder code to this.
return self::select(DB::raw('sum(user_points.points) AS points, users.id AS user_id, users.username, users.avatar, users.firstname, users.lastname'))
->join('users', 'users.id', '=', 'user_points.user_id')
->where(function($query) {
$query->Where('user_points.artist_id', 0)->orWhere(DB::raw('user_points.artist_id = users.favourite_artist_id'));
})
->where('user_points.created_at', '>=', $startDate)
->where('user_points.created_at', '<=', $endDate)
->groupBy('users.id')
->orderBy('points', 'DESC')
->orderBy('user_id', 'ASC')
->simplePaginate(100);
That didn't work as the final query ended up looking like this.
select sum(user_points.points) AS points, users.id AS user_id, users.username, users.avatar, users.firstname, users.lastname
from `user_points` inner join `users` on `users`.`id` = `user_points`.`user_id` where (`user_points`.`artist_id` = ? or user_points.artist_id = users.favourite_artist_id is null)
and `user_points`.`created_at` >= ?
and `user_points`.`created_at` <= ?
group by `users`.`id`
order by `points` desc, `user_id` asc
You need to add the second where (or) as a raw query. The where method will add the second column as a value so you are actually trying the following:
user_points.artist_id = 'users.favourite_artist_id'
Try the following:
whereRaw("user_points.artist_id = users.favourite_artist_id")

Laravel: change a raw query in a "query-builder" or "eloquent" one

I have this Laravel Query Builder snippet that is working fine:
$records = DB::table('users')
->select(
DB::raw('users.*, activations.id AS activation,
(SELECT roles.name FROM roles
INNER JOIN role_users
ON roles.id = role_users.role_id
WHERE users.id = role_users.user_id LIMIT 1)
AS role')
)
->leftJoin('activations', 'users.id', '=', 'activations.user_id')
->where('users.id', '<>', 1)
->orderBy('last_name')
->orderBy('first_name')
->paginate(10);
Is there a way to avoid use of raw queries and get the same result? In other words, how can I write this in a more "query-builder" style? Can I also translate this into an Eloquent query?
Thanks
You can used selectSub method for your query.
(1) First create the role query
$role = DB::table('roles')
->select('roles.name')
->join('roles_users', 'roles.id', '=', 'role_users.role_id')
->whereRaw('users.id = role_users.user_id')
->take(1);
(2) Second added the $role sub query as role
DB::table('users')
->select('users.*', 'activations.id AS activation')
->selectSub($role, 'role') // Role Sub Query As role
->leftJoin('activations', 'users.id', '=', 'activations.user_id')
->where('users.id', '<>', 1)
->orderBy('last_name')
->orderBy('first_name')
->paginate(10);
Output SQL Syntax
"select `users`.*, `activations`.`id` as `activation`,
(select `roles`.`name` from `roles` inner join `roles_users` on `roles`.`id` = `role_users`.`role_id`
where users.id = role_users.user_id limit 1) as `role`
from `users`
left join `activations` on `users`.`id` = `activations`.`user_id`
where `users`.`id` <> ?
order by `last_name` asc, `first_name` asc
limit 10 offset 0"

Resources