Eloquent eager load Order by - laravel

I have problem with eloquent query. I am using eager loading (one to one Relationship) to get 'student' With the 'exam', Using the code below.
Student::with('exam')->orderBy('exam.result', 'DESC')->get()
And i want to order received rows by the 'result' column in 'exam'. I am using
->orderBy('exam.result', 'DESC')
But it is not working. Any ideas how to do it ?

Try this:
Student::with(array('exam' => function($query) {
$query->orderBy('result', 'DESC');
}))
->get();

If you need to order your students collection by the result column, you will need to join the tables.
Student::with('exam')
->join('exam', 'students.id', '=', 'exam.student_id')
->orderBy('exam.result', 'DESC')
->get()
In this case, assuming you have a column student_id and your exams table are named exam.

If you ALWAYS want it sorted by exam result, you can add the sortBy call directly in the relationship function on the model.
public function exam() {
return this->hasMany(Exam::class)->orderBy('result');
}
(credit for this answer goes to pfriendly - he answered it here: How to sort an Eloquent subquery)

tl;dr
Student::with('exam')->get()->sortByDesc('exam.result');
This will sort the results of the query after eager loading using collection methods and not by a MySQL ORDER BY.
Explanation
When you eager load you can't use an ORDER BY on the loaded relations because those will be requested and assembled as a result of a second query. As you can see it in the Laravel documentation eager loading happens in 2 query.
If you want to use MySQL's ORDER BY you have to join the related tables.
As a workaround, you can run your query and sort the resulting collection with sortBy, sortByDesc or even sort. This solution has advantages and disadvantages over the join solution:
Advantages:
You keep Eloquent functionality.
Shorter and more intuitive code.
Disadvantages:
Sorting will be done by PHP instead of the database engine.
You can sort only by a single column, unless you provide a custom closure for the sorter functions.
If you need only a part of the ordered results of a query (e.g. ORDER BY with LIMIT), you have to fetch everything, order it, then filter the ordered result, otherwise you will end up with only the filtered part being ordered (ordering will not consider the filtered out elements). So this solution is only acceptable when you would work on the whole data set anyway or the overhead is not a problem.

This worked for me:
$query = Student::select(['id','name']);
$query->has('exam')->with(['exam' => function ($query) {
return $query->orderBy('result','ASC');
}]);
return $query->get();

