Laravel collection eager loading based on nested relation field value - laravel-4

I'm trying to return a Laravel collection object with relations where the resulting collection is based on a nested criteria (ie. a nested model's field value). So it would look something like this:
User -> Category -> Post -> Comments where comment.active == 1
In this case, I want the result to include all of a specific user's categories => posts => comments, where the comment is active. If it is active, it would be nested in the proper hierarchy (Category->Post->Comment). If the comment is not active, any related post and potentially category (if there are no other posts with active comments) should not show up in the collection at all.
I've tried eager loading through with(), load() and filter() with no luck. They will continue to load the relations with empty comment relations. Looking for guidance as to where to research: joins? filters? advanced wheres with nesting?
One attempt:
$user->categories->filter(function($category) {
return $category->isActive();
});
In my model I have all the relationships setup appropriately, and in addition to that I have setup isActive() as follows:
// Category model
public function isActive() {
$active = $this->posts->filter(function($post) {
return $post->isActive();
}
}
// Post model
public function isActive() {
return (boolean) $this->comments()->where('active', 1)->count();
}
This works as expected, but it also includes eagerly loaded nested relationships where comments have an active field of 0. Obviously I'm doing this the wrong way but would appreciate any direction.
Another attempt:
User::with(['categories.posts.comments' => function($q) {
$q->where('active', 1);
}])->find(1);
Unfortunately, this also loads relations (categories and posts) that have no active comments. Replacing the relations with 'categories.posts.isActive' does not work either.

Still confusing because you didn't provide enough code but you may try something like this to get all the users with nested categories.posts.comments without any condition:
$users = User::with('categories.posts.comments')->get();
But it'll give you every thing even when you don't have any comments but to add condition you may try something like this:
// It should return all user models with `categories - posts - active comments`
$users = User::with('categories.posts.activeComments')->get();
Post model:
public function activeComments() {
return $this->hasMany('Comment')->where('active', 1);
}
You may also add more filters using constraints like:
$users = User::with(array('categories.posts.activeComments' => function($query){
$query->whereNull('comments.deleted_at');
}))->get();
But I'm not sure about it, don't know enough about your relationships, so just gave you an idea.

Related

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

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

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.

Laravel: How to retrieve this nested model

In Laravel I have ModelA, ModelB and ModelC. ModelA has many ModelB. ModelB has many ModelC. I want to retrieve all ModelC for a selection of ModelA. How do I do this?
I tried the following:
$models = ModelC::with(['modelB','modelB.modelA' => function ($query) {
$query->where('owner', 123);
}])->get();
But the first query in that case is select * from model_c. Obviously not the result I am looking for.
Imagine that you were received 100 objects from the database, and each record had 1 associated model (i.e. belongsTo). Using an ORM would produce 101 queries by default; one query for the original 100 records, and additional query for each record if you accessed the related data on the model object. In pseudo code, let’s say you wanted to list all published authors that have contributed a post. From a collection of posts (each post having one author) you could get a list of author names like so:
$posts = Post::published()->get(); // one query
$authors = array_map(function($post) {
// Produces a query on the author model
return $post->author->name;
}, $posts);
We are not telling the model that we need all the authors, so an individual query happens each time we get the author’s name from the individual Post model instances.
Eager Loading
As I mentioned, ORMs “lazy” load associations. If you intend to use the associated model data you can trim that 101 query total to 2 queries using eager loading. You just need to tell the model what you need it to load eagerly.
Here’s an example from the Rails Active Record guide on using eager loading. As you can see, the concept is quite similar to Laravel’s eager loading concept.
$posts = Post::with('author')->limit(100)->get();
I find that I receive better understanding by exploring ideas from a wider perspective. The Active Record documentation covers some examples that can further help the idea resonate.
I managed to solve this with nested whereHas calls as follows:
$models = modelC::whereHas('modelB', function ($query) {
$query->whereHas('modelA', function ($query) {
$query->where('owner', 123);
});
})->get();
Laravel to the rescue, yet again!

Eloquent Ordering Related Data

I am using Eloquent Repository to get a 'menu' by ID, and return all the associated 'menuitems' along with it. This is working fine, but I am having an issue reordering the 'menuitems' by one of their fields. So I am currently doing:
$menu = $this->menuRepo->getById($id, 'menuitems');
which calls this function within the Eloquent Repo:
public function getById($id, $with = false)
{
if ($with)
{
return $this->model->withTrashed()->with($with)->findOrFail($id);
}
return $this->model->withTrashed()->findOrFail($id);
}
That function is being used throughout the system, so ideally I want to leave that as it is - or would need to change it so that it would not break in all the current usages. But even so, when I tried to add a
->orderBy('name')
within there it applies to 'menu' and not 'menuitems'.
Your with($with) needs to be rewritten, so that it uses relationship constraints:
...->with($with => function($query){
$query->orderBy('name','asc');
})->...

Filtering eager-loaded data in Laravel 4

I have the following setup:
Clubs offer Activities, which are of a particular Type, so 3 models with relationships:
Club:
function activities()
{
return $this->hasMany('Activity');
}
Activity:
function club()
{
return $this->belongsTo('Club');
}
function activityType()
{
return $this->hasMany('ActivityType');
}
ActivityType:
function activities()
{
return $this->belongsToMany('Activity');
}
So for example Club Foo might have a single Activity called 'Triathlon' and that Activity has ActivityTypes 'Swimming', 'Running', and 'Cycling'.
This is all fair enough but I need to show a list of ActivityTypes on the Club page - basically just a list. So I need to get the ActivityTypes of all the related Activities.
I can do that like so from a controller method that receives an instance of the Club model:
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)
That gets me an object with all the related Activities along with the ActivityTypes related to them. Also fair enough. But I need to apply some more filtering. An Activity might not be in the right status (it could be in the DB as a draft entry or expired), so I need to be able to only get the ActivityTypes of the Activities that are live and in the future.
At this point I'm lost... does anybody have any suggestions for handling this use case?
Thanks
To filter, you can use where() as in the fluent DB queries:
$data = Club::with(array('activities' => function($query)
{
$query->where('activity_start', '>', DB::raw('current_time'));
}))->activityType()->get();
The example which served as inspiration for this is in the laravel docs, check the end of this section: http://laravel.com/docs/eloquent#eager-loading
(the code's not tested, and I've taken some liberties with the property names! :) )
I think if you first constraint your relationship of activities, the activity types related to them will be automatically constrained as well.
So what I would do is
function activities()
{
return $this->belongsToMany('Activity')->where('status', '=', 'active');
}
and then your
$data = $this->club->with(array('activities', 'activities.activityTypes'))->find($club->id)`
query will be working as you would expect.

Resources