Eloquent query by aggregated relation - laravel

I would like to make a query by an aggregated value from the model relations.
As example I should get only Posts which has the last comment between two dates.
SELECT posts.*, MAX(comments.created_at)
as max FROM posts
JOIN comments ON (comments.post_id = posts.id)
GROUP BY posts.id HAVING max > '2014-01-01 00:00:00' AND max < '2014-02-01 00:00:00'

Instead of joins use builtin methods:
// Assuming you have relations setup
Post::whereHas('comments', function ($q) use ($from, $till) {
$q->groupBy('post_id')
->havingRaw("max(created_at) between '{$from}' and '{$till}'");
})->get();
It will produce:
select * from `posts` where
(select count(*) from `comments` where `comments`.`post_id` = `posts`.`id`
group by `post_id`
having max(created_at) between '2014-01-01' and '2014-02-01'
) >= 1 limit 1

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

Which is the best efficient way to get many-to-many relation count in laravel?

I have students and subjects table in many-to-many relation (pivot table is student_subject).
Student Model
public function subjects()
{
return $this->belongsToMany(Subject::class, 'student_subject');
}
Subject Model
public function students()
{
return $this->belongsToMany(Student::class, 'student_subject');
}
Here I want the particular student subjects counts. I tried the below methods it's working fine but I want the best efficient way for this purpose.
1.
$student = Student::find($id);
$subject_count = $student->subjects()->count();
I checked the SQL query through laravel debuger it shows as below
select * from `students` where `students`.`id` = '10' limit 1
select count(*) as aggregate from `subjects` inner join `student_subject` on `subjects`.`id` = `student_subject`.`subject_id` where `student_subject`.`student_id` = 10 and `subjects`.`deleted_at` is null
$student = Student::withCount('subjects')->find($id);
$subject_count = $student->subjects_count;
I checked the SQL query through laravel debuger it shows as below
select `students`.*, (select count(*) from `subjects` inner join `student_subject` on `subjects`.`id` = `student_subject`.`subject_id` where `students`.`id` = `student_subject`.`student_id` and `subjects`.`deleted_at` is null) as `subjects_count` from `students` where `students`.`id` = '10' limit 1
$student = Student::find($id);
$subject_count = $student->loadCount('subjects')->subjects_count;
I checked the SQL query through laravel debuger it shows as below
select * from `students` where `students`.`id` = '10' limit 1
select `id`, (select count(*) from `subjects` inner join `student_subject` on `subjects`.`id` = `student_subject`.`subject_id` where `students`.`id` = `student_subject`.`student_id` and `subjects`.`deleted_at` is null) as `subjects_count` from `students` where `students`.`id` in (10)
$student = Student::find($id);
$subject_count = DB::table('student_subject')->where('student_id', $student->id)->count();
I checked the SQL query through laravel debuger it shows as below
select * from `students` where `students`.`id` = '10' limit 1
select count(*) as aggregate from `student_subject` where `student_id` = 10
According to the above ways which one is best and why? or if any different best way also there?
Doing relation()->count() is probably faster.
But if all you need is the count, withCount() should be better in terms of memory consumption.

Using same table in subquery