You could use \Illuminate\Database\Eloquent\Relations\Relation and query scopes to add far column through relationship, I wrote a traits for this, it misses HasOne o HasMany but having BelongsTo and BelongsToMany could easily adapted
Also the method could be enhanced to support more than depth 1 for multiple chained relationship, I made room for that
<?php
/**
* User: matteo.orefice
* Date: 16/05/2017
* Time: 10:54
*/
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
trait WithFarColumnsTrait
{
public function scopeWithFarColumns(Builder $query , $relationPath , $columns , $tableAliasPrefix = null)
{
$relationPath = array_wrap($relationPath);
$tableAliasPrefix = $tableAliasPrefix ?: WithFarColumnsTrait::randomStringAlpha(3);
$currentModel = $this;
$subQueries = [];
$relationIndex = 0;
foreach ($relationPath as $relationName) {
if (method_exists($currentModel , $relationName)) {
$relation = $currentModel->$relationName();
} else {
throw new BadMethodCallException("Relationship $relationName does not exist, cannot join.");
}
$currentTable = $currentModel->getTable();
if ($relationIndex == 0) {
$query->addSelect($currentTable . '.*');
}
$relatedModel = $relation->getRelated();
/**
* #var string
*/
$relatedTable = $relatedModel->getTable();
if ($relation instanceof BelongsTo) {
foreach ($columns as $alias => $column) {
$tableAlias = $tableAliasPrefix . $relationIndex;
$tableAndAlias = $relatedTable . ' AS ' . $tableAlias;
/**
* Al momento gestisce soltanto la prima relazione
* todo: navigare le far relationships e creare delle join composte
*/
if (!isset($subQueries[$alias])) {
$subQueries[$alias] = $currentQuery = DB::query()
->from($tableAndAlias)
->whereColumn(
$relation->getQualifiedForeignKey() , // 'child-table.fk-column'
'=' ,
$tableAlias . '.' . $relation->getOwnerKey() // 'parent-table.id-column'
)
->select($tableAlias . '.' . $column);
// se la colonna ha una chiave stringa e' un alias
/**
* todo: in caso di relazioni multiple aggiungere solo per la piu lontana
*/
if (is_string($alias)) {
$query->selectSub($currentQuery , $alias);
} else {
throw new \InvalidArgumentException('Columns must be an associative array');
}
}
else {
throw new \Exception('Multiple relation chain not implemented yet');
}
} // end foreach <COLUMNs>
} // endif
else if ($relation instanceof BelongsToMany) {
foreach ($columns as $alias => $column) {
$tableAlias = $tableAliasPrefix . $relationIndex;
$tableAndAlias = $relatedTable . ' AS ' . $tableAlias;
if (!isset($subQueries[$alias])) {
$pivotTable = $relation->getTable();
$subQueries[$alias] = $currentQuery = DB::query()
->from($tableAndAlias)
->select($tableAlias . '.' . $column)
// final table vs pivot table
->join(
$pivotTable , // tabelle pivot
$relation->getQualifiedRelatedKeyName() , // pivot.fk_related_id
'=' ,
$tableAlias . '.' . $relatedModel->getKeyName() // related_with_alias.id
)
->whereColumn(
$relation->getQualifiedForeignKeyName() ,
'=' ,
$relation->getParent()->getQualifiedKeyName()
);
if (is_string($alias)) {
$query->selectSub($currentQuery , $alias);
} else {
throw new \InvalidArgumentException('Columns must be an associative array');
}
}
else {
throw new \Exception('Multiple relation chain not implemented yet');
}
} // end foreach <COLUMNs>
} else {
throw new \InvalidArgumentException(
sprintf("Relation $relationName of type %s is not supported" , get_class($relation))
);
}
$currentModel = $relatedModel;
$relationIndex++;
} // end foreach <RELATIONs>
}
/**
* #param $length
* #return string
*/
public static function randomStringAlpha($length) {
$pool = array_merge(range('a', 'z'),range('A', 'Z'));
$key = '';
for($i=0; $i < $length; $i++) {
$key .= $pool[mt_rand(0, count($pool) - 1)];
}
return $key;
}
}

There is an alternative way of achieving the result you want to have without using joins. You can do the following to sort the students based on their exam's result. (Laravel 5.1):
$students = Student::with('exam')->get();
$students = $students->sortByDesc(function ($student, $key)
{
return $student->exam->result;
});

Related

Laravel 8 Paginate Collection (sortBy)

I try to paginate a sorted collection in Laravel 8, maybee any one have an idea?
That's my code:
$where = Business::where('name', 'LIKE', '%' . $what . '%');
$businesses = $where->get()->sortByDesc(function($business) {
return $business->totalReviews;
})->paginate(10); // <--- not working.. Collection::paginate not exists
Paginate can only be called on a builder instance (it makes no sense to call it on a collection as you already have all the data). But you are doing some logic based on the review count that requires a model method which must can only be called after fetching the data.
So you must refactor the ordering so that it gets called on the builder instance so that the ordering happens on SQL before the pagination logic happens.
withCount('relation') is perfect for this as it will append on a count of a specific relation onto your query which you can then sort by on SQL.
For example you can try this where reviews is a relation on the Business model that you have many of (likely either belongsToMany or hasMany):
Business::withCount('reviews')
->where('name', 'LIKE', '%' . $what . '%')
->orderBy('reviews_count', 'desc')
->paginate(10);
Where inside your Business model you have:
public function reviews()
{
return $this->hasMany(Review::class);
}
Remove the get:
$businesses = $where->sortByDesc(function($business) {
return $business->totalReviews;
})->paginate(10);
I fixed it on this way
$businesses = $where->get()->sortByDesc(function($business) {
return $business->getTotalReviews();
});
$businesses = ViewHelper::paginate($businesses, 10);
ViewHelper.class
<?php
namespace App\Classes;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Database\Eloquent\Collection;
class ViewHelper
{
/**
* Gera a paginação dos itens de um array ou collection.
*
* #param array|Collection $items
* #param int $perPage
* #param int $page
* #param array $options
*
* #return LengthAwarePaginator
*/
public static function paginate($items, $perPage = 15, $page = null, $options = [])
{
$page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
$items = $items instanceof Collection ? $items : Collection::make($items);
return new LengthAwarePaginator($items->forPage($page, $perPage), $items->count(), $perPage, $page, $options);
}
}

