Reference parent column in nested relationship - php

Problem
I've spent the last few hours looking for a solution for this and can't seem to find anything that works. I'm trying to load all Routes that have at least one assigned Aircraft that is currently at the departure airport of the route, like this:
Route::has('availableAircraft');
The availableAircraft relationship on Route currently looks like this, with the issue stemming from not being able to find a way to inject the Route into the final where clause (see ROUTE_ID_HERE).
// Route.php
/**
* This returns all assigned aircraft that are not allocated to jobs and are at the departure airport of the route
*/
public function availableAircraft()
{
return $this->belongsToMany(
Aircraft::class, 'aircraft_route_assignments', 'route_id', 'aircraft_id')
->whereNull('current_job_id')
->where('current_airport_id', 'ROUTE_ID_HERE');
}
Raw SQL
I can perform this query using raw SQL, but I can't find a way to replicate this in Eloquent:
select
count(*) as aggregate
from
`routes`
where (
select
count(*)
from
`aircraft`
inner join `aircraft_route_assignments` on `aircraft`.`id` = `aircraft_route_assignments`.`aircraft_id`
where
`routes`.`id` = `aircraft_route_assignments`.`route_id`
and `current_job_id` is null
and `current_airport_id` = `routes`.`departure_airport_id`
) > 0
and `routes`.`deleted_at` is null
The crucial part here is the final and 'current_airport_id' = 'routes'.'departure_airport_id', which I can't seem to find a way to replicate in the query builder.
What I've Tried
I've tried to manually specify the field, like in the SQL query as so, but the actual SQL generated by this uses 'routes.departure_airport_id' as a literal string and so returns no results:
// Route.php
/**
* This returns all assigned aircraft that are not allocated to jobs and are at the departure airport of the route
*/
public function availableAircraft()
{
return $this->belongsToMany(
Aircraft::class, 'aircraft_route_assignments', 'route_id', 'aircraft_id')
->whereNull('current_job_id')
->where('current_airport_id', '`routes`.`departure_airport_id`');
}
Am I vastly over-thinking this?

Try this as your eloquent query:
Route::whereHas('availableAircraft', function (Builder $query) {
$query->whereNull('current_job_id')
->whereRaw('current_airport_id = `routes`.`departure_airport_id`');
})->get();
And change your model to this:
public function availableAircraft()
{
return $this->belongsToMany(
Aircraft::class, 'aircraft_route_assignments', 'route_id', 'aircraft_id');
}

I solved this by adapting Mahdi's answer to use whereRaw instead of where in the whereHas subquery. So the final Eloquent query looks like:
Route::whereHas('availableAircraft', function (Builder $query) {
$query->whereNull('current_job_id')
->whereRaw('current_airport_id = `routes`.`departure_airport_id`');
})->get()

Related

Eager loading relationships to sort by is not working in Laravel