I am failing to convert next SQL code into laravel eloquent:
SELECT t1.template, t1.created_at
FROM sent_emails t1
where created_at = (
select max(created_at) from sent_emails t2 where t2.template = t1.template
)
group by t1.created_at, t1.template
or:
SELECT t1.template, t1.created_at
FROM sent_emails t1
JOIN
(
SELECT Max(created_at) date, template
FROM sent_emails
GROUP BY template
) AS t2
ON t1.template = t2.template
AND t1.created_at = t2.date
group by t1.created_at, t1.template
Both queries return same data set. Creating subquery in separate variable is not an option as I need multiple values to be returned from it.
I also don't know how can I set alias name if I create table using models (and not using DB::), so this is my unsuccessful try:
$sent_emails = SentEmail::where('created_at', function($query) {
SentEmail::where('template', 't1.template')->orderBy('created_at', 'desc');
})->groupBy('template', 'created_at')->get(['template', 'created_at']);
You query should be something like this (I'm not at a computer to test this, so it may require further editing)
$sent_emails = SentEmail::where('created_at', function($query) {
$query->where('created_at', SentEmail::->whereColumn('column_name', 'table.column_name')->orderBy('created_at', 'desc')->max('created_at'));
})->groupBy('template', 'created_at')->get(['template', 'created_at']);

Raw Laravel query as a collection with conditions

I've got a fairly complex query that I'd rather not write in Eloquent so I've written it raw. The only problem is that I need to be able to search/filter through the data using the front-end this query is connected to.
This what I've tried but I'm getting an "Call to a member function get() on null" error.
Here's my code:
$report = collect(DB::connection('mysql2')->select("SELECT
t2.holder,
t2.merchantTransactionId,
t2.bin,
t2.last4Digits,
t3.expDate,
(CASE WHEN t3.expDate < CURDATE() THEN 'Expired'
WHEN t3.expDate > CURDATE() THEN 'Due to expire' END) AS expInfo,
t2.uuid
FROM transactions AS t2
INNER JOIN (
SELECT t1.uuid, t1.holder, t1.bin, t1.last4Digits, LAST_DAY(CONCAT(t1.expiryYear, t1.expiryMonth, '01')) AS expDate
FROM transactions t1
JOIN (SELECT t1.merchant_access
FROM total_control.users,
JSON_TABLE(merchant_access, '$[*]' COLUMNS (
merchant_access VARCHAR(32) PATH '$')
) t1
WHERE users.id = :userId
) AS t2
ON t1.merchantUuid = t2.merchant_access
WHERE t1.paymentType = 'RG'
AND t1.status = 1
) t3
ON t2.uuid = t3.uuid
WHERE t3.expDate BETWEEN DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND DATE_ADD(CURDATE(), INTERVAL 30 DAY)
GROUP BY t2.holder, t2.bin, t2.last4Digits
ORDER BY t2.holder ASC", ['userId' => $request->userId]))
->when($request->search['holder'], function($q) use ($request) {
$q->where('t2.holder', 'LIKE', '%'.$request->search['holder'].'%');
})->get();
return $report;

Converting a raw query to Laravel query builder

I have the following MySQL query which fetches a list of the last 9 authors to write a post and lists them in order of the date of the last post they wrote.
It's working properly but I'd like to re-write it using the Laravel Query Builder. Here is the query at the moment:
$authors = DB::select("
SELECT
`a`.`id`,
`a`.`name`,
`a`.`avatar`,
`a`.`slug` AS `author_slug`,
`p`.`subheading`,
`p`.`title`,
`p`.`slug` AS `post_slug`,
`p`.`summary`,
`p`.`published_at`
FROM
`authors` AS `a`
JOIN
`posts` AS `p`
ON `p`.`id` =
(
SELECT `p2`.`id`
FROM `posts` AS `p2`
WHERE `p2`.`author_id` = `a`.`id`
ORDER BY `p2`.`published_at` DESC
LIMIT 1
)
WHERE
`a`.`online` = 1
ORDER BY
`published_at` DESC
LIMIT 9
");
I understand the basics of using the query builder, but there doesn't appear to be anything in the Laravel docs that allows for me to JOIN a table ON a SELECT.
Can anyone suggest a way that I can write this query using the Laravel Query builder, or perhaps suggest a way that I can rewrite this query to make it easier to structure with the query builder?
Try to do like this
$data = DB::table('authors')
->select(
'a.id',
'a.name',
'a.avatar',
'a.slug AS author_slug',
'p.subheading',
'p.title',
'p.slug AS post_slug',
'p.summary',
p.published_at')
->from('authors AS a')
->join('posts AS p', 'p.id', '=', DB::raw("
(
SELECT p2.id FROM posts AS p2
WHERE p2.author_id = b.id
ORDER BY p2.published_at
DESC LIMIT 1
)"))
->where('a.online', 1)
->limit(9)
->orderBy('p.published_at', 'desc')
->get();

Resources