Column 'department_id' in where clause is ambiguous

I got error on my page like the title above.
I am trying to export an Excel with the Laravel Excel extension.
Here is my code:
public function query()
{
$test = Leave::query()
->join('departments as dep', 'leaves.department_id', '=', 'dep.id')
->join('employees as emp', 'leaves.employee_id', '=', 'emp.id')
->join('users as emplUser', 'emp.user_id', '=', 'emplUser.id')
->join('users as apprUser', 'leaves.approved_by_id', '=', 'apprUser.id')
->select('leaves.id',
'dep.name',
'emplUser.first_name',
'leaves.start',
'leaves.end',
'leaves.type',
'leaves.reason',
'leaves.approved',
'leaves.approved_on',
'apprUser.first_name',
'leaves.approved_comment',
'leaves.created_at',
'leaves.updated_at',
)
->whereDate('leaves.start','>=', $this->periodStart)
->whereDate('leaves.end', '<=', $this->periodEnd);
return $test;
}
and here is the SQL from the error message:
select
`leaves`.`id`,
`dep`.`name`,
`emplUser`.`first_name`,
`leaves`.`start`,
`leaves`.`end`,
`leaves`.`type`,
`leaves`.`reason`,
`leaves`.`approved`,
`leaves`.`approved_on`,
`apprUser`.`first_name`,
`leaves`.`approved_comment`,
`leaves`.`created_at`,
`leaves`.`updated_at`
from `leaves`
inner join `departments` as `dep` on `leaves`.`department_id` = `dep`.`id`
inner join `employees` as `emp` on `leaves`.`employee_id` = `emp`.`id`
inner join `users` as `emplUser` on `emp`.`user_id` = `emplUser`.`id`
inner join `users` as `apprUser` on `leaves`.`approved_by_id` = `apprUser`.`id`
where date(`leaves`.`start`) >= 2021-07-04 and date(`leaves`.`end`) <= 2021-12-31
and (`department_id` = 2 or `department_id` is null)
order by `leaves`.`id` asc limit 1000 offset 0
I have notice that it says:
where ... and (`department_id` = 2 or `department_id` is null)
But I have never specified department_id, just like the start and end date. I think it needs like leaves.department_id, but how can I do that when I have never write it from the first time?
Update with more code:
This is from the LeaveController:
public function export()
{
$now = Carbon::now()->startOfWeek(Carbon::SUNDAY);
$start = $now;
$end = $now->copy()->endOfYear();
$period = new Period($start, $end);
return (new LeavesExport)->forPeriod($period->start, $period->end)->download('download.xlsx');
}
This is some of the code from Leave, that I found that contains department in some way:
use App\Traits\HasDepartment;
* App\Leave
* #property int $department_id
* #property-read \App\Department $department
* #method static \Illuminate\Database\Eloquent\Builder|\App\Leave whereDepartmentId( $value )
class Leave extends Model
{
use HasDepartment, ...
public static function getTypes()
{
try {
return LeaveType::where('department_id', current_department()->id)->pluck('name', 'id');
} catch (\Exception $e) {
error_log('User id: ' . auth()->user()->id . ' does not have an assigned Department');
return collect([]);
}
}
}
The error in your WHERE clause is ambiguous means that the system is not able to indentify department_id because there are more than 1 column with that name. You need to specify it first.
return LeaveType::where('leaves.department_id', current_department()->id)->pluck('name', 'id');

Laravel many to many relationship making a list with pivot table for order

I have made a list of all items using many to many relationship.
With a pivot I make sure that there is an order.
Now I am trying to remove 1 item from the list.
And the order numbers must also move, this does not work.
From the view I provide the $id of the item and the order number.
public function removefromlist($id, $nummer)
{
$user = User::find(auth()->user()->id);
$user->manytomany()->detach($id);
$nummerlist = $user->manytomany()->count();
for($i = $nummer + 1;$i <= $nummerlist;$i++){
$testt = $user->manytomany()->where('nummer', $i);
$user->manytomany()->updateExistingPivot($testt->first()->item_id , ['nummer' => $i -1]);
}
return view('welcome')->with('itemslist', $user->manytomany);
}
How can I ensure that when I delete an item, the other move up?
You can try the following
public function removefromlist($id, $nummer)
{
$user = auth()->user();
$user->manytomany()->detach($id);
$user->manytoMany()
->where('nummer', '>', $nummer)
->get()
->each(function($item) use($user) {
$user->manytomany()
->updateExistingPivot(
$item->id,
['nummer' => $item->pivot->nummer - 1]
);
});
return view('welcome')->with('itemslist', $user->manytomany);
}

