Laravel nested relations eager load respect ancestor id - mysql

Let's say I have a model called Research. Each research belongsToMany Location models. And each Location model BelongsToMany Contact models. BUT, each Contact is also related to Research.
class Research extends Model {
protected $table = 'researches';
public function locations()
{
return BelongsToMany( Location::class, 'research_locations_list', 'research_id', 'location_id' );
}
}
class Location extends Model {
protected $table = 'locations';
public function researches()
{
return BelongsToMany( Research::class, 'research_locations_list', 'research_id', 'location_id' );
}
public function contacts()
{
return BelongsToMany( Contact::class, 'location_contacts_list', 'location_id', 'contact_id' );
}
}
class Contact extends Model {
protected $table = 'contacts';
public function locations()
{
return BelongsToMany( Location::class, 'location_contacts_list', 'location_id', 'contact_id' );
}
}
researches table:
+----+------------+
| id | research |
+----+------------+
| 1 | Research 1 |
| 2 | Research 2 |
+----+------------+
locations table:
+----+---------------+
| id | location |
+----+---------------+
| 1 | United States |
| 2 | Great Britain |
| 3 | Germany |
+----+---------------+
contacts table:
+----+---------+
| id | contact |
+----+---------+
| 1 | Jack |
| 2 | John |
| 3 | Hanz |
+----+---------+
research_locations_list table:
+----+-------------+-------------+
| id | research_id | location_id |
+----+-------------+-------------+
| 1 | 1 | 1 |
| 2 | 1 | 2 |
| 3 | 2 | 2 |
| 4 | 2 | 3 |
+----+-------------+-------------+
So Research 1 is being conducted in United States and Great Britain, Research 2 in Great Britain and Germany
location_contacts_list table:
+----+-------------+------------+-------------+
| id | location_id | contact_id | research_id |
+----+-------------+------------+-------------+
| 1 | 1 | 1 | 1 |
| 2 | 1 | 2 | 1 |
| 3 | 2 | 1 | 2 |
| 4 | 3 | 3 | 2 |
+----+-------------+------------+-------------+
Research 1 should have Jack and John as contacts in United States and no contacts elsewhere;
Research 2 should have John as contact in Great Britain and Hanz in Germany;
Now, with lazy load I can achieve that:
$researches = Research::all();
foreach( $researches as $research )
{
foreach( $research->locations as $location )
{
$contacts = $location->contacts()->wherePivot( 'research_id', $research->id )->get();
// Will return John and Jack in United States for Research 1 and John in Great Britain and Hanz in Germany for Research 2
}
}
Now, the question is: how do I achieve this with eager loading?
$researches = Research::with( 'locations.contacts' )->all();
foreach( $researches as $research )
{
foreach( $research->locations as $location )
{
$contacts = $location->contacts;
// Will return John and Jack in United States, John in Great Britain ( which is not supposed to happen ) for Research 1 and John in Great Britain and Hanz in Germany for Research 2
}
}
Perhaps I can instruct somehow for contacts to respect ancestor id? Like:
$research = Research::with( 'locations.contacts' )->where( 'researches.id = location_contacts_list.research_id' )->all();
UPDATE
The closest I came up to solving this is modifying the Location model like this:
class Location extends Model {
protected $table = 'locations';
public function researches()
{
return BelongsToMany( Research::class, 'research_locations_list', 'research_id', 'location_id' );
}
public function contacts()
{
return BelongsToMany( Contact::class, 'location_contacts_list', 'location_id', 'contact_id' );
}
// Modify contacts attribute getter
public function getContactsAttribute()
{
$contacts = $this->contacts();
if( !empty( $this->pivot->research_id ) )
{
$contacts = $contacts->wherePivot( 'research_id', $this->pivot->research_id );
}
return $contacts->get();
}
}
But it looks kind of dirty...

In your solution you get N+1 query problem. I can suggest the following solution:
class Research extends Model
{
protected $table = 'researches';
public function locations(): BelongsToMany
{
return $this->belongsToMany(Location::class, 'research_locations_list');
}
public function contacts(): BelongsToMany
{
return $this->belongsToMany(Contact::class, 'location_contacts_list')
->withPivot('location_id');
}
public function contactsByLocationAttribute(int $locationId): Collection
{
return $this->contacts
->filter(static function ($contact) use ($locationId) {
return $contact->pivot->location_id === $locationId;
});
}
}
$researches = Research::with(['locations', 'contacts'])->get();
foreach ($researches as $research) {
foreach ($research->locations as $location) {
$contacts = $research->contactsByLocation($location->id);
}
}
here there will always be only 3 queries to the database. And only necessary models will be loaded

