How to get average with orderBy Desc in Laravel 5 - php

I have 2 tables in my database.
books and ratings
in books
id, name
in ratings
id, book_id, rating
i have set has many relationship for these models.
So in Book model -
public function ratings()
{
return $this->hasMany('App\Rating');
}
in Rating Model -
public function book()
{
return $this->belongsTo('App\Book');
}
Now i want to fetch all books with there average rating but order by high rating.
so i high rated books first and then low ratings.
So How i can join 2 tables to achieve this result.
Thanks.

You can use a modified withCount():
$books = Book::withCount(['ratings as average_rating' => function($query) {
$query->select(DB::raw('coalesce(avg(rating),0)'));
}])->orderByDesc('average_rating')->get();

Books::withAvg('ratings', 'rating')->orderBy('ratings_avg_rating', 'desc')->take(5)->get();
this works in Laravel 8

Via Collections*
$books = Book::with('ratings')
->get()
->sortBy(function($bk, $key) {
if($bk->rating) {
return $bk->rating->rating;
}
return null;
});
Via Joins
$books = Book::join('rating', 'rating.book_id', '=', 'book.id')
->orderBy('ratings.rating', 'desc')
->select('books.*')
->get();

That query would look something like this:
SELECT book.id, AVG(ratings.rating) AS avg_rating
FROM ratings
JOIN book ON book.id = ratings.book_id
/* WHERE AVG(ratings.rating) > 4 You could say only return results where rating > 4 */
GROUP BY book.id
ORDER BY AVG(ratings.rating) DESC LIMIT 5;
You need to join the two tables, use an aggregate function with a group by on the book. Then just sort and limit the results.
UPDATE:
In response to your question:
SELECT book.id, COALESCE(AVG(ratings.rating), 0) AS avg_rating
FROM book
*LEFT* JOIN ratings ON book.id = ratings.book_id
GROUP BY book.id
ORDER BY AVG(ratings.rating);
Use of a view might be something of a compromise between ease of the ORM and sanity in your querying.

Related

Laravel Eloquent : Select random rows on relation based