Laravel Pivot Table, Get Room belonging to users

The structure of my pivot table is
room_id - user_id
I have 2 users that exist in the same room.
How can I get the rooms they both have in common?
It would be nice to create a static class to have something like this.
Room::commonToUsers([1, 5]);
Potentially I could check more users so the logic must not restrict to a certain number of users.
Room::commonToUsers([1, 5, 6, 33, ...]);
I created a Laravel project and make users, 'rooms', 'room_users' tables and their models
and defined a static function in RoomUser Model as below :
public static function commonToUsers($ids)
{
$sql = 'SELECT room_id FROM room_users WHERE user_id IN (' . implode(',', $ids) . ') GROUP BY room_id HAVING COUNT(*) = ' . count($ids);
$roomsIds = DB::select($sql);
$roomsIds = array_map(function ($item){
return $item->room_id;
}, $roomsIds);
return Room::whereIn('id', $roomsIds)->get();
}
in this method, I use self join that the table is joined with itself, A and B are different table aliases for the same table, then I applied the where condition between these two tables (A and B) and work for me.
I hope be useful.
I don't know the names of your relations, but I guess you can do like this :
$usersIds = [1, 5];
$rooms = Room::whereHas('users', function($query) use ($usersIds) {
foreach ($usersIds as $userId) {
$query->where('users.id', $userId);
}
})->get();
It should work. whereHas allows you to query your relation. If you need to have a static method, you can add a method in your model.
There might be a more efficient way but laravel collection does have an intersect method. You could create a static function that retrieves and loop through each object and only retain all intersecting rooms. something like this
public static function commonToUsers($userArr){
$users = User::whereIn('id',$userArr)->get();
$rooms = null;
foreach($users as $user){
if($rooms === null){
$rooms = $user->rooms;
}else{
$rooms = $rooms->intersect($user->rooms);
}
}
return $rooms;
}
This code is untested but it should work.
Room has many users, user has many rooms, so you can find the room which have those two users.
If your pivot table's name is room_users, then you can easily get the common room like this:
public static function commonToUsers($user_ids) {
$room = new Room();
foreach($user_ids as $user_id) {
$room->whereHas('users', function($query) use ($user_id) {
$query->where('room_users.user_id', $user_id);
});
}
return $room->get();
}
This code will convert to raw sql:
select *
from `rooms`
where exists (
select * from `rooms` inner join `room_users` on `rooms`.`id` = `room_users`.`room_id` where `rooms`.`id` = `room_users`.`room_id` and `room_users`.`user_id` = 1
)
and exists
(
select * from `rooms` inner join `room_users` on `rooms`.`id` = `room_users`.`room_id` where `rooms`.`id` = `room_users`.`room_id` and `room_users`.`user_id` = 5
)

codeigniter LEFT JOIN array issue

Can someone tell me how to write this properly?
function get_tech() {
$this->db->select('u.id
,u.first_name
,us.id
,us.group_id');
$this->db->from('users u');
$this->db->join('users_groups us','us.id = u.id','left');
$records = $this->db->where('us.group_id', '3');
$data=array();
foreach($records->result() as $row)
{
$data[$row->id] = $row->first_name;
}
return ($data);
}
I'm trying to populate a drop down menu using an array, but i need to only grab users that are part of users_group/group_id = 3
therefore in my very limited knowledge I'm needing:
select X from Users LEFT JOIN users_groups WHERE group_ID = 3
You need to call $this->db->get() in order to actually run your query.
function get_tech() {
$this->db->select('u.id
,u.first_name
,us.id
,us.group_id');
$this->db->from('users u');
$this->db->join('users_groups us','us.id = u.id','left');
$this->db->where('us.group_id', '3');
$records = $this->db->get();
$data = array();
foreach($records->result() as $row){
$data[$row->id] = $row->first_name;
}
return $data;
}

Resources