Laravel / Eloquent: Is it possible to select all child model data without setting a parent? - laravel

I have various parent/child relationships, drilling down a few levels. What I want to know is if its possible to do something like this:
$student = Student::find(1);
$student->bursaries()->enrolments()->courses()->where('course','LIKE','%B%');
(With the end goal of selecting the course which is like '%B%'), or if I would have to instead use the DB Query builder with joins?
Models / Relationships
Student:
public function bursaries() {
return $this->hasMany('App\StudentBursary');
}
StudentBursary:
public function enrolments() {
return $this->hasMany('App\StudentBursaryEnrolment');
}

If what you want is to query all courses, from all enrollments, from all bursaries, from a students, then, unfortunately, you are one table too many from getting by with the Has Many Through relationship, because it supports only 3 tables.
Online, you'll find packages that you can import / or answers that you can follow to provide you more though of solutions, for example:
1) How to use Laravel's hasManyThrough across 4 tables
2) https://github.com/staudenmeir/eloquent-has-many-deep
Anyhow, bellow's something you can do to achieve that with Laravel alone:
// Eager loads bursaries, enrolments and courses, but, condition only courses.
$student = Student::with(['bursaries.enrolments.courses' => function($query) {
$query->where('course','LIKE','%B%');
}])->find(1);
$enrolments = collect();
foreach($student->bursaries as $bursary) {
$enrolments = $enrolments->merge($bursary->enrolments);
}
$courses = collect();
foreach ($enrolments as $enrolment) {
$courses = $courses->merge($enrolment->courses);
}

When you do $student->bursaries() instead of $student->bursaries, it returns a query builder instead of relationship map. So to go to enrolments() from bursaries() you need to do a bursaries()->get(). It should look like this.
$student->bursaries()->get()[0]->enrolments(), added the [0] because im using get(), you can use first() to avoid the [0]
$student->bursaries()->first()->enrolments()
But I'm not sure if it will suffice your requirement or not.

Related

many to many and one to many relationship in Eloquent

I have three tables: "courriers" which is connected with "reponses" by one to-many relationship (1 courrier could have many reponses), and "structures" which is connected with "courriers" by many-to-many relationship
I want to find the courriers which are connected to a certain structure and doesn't have a reponse in table "reponses".
For example, for the structures "DMO" that has 1 as identifiant in "structures", I wish find the courriers that belongs to this structure and doesn't appear in "reponses".
Am using Laravel 8, I want to do this with Eloquent ORM.
Am trying this
public function dmoDG()
{
$structure = Structure::find(1);
$cou = $structure->courriers;
$courr = $cou->where('repondre','=',1)-
>where('dmo_sec','<>',NULL);
$courriers = $courr->doesntHave('reponses')->get();
return view("DG\dmoDG", compact('courriers'));
}
Method Illuminate\Database\Eloquent\Collection::doesntHave does not exist.
When you use $model->relation, it fetches all related records and returns them as a Collection.
If you want to use query builder on a relation, you need to use it as a method: $model->relation()
So, if you access your relation as a property, you got Collection.
But if you access your relation as a method, you got query builder and add your where clauses on it.
In your example;
public function dmoDG()
{
$structure = Structure::find(1);
// $cou = $structure->courriers; // for using without parentheses you got a collection, not a query builder
$cou = $structure->courriers(); // now you will have a query builder and Where clauses will work on this
$courr = $cou->where('repondre', '=', 1)->where('dmo_sec', '<>', NULL);
$courriers = $courr->doesntHave('reponses')->get();
return view("DG\dmoDG", compact('courriers'));
}
Actually you can pipe them to one liner:
public function dmoDG()
{
$structure = Structure::find(1);
$courriers = $structure->courriers()->where('repondre', '=', 1)->where('dmo_sec', '<>', NULL)->doesntHave('reponses')->get();
return view("DG\dmoDG", compact('courriers'));
}
Make sure your relations and column names correctly specified.

Prevent duplicate queries and N+1 problem in Laravel collection