If I got it right, you want to add some conditions inside your with statement. If you want to use eloquent syntax, you can do it like this:
$research = Research::with(['YOUR RELATION' => function ($query) {
$query->where('YOUR COLUMN', 'EQUALS TO SOMETHING');
}])->get();
Keep in mind that since inside with you use nested relationships, like locations.contacts, the where function inside the query, will filter only the last model (in this case that would be contacts). If you want to filter both locations and contacts based on some conditions, you have to write something similar to this (just an example):
$research = Research::with(['locations' => function ($query) {
$query->where('id', 1)->with(['contacts' => function ($query) {
$query->where('name', 'Tim');
}]);
})->get();
In order to do that though, you need to create a relationship also with your pivot table (if you want to use it also inside the conditions). Otherwise, you have to use a different syntax, using joins. Check this page from docs for query builders https://laravel.com/docs/9.x/queries#main-content

Perhaps, this https://laravel.com/docs/9.x/eloquent-relationships#has-many-through helps you. You should try to this

Related

Laravel 7 select from table with two columns having ids from a Single Table

My requirement?
If i am the logged in user with Id = 1, then through the Messages Table i want to select users from Users Table to whom i sent the message or from whome i received the message.
Table 1: Users
+----+------+-------+
| id | name | email |
+----+------+-------+
| 1 | a | ??? |
| 2 | b | ??? |
| 3 | c | ??? |
| 4 | d | ??? |
| 5 | e | ??? |
| 6 | f | ??? |
| 7 | g | ??? |
| 8 | h | ??? |
| 9 | i | ??? |
| 10 | j | ??? |
+----+------+-------+
Table 2: Messages
+----+---------+-------------+
| id | user_id | receiver_id |
+----+---------+-------------+
| 1 | 1 | 2 |
| 2 | 3 | 1 |
| 3 | 4 | 1 |
| 4 | 1 | 5 |
| 5 | 1 | 3 |
+----+---------+-------------+
User Model
public function messages()
{
return $this->belongsToMany(User::class, 'messages', 'user_id', 'receiver_id');
}
Message Model
public function user()
{
return $this->belongsTo(User::class);
}
So what i have tried so far?
$id = Auth::id();
$users = User::with(['messages' => function($query) use($id){
$query->where('user_id', $id)
->orWhere('received_id', $id)
->orderBy('created_at', 'DESC');
}])->get();
dd($users);
What is the expected result?
Using this query, i am getting all of my 10 users. Although i should only get 4 users(those with id's 2,3,4,5).
If the above query is wrong, or i should follow another method or i should created some sort of relationships Please help.
Hopefully you have understood the question, i am new to Laravel but i am learning.
Probably what you need is three relations(one to many) in the User model. One for sent messages, one for received messages and one for both, like this:
public function messagesSent()
{
return $this->hasMany(Message::class, 'user_id');
}
public function messagesReceived()
{
return $this->hasMany(Message::class, 'receiver_id');
}
public function messages()
{
return $this->messagesSent()->union($this->messagesReceived()->toBase());
}
Then you should be able to get user messages like this: User::with('messages')->get();
I think you should use a join statement or "whereHas" to select users who have any messages.
$id = Auth::id();
$users = User::whereHas('messages', function ($query) use($id){
$query->where('user_id', $id)
->orWhere('received_id', $id);
})
->get();
To have access to "messages" you should add "with" statement too.
Adding my own solution(i.e working) to this question.
User Model
public function sent()
{
return $this->hasMany(Message::class, 'user_id');
}
public function received()
{
return $this->hasMany(Message::class, 'receiver_id');
}
Query
$users = User::whereHas('sent', function ($query) use($id) {
$query->where('receiver_id', $id);
})->orWhereHas('received', function ($query) use($id) {
$query->where('user_id', $id);
})->get();
dd($users);

Laravel get MySQL to query Builder statement in Eloquent model

Given the following table:
+----+----------+------------+
| id | parent_id| Name |
+----+----------+------------+
| 1 | NULL | Parent 1|
| 2 | 1 | Child 1|
| 3 | 1 | Child 2|
| 4 | 1 | Child 3|
| 5 | NULL | Parent 2|
| 6 | 1 | Child 4|
| 7 | 1 | Child 5|
| 8 | 1 | Child 6|
+----+----------+------------+
I want to select the same data, even if the provided ID is a parent or not.
So the provided ID = 1 (parent) or if the ID = 3 (child) I want to select this exact same data:
+----+----------+------------+
| id | parent_id| Name |
+----+----------+------------+
| 1 | NULL | Parent 1|
| 2 | 1 | Child 1|
| 3 | 1 | Child 2|
| 4 | 1 | Child 3|
+----+----------+------------+
I have a working MySQL query for it:
SELECT * FROM packages
WHERE id = 3
OR parent_id = 3
OR parent_id = (SELECT parent_id FROM packages WHERE id = 3)
OR id = (SELECT parent_id FROM packages WHERE id = 3)
So if you replace 3 with 1 you get the same results as expected.
I have already the following function in my Package Model:
<?php
public function related()
{
$childParent = Package::select( 'parent_id' )->where( 'id', $this->id )->first();
$query = $this->where( 'id', $this->id )
->orWhere( 'parent_id', $this->id );
if ( null !== $childParent->parent_id ) {
$query
->orWhere( 'parent_id', $childParent->parent_id )
->orWhere( 'id', $childParent->parent_id );
}
return $query;
}
But this feels... well... ugly. I am coming from Symfony where this kind of queries can get easily build (and with little codelines) with the ORM query builder.
Are I am missing a feature in Laravel Eloquent to make this nice and short?
Edit: I got already these functions which fall both short at not selecting the other children and eventual parent:
<?php
public function parent()
{
return $this->belongsTo( Package::class, 'parent_id' )->withTrashed();
}
public function childs()
{
return $this->hasMany( Package::class, 'parent_id' );
}
This is the perfect use-case for a relationship:
public function parent() {
$this->belongsTo(self::class, 'parent_id');
}
public function children() {
$this->hasMany(self::class, 'parent_id');
}
$parent = Model::with('children')->find(1); // with eager loading
$parent->children;
Hope this helps.

Laravel5: Eloquent and JOIN

Items Table
| id | item_id | item_title |
|-------|---------|------------|
| 1 | 1002 | A |
| 2 | 1003 | B |
| 3 | 1004 | C |
Sells Table
| id | item_id |
|----|-----------|
| 1 | 1002 1003 |
| 2 | 1003 1004 |
| 3 | 1004 1002 |
I want result : Sells Table 1. item title is A B
I want to combine the sells table with the item table and then match the item_id of the sells table to the item_title of the item table.
The table definitions look incorrect, you should have a pivot table linking items with sells, so a sell_item table:
item_id | sell_id
-----------------
1 | 1
1 | 3
2 | 1
2 | 2
3 | 2
3 | 3
Then using eloquent, you'd create models to represent your tables and define the relationships using BelongsToMany:
class Item extends Model {
public function sells() {
return $this->belongsToMany(Sell::class);
}
}
class Sell extends Model {
public function items() {
return $this->belongsToMany(Item::class);
}
}
Each instance of either model will then have access to it's related models via $item->sells and $sell->items.
The query builder can perform a join if not going the Eloquent route:
DB::table('sells')->join('items', 'sells.item_id', '=', 'items.item_id')
->select('sells.*', 'items.title')
->get();
The table definitions look incorrect, If you corrected already then your model replationship should be like
class Item extends Model {
public function sells() {
return $this->belongsToMany(Sell::class);
}
}
class Sell extends Model {
public function items() {
return $this->belongsToMany(Item::class);
}
}
Each instance of either model will then have access to it's related models via $item->sells and $sell->items.
The query builder can perform a join if not going the Eloquent route:
DB::table('sells')->join('items', 'sells.item_id', '=', 'items.item_id')
->select('sells.*', 'items.title')
->get();
Or if your model name is Sell then
$response=Sell::with('items')->get();

Laravel how to join one to many relationship tables

I am using Laravel 5.5 and I want to display list of data by joining tables that have one to many relationship.
Currently, I do this by going through the loop and make queries to retrieve data. This way, I think, is very inefficient, because if I were to display 1000 rows of data record, I will have to go 1000 loops to append other data with one-to-many relationship.
I am thinking to get around this problem using cache but it does not seem to solve fundamental problem.
For more understanding I have shared tables that I want do join as below.
Post Table
| id | comment_id | status |
|----|------------|--------|
| 1 | 1 | 0 |
| 2 | 2 | 0 |
| 3 | 3 | 1 |
| 4 | 4 | 0 |
| 5 | 5 | 1 |
| 6 | 6 | 1 |
Comment Table
| id | order_id | content |
|----|----------|----------|
| 1 | 1 | hi |
| 2 | 1 | hellow |
| 3 | 1 | yes |
| 4 | 1 | okay |
| 5 | 2 | bye |
| 6 | 2 | good bye |
If I were to join Table Post with Table Comment, because they have one to many relationship, rows would not match. How would I join these two tables to show the list of post with comments?
Sample List Controller
/**
* #param Request $request
* #return \Illuminate\Http\JsonResponse
*/
public function list(Request $request)
{
$vaildData = $request->validate([
'comment_id' => 'numeric',
]);
$posts = new PostModel;
$posts->find(1);
$displayPosts = [];
foreach ( $posts->find(1)->get() as $post ) {
$displayPosts->comments = $post->comment()->get();
}
return $displayPosts;
}
Post Model
namespace App\Model\Post;
use SoftDeletes;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
public function comments()
{
return $this->hasMany('App\Model\Post\Comment’, ‘post_id', 'id');
}
}
Use with() for eager loading your comments.
$posts = PostModel::with('comments')->find($id);
So your function will be like-
public function list(Request $request)
{
$vaildData = $request->validate([
'comment_id' => 'numeric',
]);
$posts = PostModel::with('comments')->find(1);
return $displayPosts;
}
You can filter your comments with comment_id using whereHas() like the following-
$comment_id = $request->input('comment_id');
$posts = PostModel::with('comments')->whereHas('comments', function ($query) use($comment_id)
{
$query->where('id', '=', $comment_id);
})->find(1);
https://laravel.com/docs/5.1/eloquent-relationships
Firstly, you may refer to this documentation.
To setup one-to-many relationship for Post and Comment table:
A Post has Many Comments
So in you Comment table there should be a column named post_id
Inside your Post.php
public function comments()
{
return $this->hasMany('App\Comment’);
}
Inside your controller
public function list(Request $request){
$posts = Post::where('id', 1)
->with('comments')
->get()
return $posts;
}
public function list(Request $request, $id)
{
$vaildData = $request->validate([
'comment_id' => 'numeric',
]);
$posts = PostModel::find($id);
return $posts->comments;
}
try this... Hope this can help you
or you can try like this
public function list(Request $request, $id)
{
$vaildData = $request->validate([
'comment_id' => 'numeric',
]);
$posts = PostModel::find($id)->comments()->get();
return $posts;
}
public function list(Request $request)
{
$vaildData = $request->validate([
'comment_id' => 'numeric',
]);
$posts = new PostModel;
$results = $posts->find(1)->with('comments')//comments is the function name you defined in the App\Model\Post
return resurts;
}
collection results contain the infomation of the post and another extra comment collection that belong to the post

Laravel query builder doesn't return all prices based on role = customer. Just return 1 price

I'm designing an application where a retailer can add a product with an initial price (store in a products table shown as example), then customers can claim the price of the product purchased from the retailer (this information stores in the prices table shown as example). The retailer then can update / reclaim the price inside the prices table too. and customers can reclaim the price of the product over and over again.
So, I have 2 roles of users called retailer and customer. I'm using Entrust Role package with the default relationship between role and user in the model. Before I explain next, here is my simple database design with all working example (feel free to ask for anything to include):
=============== MY Database Design with sample ===============
table users
__________________________
| id | email | password |
|-------------------------|
| 1 | a#g.com | 123 |
| 2 | b#g.com | 123 |
| 3 c#g.com | 123 |
| 4 d#g.com | 123 |
--------------------------
table roles
______________
|id | slug |
|--------------|
|1 | customer |
|2 | retailer |
----------------
table role_user
__________________
|id_user | id_role|
|------------------|
| 1 | 1 | -> a#gmail.com is a customer
| 2 | 2 | -> b#gmail.com is a retailer
| 3 | 1 | -> c#gmail.com is a customer
| 4 | 1 | -> d#gmail.com is a customer
------------------
table price:
(customer or retailer can claim 1 or more prices):
_____________________________________
|id| user_id | product_id | price |
|----------------------------|
|1 | 1 | 1 |10.00 | -> price claimed by a customer a#gmail.com on product 1
|2 | 2 | 1 |5.00 | -> price claimed by a retailer b#gmail.com on product 1
|3 | 1 | 1 |6.00 | -> price claimed by a previous customer a#gmail.com on product 1
|4 | 3 | 1 |5.00 | -> price claimed by a customer c#gmail.com on product 1
|5 | 2 | 1 |7.00 | -> price claimed by a previous retailer b#gmail.com on product 1
|6 | 3 | 1 |8.00 | -> price claimed by a customer c#gmail.com on product 1
Table products
_____________________________________
|id | user_id| name | Price
|-------------------------------------
| 1 | 1 | Milk | 10.00
| 2 | 2 | Phone | 12.33
| 3 | 1 | computer | 33.44
| 4 | 1 | Banana | 33.22
--------------------------------------
=============== MY Model Relationship ===============
Price model relationship
class Price extends Model
{
public function product()
{
return $this->belongsTo('App\Product');
}
public function user()
{
return $this->belongsTo('App\User');
}
}
Product model relationship
class Product extends Model
{
public function prices()
{
return $this->hasMany('App\Price');
}
}
User model relationship //a user can claim 1 or more prices
class User extends Model
{
public function prices ()
{
return $this->hasMany('App\Price');
}
}
=============== MY Product Controller ===============
This is the tricky part here on how to get the price of all customers except retailer:
class ProductController extends Controller
{
public function show($id)
{
$product = Product::findOrFail($id);
// This query should return all price claimed by customers except retailer. But the problem is, it only return 1 row, the first row which the output is 10.00.
$query_customer =$product->prices()->whereHas('user', function ($q) {
$q->whereHas('roles', function ($q) {
$q->where('slug', 'customer');
});
});
$latest_price_by_customer= $query_customer->value('price');
dd($latest_price_by_customer);
//it just return 1 row: price 10.00
/* It should return the collection that I can do foreach statement. The output should be like this:
10.00
6.00
5.00
7.00
8.00
*/
}
}
The query in the controller above return all prices claimed by customers except retailer. But the problem is, it only return 1 row, the first row which the output is 10.00.
It should output all prices claimed by customers from the prices table like below:
10.00
6.00
5.00
7.00
8.00
Any idea?
Update:
So far I changed my controller codes from this:
$product = Product::findOrFail($id);
$query_customer =$product->prices()->whereHas('user', function ($q) {
$q->whereHas('roles', function ($q) {
$q->where('slug', 'customer');
});
});
$latest_price_by_customer= $query_customer->value('price');
dd($latest_price_by_customer);
to this:
$product = Product::with('prices')->findOrFail($id);
$product_query= $product->prices()->where('product_id', $id) ->whereHas('user', function ($q) {
$q->whereHas('roles', function ($q) {
$q->where('slug', 'customer');
});
})->select('price')->get();
dd($product_query); //display collection and return the correct values
}
I have one small problem here: When loop through the collection
foreach($product_query->prices as $pr)
{
// dd($pr);
// echo $pr->price . ' ___ ' ;
}
I got an error of ErrorException in ProductController.php line 72:
Undefined property: Illuminate\Database\Eloquent\Collection::$prices
but the relationship is exist as shown.
If anyone looking for the answer this is the correct query that returns collection instead of 1 row:
$product = Product::with('prices')->findOrFail($id);
$product_query= $product->prices()->where('product_id', $id) ->whereHas('user', function ($q) {
$q->whereHas('roles', function ($q) {
$q->where('slug', 'customer');
});
})->select('price')->get();
foreach($product_query as $price)
{
echo $price->price;
}