Querying the existence of an exact set of many to many relationship with Eloquent? - laravel

I need to make a query to find a Model that has an exact set of a many-to-many relationship.
For example, I have a Blog post that has many Tags. The tags can belongs to many blog posts.
How could I retrieve all blogs posts that have the tag #1, #2 and #3 without any other tag, only these 3, in an efficient manner?

First approach that comes to my mind is a ->whereHas() for each Tag you want to find, something like:
$tagIds = [1, 2, 3];
$query = Blog::where(function ($query) use ($tagIds) {
foreach ($tagIds as $tagId) {
$query->whereHas('tags', function ($subQuery) use ($tagId) {
return $subQuery->where('tags.id', $tagId);
});
}
})->has('tags', '=', count($tagIds));
// Additional Clauses/Filters/Etc.
$blogs = $query->get();
This will execute an additional query for the Tags supplied (based on ID), and filter your Blog records to only those that match all supplied Tags.

Related

How to express Larvel subqueries to use within a whereIn

I'm sorry for asking somthing so simple, but I can't get the docs (https://laravel.com/docs/9.x/queries#subquery-where-clauses)
I'm writting something like a social network functionality, so I have my messages table and my users table, there's another pivot table where I store the users the user follows, and they work pretty well.
I want to represent the following SQL in Laravel's ORM
SELECT * FROM mensajes
WHERE user_id=1
OR user_id IN (SELECT seguido_id FROM seguidos WHERE user_id=1)
The idea is I'll get the user's posts, and also the posts from the users that the user follows.
My following solution works, but I feel it's quite dirty, and should be solved with a Subquery
// this relation returns the users the user is following, ans works correctly
$seguidos = auth()->user()->seguidos;
// I store in an array the ids of the followed users
$seg = [];
foreach ($seguidos as $s) {
array_push($seg, $s->id);
}
array_push($seg, auth()->user()->id);
// Then I retrieve all the messages from the users ids (including self user)
$this->mensajes = Mensaje::whereIn('user_id', $seg)
->orderBy('created_at', 'desc')
->get();
I'd like to change everything to use subqueries, but I don't get it
$this->mensajes = Mensaje::where('user_id', auth()->user()->id)
->orWhereIn('user_id', function($query) {
// ... what goes here?
// $query = auth()->user()->seguidos->select('id');
// ???? This doesn't work, of course
}
->orderBy('created_at', 'desc')
->get();
You can simply construct the raw query as you have done with the SQL.
->orWhereIn('user_id', function($query) {
$query->select('seguido_id')
->from('seguidos')
->where('seguidos.user_id', auth()->user()->id);
});
But normally sub queries have a relationship between the primary SQL query with the sub query, you don't have this here and instead of doing one query with a sub query, you can quite simply write it as two queries. If you are not calling this multiple times in a single request, one vs two queries is insignificant performance optimization in my opinion.
Mensaje::where('user_id', auth()->user()->id)
->orWhereIn(
'user_id',
Seguidos::where('user_id', auth()->user()->id)->pluck('seguido_id'),
);

Laravel query on Many to Many relationship

I have an API to keep tracked photos and tags. Each photo can have N tags and one tag can be linked to N photos. That's done using 3 tables:
Photo's table.
Tag's table.
photo_tag relation table.
Now I'm working to get all photos tagged with a set of tags the idea is to make requests to my API with a list of tags and get a list of photos that has at least all the tags.
I've been trying with the whereIn operator.
This is my code (now it's all hardcoded):
$photos = Photo::whereHas('tags', function (Builder $query) {
$query->whereIn('tag', ['a5', 'mate', 'Brillante']);
})->get();
return response()->json($photos, 200);
When I execute it it return all that photos that match one tag and I need only photos that hast all the requested tags (in this example a5, mate).
I'm working on Laravel 9.
Edit:
As Tim Lewis suggested I've tried looping:
$tags = array("a5", "mate", "Brilante");
$photoQuery = Photo::query();
foreach($tags as $tag) {
\Log::debug($tag);
$photoQuery->whereHas('tags', function($query) use ($tag) {
return $query->where('tag', $tag);
});
}
$photos = $photoQuery->get();
Now it's returning an empty list I think because is looking for Photos that only have the 3 tags I hardcoded on the array.
Edit 2:
It seems that those changes were right, but for some reason Postman was not showing me any results of those changes are the solutions to my issue.
Since the whereIn() method matches against any of the values provided, and not all, you'll need to modify this. Specificying a number of whereHas() clauses, 1 for each Tag, should work:
$photoQuery = Photo::query();
foreach ($request->input('tags') as $tag) {
$photoQuery = $photoQuery->whereHas('tags', function ($query) use ($tag) {
return $query->where('tag', $tag);
});
}
$photos = $photoQuery->get();
Now, depending on the tags being sent to your API (assuming through the $request variable as a 'tags' => [] array), this query will include a whereHas() clause for each Tag, and only return Photo records that have all specified Tags.

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!

Laravel collection eager loading based on nested relation field value

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.

Resources