I want to pick questions randomly but category dependent. For example, if the test is given with 10 questions and the total category is 5, the test flow should take 2 questions randomly from each category. Is there a way to select it through random and eloquent relations?
and the question table
+-------+-------+-------+-------+
| id | category_id |.......|
+-------+-------+-------+-------+
already I am using random eloquent but the probability of getting questions from each category is low
public getRandomQuestions($limit)
{
$this->inRandomOrder()->limit($limit)->get()
}
and I'm clueless when it's coming to relations.
You can also use the inRandomOrder and groupBy method together to select random questions from each category.
$questions = Question::with('category')->inRandomOrder()->groupBy('category_id')->limit(2)->get();
This will give you 2 random questions from each category.
You can also use subquery to select questions with certain number of random questions per category
$questions = Question::with('category')
->whereIn('category_id', function($query) use ($limit) {
$query->select('category_id')
->from('questions')
->groupBy('category_id')
->inRandomOrder()
->limit($limit)
})->get();
the query to get 1 random question for each category:
SELECT *
FROM
(SELECT *,
#position := IF(#current_cate=category_id, #position + 1, 1) AS POSITION,
#current_cate := category_id
FROM
(SELECT q.*
FROM category c
INNER JOIN question q ON c.id = q.category_id
ORDER BY RAND()) temp
ORDER BY category_id) temp1
WHERE POSITION <= 2
ORDER BY category_id;
explanation:
since you want the question to be take randomly we need order by rand(), note: inRandomOrder also uses order by rand() under the hood
to be able to get 2 questions for each category, we need a variable (#position) to mark the order of question
laravel implementation:
public getRandomQuestions($limit)
{
$questions = DB::select("SELECT *
FROM
(SELECT *,
#position := IF(#current_cate=category_id, #position + 1, 1) AS POSITION,
#current_cate := category_id
FROM
(SELECT q.*
FROM category c
INNER JOIN question q ON c.id = q.category_id
ORDER BY RAND()) temp
ORDER BY category_id) temp1
WHERE POSITION <= 2
ORDER BY category_id");
return Question::hydrate($questions->toArray());
}
If you're using PHP >= 7.2 the use shuffle()
public function getRandomQuestions($limit) {
return Question::limit($limit)->groupBy('category_id')->get()->shuffle();
}
or else
public function getRandomQuestions($limit) {
return Question::inRandomOrder()->limit($limit)->groupBy('category_id')->get();
}
the trick is you need to use groupBy() clause for this

How to select from a table based on another table's values Eloquent?

I am trying to get the data on some students that are still active. Even tho I have data from inactive students in the same table.
This is the StudentAttendance
This is the StudentClass
This is the Eloquent query that I came up with:
StudentAttendance::
select('student_classes.active', 'student_attendances.student_id', 'student_attendances.classroom_id', 'classrooms.classroom', 'attendance_rules.option')
->join('classrooms', 'classrooms.id', '=', 'student_attendances.classroom_id')
->join('attendance_rules','attendance_rules.id', '=', 'student_attendances.attendance_id')
->join('student_classes', 'student_attendances.student_id', '=', 'student_classes.student_id')
->where('attendance_date', date("Y-m-d"))
->orderBy('classrooms.classroom', 'ASC')
->get();
SQL:
select `student_classes`.`active`, `student_attendances`.`student_id`, `student_attendances`.`classroom_id`, `classrooms`.`classroom`, `attendance_rules`.`option`
from `student_attendances`
inner join `classrooms` on `classrooms`.`id` = `student_attendances`.`classroom_id`
inner join `attendance_rules` on `attendance_rules`.`id` = `student_attendances`.`attendance_id`
inner join `student_classes` on `student_attendances`.`student_id` = `student_classes`.`student_id`
where `attendance_date` = '2020-02-11'
order by `classrooms`.`classroom` asc
Now my Eloquent query results into this:
As you can see the student_id 22 with the classroom_id of 2 is inactive but it appears to be inactive once and the rest active. If I remove the student_classes join I won't get all the repeated results.
The goal is to display all the attendances of today where the student is active (active=1) in the StudentClass even if I query in the StudentAttendance.
You will want to scope your join to student_classes to only look at active records.
You can do this by using a callback in your join method:
->join('student_classes', function ($join) {
$join->on('student_attendances.student_id', '=', 'student_classes.student_id')
->on('student_classes.classroom_id', '=', 'student_attendances.classroom_id')
->where('student_classes.active', 1);
})
This is covered under the 'Advanced Join Clauses' in the Query Builder docs - https://laravel.com/docs/5.8/queries#joins

QueryBuilder: Sum values of a pivot table

I'm new on Laravel & Eloquent. I have Users, Products and Votes tables on my DB. Users can vote (0 to 5) on products, so it is a "many to many" relationship, with the Votes table acting like a pivot:
Users: id, name, email, password
Products: id, name, model, brand
Votes: user_id, product_id, vote
I mapped this schema like this:
// User model:
public function product_votes()
{
return $this->belongsToMany('App\Product', 'votes')->withPivot('vote');
}
// Product model:
public function product_votes()
{
return $this->belongsToMany('App\User', 'votes')->withPivot('vote');
}
So John can vote 5 on product X and 0 on product Y. Bob can vote 2 on product X, 3 on product Y, and so on...
I'm trying to query all products, with the sum of votes of each one of them. Something like:
SELECT p.*, (SUM(v.vote) / COUNT(*)) as votes FROM products p INNER JOIN votes v on v.product_id = p.id GROUP BY p.id
How can I do that with QueryBuilder? My mapping is right?
The following will do the trick:
Product::select('products.*', DB::raw('SUM(vote)/COUNT(*) as votes'))
->join('votes', 'votes.product_id', '=', 'products.id')
->groupBy('products.id')
->get();
You can see the query that will be run by calling toSql() instead of get().

How to order grouped results by last id using Eloquent and MySQL?

I tried this query but it only order id column, rest is not.
$chapters = Test::select(DB::raw('*, max(id) as id'))
->groupBy('id_equip')
->orderBy('id', 'asc')
->get();
In MySQL when using group by you can't rely on order by clause (it won't work as you expect, ie. it will not order the results in groups, but rather return random row from the group).
So in order to achieve what you want, you need a subquery or join:
// assuming tests table, group by id_equip, order by id
SELECT * FROM tests WHERE id = (
SELECT MAX(id) FROM tests as t WHERE t.id_equip = tests.id_equip
) ORDER BY id
SELECT * FROM tests
JOIN (SELECT MAX(id) as id FROM tests ORDER BY id DESC) as sub
ON sub.id = tests.id
This will get the highest id for each id_equip and return whole row for each of them.
Now, in eloquent I suggest first approach, if you want it to look more intuitive:
Test::where('id', function ($sub) {
// subquery
$sub->selectRaw('max(id)')
->from('tests as t')
->where('t.id_equip', DB::raw('tests.id_equip'));
// order by
})->orderBy('id', 'desc')->get();
but 2nd appreach is probably the way if you have big table to scan (in terms of performance):
Test::join( DB::raw(
'(select max(id) as id from tests group by id_equip order by id desc) sub'
), 'sub.id', '=', 'posts.id')
->get(['tests.*']);
Here you need to set order by clause inside the raw join statement.
You could also build that join subquery with the builder if you like:
$sub = Test::selectRaw('max(id)')
->groupBy('id_equip')
->orderBy('id', 'desc')
->toSql();
Test::join( DB::raw(
"({$sub}) as sub"
), 'sub.id', '=', 'posts.id')
->get(['tests.*']);

Calculate "most popular" ids in multiple database columns

On my site (aimed at PS3 gamers), users can select the 3 games they're currently playing. I will then work out which games are the most popular, and list the top 5 based on active users.
I have 3 columns in my users table called game1_id, game2_id and game3_id. These values are relational to the id column in another table called game, which contains all of the information on the games such as it's title.
How would I go about tallying up these totals and returning the top 5 most active games?
Here is my model function thus far:
function get_5_popular_games()
{
$this->db->select('user.game1_id, user.game2_id, user.game3_id');
$this->db->from('user');
$query = $this->db->get();
if ($query->num_rows() > 0)
{
return $query->result();
}
}
I assume I need to somehow tally up how many times a games id is found in any of the three columns (game1_id, game2_id, game3_id), but I'm not entirely sure how to go about it?
[EDIT] AND HERE IS THE FINISHED FUNCTION.
function get_popular_games($limit)
{
$this->db->select('count(*) as popularity, game.title');
$this->db->from('user_game');
$this->db->join('game', 'game.id = user_game.game_id');
$this->db->group_by('game_id');
$this->db->order_by('popularity', 'desc');
$this->db->limit($limit);
$query = $this->db->get();
if ($query->num_rows() > 0)
{
return $query->result();
}
}
Instead of having 3 columns, use a separate table to store the favorite games for each user. Then you could use a simple query like the following to get the top 5 games:
SELECT game_id, count( * ) as popularity
FROM users_favorite_games
GROUP BY game_id
ORDER BY popularity DESC
LIMIT 5;
Edit
Somebody didn't like this answer, so a little more context may be in order...
You could do what you want without changing your schema. Your query would look similar to the following:
SELECT game_id, count( game_id ) AS Popularity
FROM (
SELECT id, game1_id AS game_id
FROM user
UNION SELECT id, game2_id
FROM user
UNION SELECT id, game3_id
FROM user
) AS user_games
GROUP BY game_id
ORDER BY Popularity DESC;
However, the example in the question shows that you are using CodeIgniter's ActiveRecord, which does not support UNION. So you would end up with a more complicated query and some extra hacking around in your method. Not exactly desirable.
I think a better design would be to have a many-to-many table relating the user_id with the game_id, resulting in each user having three rows for their favorite games.

Categories