I'm currently working on a simple Laravel project where I need to get the posts of the users I'm following. With the code below, I can get the posts but I also add a lot of duplicate queries and an N+1 issue on the Authenticated user. So it's becoming sort of a head scratcher. I've looked though other similar scenarios online but I haven't been able to pinpoint what I'm doing wrong. Perhaps there is a better way. Currently, I have on the User model:
public function usersImFollowing()
{
return $this->belongsToMany(User::class, 'follow_user', 'user_id', 'following_id')
->withPivot('is_following', 'is_blocked')
->wherePivot('is_following', true)
->wherePivot('is_blocked', false)
->paginate(3);
}
public function userPosts()
{
return $this->hasMany(Post::class, 'postable_id', 'id')
->where('postable_type', User::class);
}
As you can see, I am using two booleans to determine if a user is following or is blocked. Also, the Post model is a polymorphic model. There are several things I've tried, among them, I tried a hasManyThrough, without using the hasMany Posts relationship above. It got the posts for each user but since I'm using the booleans above, I couldn't use them in the hasManyThrough, it simply got the posts based on the following_id, whether or not the user was following or was blocked became irrelevant.
Then in a separate service class, I tried the methods below (I'm using a separate class to maintain the code easier). They both get the posts for each user but add an N+1 problem and 12 duplicate queries based on 5 posts from 2 users. I will also need to filter the results based on some conditions, so it will probably add more queries. Additionally, I'm using a Laravel resource collection that would pull other items for each post, such as images, comments, etc., so the amount of queries would increase even more. Not sure, perhaps I'm doing too much and there is an easier way:
Either:
$following = $request->user()->usersImFollowing();
$posts = $following->map(function($user){
return $user->userPosts()->get();
})->flatten(1);
return $posts;
Or
$postsfromfollowing = [];
$following = $request->user()->usersImFollowing()->each(function($user) use (&$postsfromfollowing){
array_push($postsfromfollowing,$user->userPosts);
});
$posts = Arr::flatten($postsfromfollowing);
return $posts;
Maybe you could use scopes to do little celanup of code and generated sql.
In User model something like
public function scopeIsFollowedBy(Builder $query, int $followerId) {
return $query->where('following_id', '=', $followerId);
}
And in Post model
public function scopeIsFollowedBy(Builder $query, int $followerId) {
return $query->whereHas('user', function($q) use ($followerId) {
$q->isFollowedBy($followerId);
});
}
You can use it then in coltroller like any other condition like this:
Post::isFollowedBy($followerId)->...otherConditions...->get();
The SQL generated won't go through foreach but only add one IF EXISTS select (generated by whereHas part of the code)
More on local scopes in Laravel is here https://laravel.com/docs/8.x/eloquent#local-scopes

Querying Distant Relationships

I have three tables: users, accounts and hotels. Users and Accounts are connected with belongstoMany relation and Accounts and Hotels are connected the same way. Each User has Accounts and those Accounts have Hotels.
When I have Auth::user(), how I can return all hotels?
$accounts = Auth::user()->accounts()->get();
With the above statement, I can get all Accounts. How can I return all Hotels?
WHAT I TRIED?
public function index(Request $request)
{
$accounts = Auth::user()->accounts()->get();
$hotels = collect();
foreach ($accounts as $key => $a) {
$h = $a->hotels();
$hotels = $hotels->toBase()->merge($h);
}
dd($hotels);
return view('hotels.index',compact('hotels'));
}
but this code dont return me hotels collection which I can use in blade view files
Case 1
In the case you have a relationship as shown in the diagram below
What you are looking for is the hasManyThrough relationship.
From the Laravel documentation
The "has-many-through" relationship provides a convenient shortcut for accessing distant relations via an intermediate relation
In your case, on your User model, you can define the relationship
public function hotels()
{
return $this->hasManyThrough('App\Hotel', 'App\Account');
}
To then get your collection of hotels you can simply use
$hotels = Auth::user()->hotels;
You can also provide extra arguments to the hasManyThrough function to define the keys that are used on each table, great examples of this are given in the documentation linked above!
Case 2
If, instead, you have a relation as shown in the following diagram
It is a little more tricky (or, at least, less clean). The best solution I can think of, that uses the fewest queries is to use with.
$accounts = Auth::user()->accounts()->with('hotels')->get();
Will give you a collection of accounts, each with a hotels child. Now, all we have to do is get the hotels as a standalone collection, this is simple with some neat collection functions provided by Laravel.
$hotels = $accounts->flatMap(function($account) {
return $account->hotels;
})->unique(function ($hotel) {
return $hotel->id;
});
This will do the job of creating a collection of hotels. In my opinion, it would be cleaner and more efficient to simply make a new relationship as shown below.
And then to perform queries, using basic Eloquent methods.

Laravel Return Grouped Many To Many Relations

How do you return a collection of a grouped dataset in a ManytoMany relationship with this scenario?
Here is a sample of what dataset I want to return
So let's take the favorites as the genres and the highlighted date is the genres name, it's also a collection as well. I want to group it based on the genres name in that collection.
My model:
Video
```
public function genres() {
return $this->belongsToMany(Genre::class);
}
```
Genre
```
public function videos() {
return $this->belongsToMany(Video::class);
}
```
I tried the following already but can't seem to get it.
```
$videos = Video::with('genres')->all();
$collection = $videos->groupBy('genres.name');
```
I want to group the dataset by the genres name knowing the genre's relationship is also a collection of genres.
Try something like:
Video::with('genres')->get()->groupBy('genres.*.name');
Or:
$videos = Video::with('genres')->all();
$collection = $videos->groupBy('genres.*.name');
Note that above is code you posted, just after replacing "genres.name" with "genres.*.name".
Just noticed the post is old, this at least works on latest Laravel.
Collections and query builders share many similar functions such as where() groupBy() and so on. It's nice syntax sugar, but it really does obscure the underlying tech.
If you call $model->videos... like a property, that's a collection (query has executed).
If you call $model->videos()... like a method, that's a query builder.
So if you want to get the job done in sql, you can do something like...
$video_query_builder = Video::with('genere');
$video_query_builder->groupBy('genere_id');
$result = $video_query_builder->get();
You can chain it all together nice and neatly as was suggested in the comments... like this:
$result = Video::with('genere')
->groupBy('genere_id')
->get();

How to fetch two related objects in Laravel (Eloquent) with one SQL query

I am trying to get two related objects in Laravel using eager loading as per documentation.
https://laravel.com/docs/5.4/eloquent-relationships#eager-loading
My models are:
class Lead extends Model {
public function session() {
return $this->hasOne('App\LeadSession');
}
}
class LeadSession extends Model {
public function lead() {
return $this->belongsTo('App\Lead');
}
}
I want to get both objects with one SQL query. Basically I want to execute:
select * from lead_sessions as s
inner join lead as l
on l.id = s.lead_id
where s.token = '$token';
and then be able to access both the LeadSession and Lead objects. Here is the php code I am trying:
$lead = Lead::with(['session' => function ($q) use ($token) {
$q->where('token','=',$token);
}])->firstOrFail();
print($lead->session->id);
I have also tried:
$lead = Lead::whereHas('session', function($q) use ($token) {
$q->where('token','=',$token);
})->firstOrFail();
print($lead->session->id);
and
$session = LeadSession::with('lead')->where('token',$token)->firstOrFail();
print($session->lead->id);
In all three cases I get two queries executed, one for the leads table, and another for the lead_sessions table.
Is such a thing possible in Eloquent? In my view it should be a standard ORM operation, but for some reason I am struggling a whole day with it.
I don't want to use the Query Builder because I want to use the Eloquent objects and their functions afterwards.
I am coming from Python and Django and I want to replicate the behavior of select_related function in Django.
Try this and see if it makes more than one query
$session = LeadSession::join('leads', 'leads.id', '=', 'lead_sessions.lead_id')
->where('token',$token)
->firstOrFail();
I hope it only runs a single query. I didnt test this. Not sure if you have to add a select() to pick the columns. But yeah, try this first.
Updates
Just adding how to use both session and lead data. Try a select and specify the data you need. The reason being that if both tables have similar columns like 'id', one of them will be overwritten. So you have to alias your select like
$session = LeadSession::join('leads', 'leads.id', '=', 'lead_sessions.lead_id')
->where('token',$token)
->select(
'lead_sessions.*',
'leads.id as lead_id',
'leads.name',
'leads.more_stuff'
)
->firstOrFail();
Now all this data belongs to $session variable. For testing you were doing
print($lead->session->id);
//becomes
print($session->lead_id); //we aliased this in the query

Resources