I am trying to use Eager Loading to dynamically order by relationships in Laravel.
dd(SomeModel::with(['someRelation' => function ($query) {
$query->orderByDesc('column');
})->toSql());
I'm using dd() and toSql() to try to debug what is happening and this is what I see:
"select * from "some_table" where "some_table"."deleted_at" is null"
No matter if I orderBy('column', 'ASC') or orderBy('column', 'DESC') without the dd or toSql, I get the same output as if it is ignoring the entire eager load.
Am I missing something or doing something wrong? My relation in this case looks like this:
class SomeModel
{
protected $table = 'some_table'; # for visual aid
public function someRelation(): BelongsTo
{
$this->belongsTo(SomeOtherModel::class)->select('id', 'column');
}
}
FYI, some more debug later. I attempted to try see if the function ever executes, to which it does:
SomeModel::with(['someRelation' => function ($query) {
$query->orderByDesc('column');
dd($query->toSql());
});
The dd block executes telling me the function executed and gives me:
"select "id", "name" from "some_other_table" where "some_other_table"."id" in (?, ?) and "some_other_table"."deleted_at" is null order by "name" desc"
Any help appreciated.
Update to check for subqueries SQL:
\DB::enableQueryLog();
SomeModel::with(['someRelation' => function ($test) {
$test->orderBy('name', 'DESC');
}])->get();
dd(\DB::getQueryLog());
This returns me an empty array:
[]
After reading through the full article as #TimLewis alluded to, I went with the join solution.
SomeModel::query()->select('some_table.id', 'some_table.name') # Only bring back what we need without the join
->join('some_other_table', 'some_other_table.id', '=', 'some_table.some_other_table_id')
->orderByDesc('column');
Alternatively, you can always leftJoin if you don't expect data all the time, just make some null exception handling.
I actually created an abstract class which has a global scope to do this dynamically in any Model based on a hash-map approach.

How to select only one Column with scope of eloquent model?

I got two tables. Both have a relationship to each other. I´m trying to query both to get the matching results. This results get checked if they also have an column which matches with a parameter value.
I´m trying it with a scope and it work. I only need one column from the second table and I´m trying to use it as column in my first table when I got my result.
So the code works and I got an result but I´m trying to filter to select only one column from the second table.
My code look like that.
My controller:
public function test()
{
$UID='LQuupgYvnuVzbEoguY4TF8bnHUU2';
$event=Events::withState($UID)->get();
echo $event;
}
My model scope function:
public function scopeWithState($query,$UID){
return $query->with(['EventLiked' => function($query) use($UID) {
$query
->where('EventLiked.UID', $UID)
;
}]);
}
My hasMany relationship function:
public function EventLiked()
{
return $this->hasMany(EventLiked::class,'EID','ID')->select('State','UID','EID');
}
I would go for specifying columns inside closure.
New scope:
public function scopeWithState($query,$UID){
return $query->with(['EventLiked' => function($query) use($UID) {
$query
->where('EventLiked.UID', $UID)
->select('State');
}]);
}
Calling scope:
$event=Events::withState($UID)->get();
You're not getting expected results because Laravel splits it into 2 queries:
First, for selecting events.
Then it plucks EID
Second, when it looks for EventLiked where matching ID's is found (from second step) and loads as relationships.
So you want to change select statement only in 2nd query. Not in a first one

Is there a way to have table name automatically added to Eloquent query methods?

I'm developing an app on Laravel 5.5 and I'm facing an issue with a specific query scope. I have the following table structure (some fields omitted):
orders
---------
id
parent_id
status
The parent_id column references the id from the same table. I have this query scope to filter records that don't have any children:
public function scopeNoChildren(Builder $query): Builder
{
return $query->select('orders.*')
->leftJoin('orders AS children', function ($join) {
$join->on('orders.id', '=', 'children.parent_id')
->where('children.status', self::STATUS_COMPLETED);
})
->where('children.id', null);
}
This scope works fine when used alone. However, if I try to combine it with any another condition, it throws an SQL exception:
Order::where('status', Order::STATUS_COMPLETED)
->noChildren()
->get();
Leads to this:
SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'status' in where clause is ambiguous
I found two ways to avoid that error:
Solution #1: Prefix all other conditions with the table name
Doing something like this works:
Order::where('orders.status', Order::STATUS_COMPLETED)
->noChildren()
->get();
But I don't think this is a good approach since it's not clear the table name is required in case other dev or even myself try to use that scope again in the future. They'll probably end up figuring that out, but it doesn't seem a good practice.
Solution #2: Use a subquery
I can keep the ambiguous columns apart in a subquery. Still, in this case and as the table grows, the performance will degrade.
This is the strategy I'm using, though. Because it doesn't require any change to other scopes and conditions. At least not in the way I'm applying it right now.
public function scopeNoChildren(Builder $query): Builder
{
$subQueryChildren = self::select('id', 'parent_id')
->completed();
$sqlChildren = DB::raw(sprintf(
'(%s) AS children',
$subQueryChildren->toSql()
));
return $query->select('orders.*')
->leftJoin($sqlChildren, function ($join) use ($subQueryChildren) {
$join->on('orders.id', '=', 'children.parent_id')
->addBinding($subQueryChildren->getBindings());
})->where('children.id', null);
}
The perfect solution
I think that having the ability to use queries without prefixing with table name without relying on subqueries would be the perfect solution.
That's why I'm asking: Is there a way to have table name automatically added to Eloquent query methods?
I would use a relationship:
public function children()
{
return $this->hasMany(self::class, 'parent_id')
->where('status', self::STATUS_COMPLETED);
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('children')
->get();
This executes the following query:
select *
from `orders`
where `status` = ?
and not exists
(select *
from `orders` as `laravel_reserved_0`
where `orders`.`id` = `laravel_reserved_0`.`parent_id`
and `status` = ?)
It uses a subquery, but it's short, simple and doesn't cause any ambiguity problems.
I don't think that performance will be a relevant issue unless you have millions of rows (I assume you don't). If the subquery performance will be a problem in the future, you can still go back to a JOIN solution. Until then, I would focus on code readability and flexibility.
A way to reuse the relationship (as pointed out by the OP):
public function children()
{
return $this->hasMany(self::class, 'parent_id');
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('children', function ($query) {
$query->where('status', self::STATUS_COMPLETED);
})->get();
Or a way with two relationships:
public function completedChildren()
{
return $this->children()
->where('status', self::STATUS_COMPLETED);
}
Order::where('status', Order::STATUS_COMPLETED)
->whereDoesntHave('completedChildren')
->get();
In MySQL there are two good ways to find the leaf nodes (rows) in an adjacency list. One is the LEFT-JOIN-WHERE-NULL method (antijoin), which is what you did. The other is a NOT EXISTS subquery. Both methods should have a comparable performance (in theory they do exactly the same). However the subquery solution will not introduce new columns to the result.
return $query->select('orders.*')
->whereRaw("not exists (
select *
from orders as children
where children.parent_id = orders.id
and children.status = ?
)", [self::STATUS_COMPLETED]);
You must create a SomeDatabaseBuilder extending the original Illuminate\Database\Query\Builder, and a SomeEloquentBuilder extending the Illuminate\Database\Eloquent\Builder and, finally, a BaseModel extending Illuminate\Database\Eloquent\Model and overwrite these methods:
/**
* #return SomeDatabaseBuilder
*/
protected function newBaseQueryBuilder()
{
$connection = $this->getConnection();
return new SomeDatabaseBuilder(
$connection, $connection->getQueryGrammar(), $connection->getPostProcessor()
);
}
/**
* #param \Illuminate\Database\Query\Builder $query
* #return SameEloquentBulder
*/
public function newEloquentBuilder($query)
{
return new SameEloquentBulder($query);
}
Then, on SomeDatabaseBuilder and SameEloquentBulder, change the methods to qualify columns by default (or make it optional).

Laravel Eloquent - "whereNotIn" using a subquery?

I'm struggling to figure out how I can use this SQL with eloquent methods.
SELECT * FROM artists WHERE artists.id NOT IN
(SELECT artist_id FROM artist_issues WHERE issue = 'update_images')
I see that the "whereNotIn" method takes a column, and then an array as the second parameter, so it's not possible to pass a subquery.
Any ideas how I could do this?
Thanks.
Assuming you have the correct relationships set, it should be something like this:
$artists = Artist::whereHas('artist_issues', function(q) {
$q->where('issue', '<>', 'update_images');
});
I guess you have something like this in your Artist model:
public function artist_issues()
{
return $this->belongsTo('App\ArtistIssue');
}

How to order by pivot table data in Laravel's Eloquent ORM

In my Database, I have:
tops Table
posts Table
tops_has_posts Table.
When I retrieve a top on my tops table I also retrieve the posts in relation with the top.
But what if I want to retrieve these posts in a certain order ?
So I add a range field in my pivot table tops_has_posts and I my trying to order by the result using Eloquent but it doesn't work.
I try this :
$top->articles()->whereHas('articles', function($q) {
$q->orderBy('range', 'ASC');
})->get()->toArray();
And this :
$top->articles()->orderBy('range', 'ASC')->get()->toArray();
Both were desperate attempts.
Thank you in advance.
There are 2 ways - one with specifying the table.field, other using Eloquent alias pivot_field if you use withPivot('field'):
// if you use withPivot
public function articles()
{
return $this->belongsToMany('Article', 'tops_has_posts')->withPivot('range');
}
// then: (with not whereHas)
$top = Top::with(['articles' => function ($q) {
$q->orderBy('pivot_range', 'asc');
}])->first(); // or get() or whatever
This will work, because Eloquent aliases all fields provided in withPivot as pivot_field_name.
Now, generic solution:
$top = Top::with(['articles' => function ($q) {
$q->orderBy('tops_has_posts.range', 'asc');
}])->first(); // or get() or whatever
// or:
$top = Top::first();
$articles = $top->articles()->orderBy('tops_has_posts.range', 'asc')->get();
This will order the related query.
Note: Don't make your life hard with naming things this way. posts are not necessarily articles, I would use either one or the other name, unless there is really need for this.
For Laravel 8.17.2+ you can use ::orderByPivot().
https://github.com/laravel/framework/releases/tag/v8.17.2
In Laravel 5.6+ (not sure about older versions) it's convenient to use this:
public function articles()
{
return $this->belongsToMany('Article', 'tops_has_posts')->withPivot('range')->orderBy('tops_has_posts.range');
}
In this case, whenever you will call articles, they will be sorted automaticaly by range property.
In Laravel 5.4 I have the following relation that works fine in Set model which belongsToMany of Job model:
public function jobs()
{
return $this->belongsToMany(Job::class, 'eqtype_jobs')
->withPivot(['created_at','updated_at','id'])
->orderBy('pivot_created_at','desc');
}
The above relation returns all jobs that the specified Set has been joined ordered by the pivot table's (eqtype_jobs) field created_at DESC.
The SQL printout of $set->jobs()->paginate(20) Looks like the following:
select
`jobs`.*, `eqtype_jobs`.`set_id` as `pivot_set_id`,
`eqtype_jobs`.`job_id` as `pivot_job_id`,
`eqtype_jobs`.`created_at` as `pivot_created_at`,
`eqtype_jobs`.`updated_at` as `pivot_updated_at`,
`eqtype_jobs`.`id` as `pivot_id`
from `jobs`
inner join `eqtype_jobs` on `jobs`.`id` = `eqtype_jobs`.`job_id`
where `eqtype_jobs`.`set_id` = 56
order by `pivot_created_at` desc
limit 20
offset 0
in your blade try this:
$top->articles()->orderBy('pivot_range','asc')->get();
If you print out the SQL query of belongsToMany relationship, you will find that the column names of pivot tables are using the pivot_ prefix as a new alias.
For example, created_at, updated_at in pivot table have got pivot_created_at, pivot_updated_at aliases. So the orderBy method should use these aliases instead.
Here is an example of how you can do that.
class User {
...
public function posts(): BelongsToMany {
return $this->belongsToMany(
Post::class,
'post_user',
'user_id',
'post_id')
->withTimestamps()
->latest('pivot_created_at');
}
...
}
You can use orderBy instead of using latest method if you prefer. In the above example, post_user is pivot table, and you can see that the column name for ordering is now pivot_created_at or pivot_updated_at.
you can use this:
public function keywords() {
return $this->morphToMany(\App\Models\Keyword::class, "keywordable")->withPivot('order');
}
public function getKeywordOrderAttribute() {
return $this->keywords()->first()->pivot->order;
}
and append keyword attribiute to model after geting and use sortby
$courses->get()->append('keyword_order')->sortBy('keyword_order');

Categories