Double relation in eloquent, no results - laravel

TL:DR;
City model - Locations model - Offers model.
1 city hasMany locations, locations belongToMany offers
City::with('locations.offers')->where('slug','=', $city)->first();
shows 0 offers from relation even though there is an offer record connected to location (which is connected to the city).
Longer version:
I have 3 models which should be connected to each other. City, Location & Offer.
I have the given city, and want to retrieve all locations with offers which are connected to the city. In the output of:
City::with('locations.offers')->where('slug','=', $city)->first();
I see the city and all locations connected to the city. Each location has an empty offer relation.
Model City:
class City extends Model
{
public function locations()
{
return $this->hasMany('App\Location');
}
}
Model Location
class Location extends Model
{
public function city()
{
return $this->belongsTo('App\City');
}
public function offers()
{
return $this->belongsToMany('App\Offer','location_offer','offer_id','location_id');
}
}
Model Offer
class Offer extends Model
{
public function locations()
{
return $this->belongsToMany('App\Location','location_offer','offer_id','location_id');
}
}
Database is pretty basic:
city table holds the city information (id+title)
location table has city_id (and location title)
location_offer table has location_id & offer_id
offer has offer information.
Clearly I'm doing something wrong, but can't seem to figure it out. Any tips would be greatly appreciated.

Your code is perfect there is a small mistake in your relation
public function offers()
{
return $this->belongsToMany('App\Entities\Offers','location_offer','location_id','offer_id');
}
You need to check this link the third argument is wrong
The third argument is the foreign key name of the model on which you are defining the relationship, while the fourth argument is the foreign key name of the model that you are joining to:
Check the Queries
This is the one that will be executed by your code
select `offer`.*, `location_offer`.`offer_id` as `pivot_offer_id`, `location_offer`.`location_id` as `pivot_location_id` from `offer` inner join `location_offer` on `offer`.`id` = `location_offer`.`location_id` where `location_offer`.`offer_id` in ('1', '2')
See join is wrong :
join location_offer on offer.id = location_offer.location_id
This is the right one
select `offer`.*, `location_offer`.`location_id` as `pivot_location_id`, `location_offer`.`offer_id` as `pivot_offer_id` from `offer` inner join `location_offer` on `offer`.`id` = `location_offer`.`offer_id` where `location_offer`.`location_id` in ('1', '2')
join location_offer on offer.id = location_offer.offer_id

Related

Laravel - Eloquent relation whereHas one or more other relations

I'm learning Laravel and Laravel eloquent at the moment and now I try to solve a problem using relations in Laravel.
This is what I want to archive:
The database holds many sport clubs. A sport club has a lot of teams. Each team has games. The teams table has a column named club_id. Now I want to create Eloquent relations to get all games of a club.
Here is what I got so far:
Club model
id => PRIMARY
public function games()
{
return $this->hasMany('App\Models\Games')->whereHas('homeTeam')->orWhereHas('guestTeam');
}
Game model
home_id => FOREIGN KEY of team ; guest_id => FOREIGN KEY of team
public function homeTeam()
{
return $this->belongsTo('App\Models\Team','home_id')->where('club_id','=', $club_id);
}
public function guestTeam()
{
return $this->belongsTo('App\Models\Team','guest_id')->where('club_id','=', $club_id);
}
Team model
id => PRIMARY ; club_id => FOREIGN
In my controller all I want to do is Club::findOrFail($id)->games()
Executing the code above returns a SQL error that the games table does not have a column named club_id.
What is the correct way to create this kind of relation?
Thanks!
EDIT
Thanks to Nikola Gavric I've found a way to get all Games - but only where club teams are the home or away team.
Here is the relation:
public function games()
{
return $this->hasManyThrough('App\Models\Game','App\Models\Team','club_id','home_id');
}
How is it possible to get the games where the home_id OR the guest_id matches a team of the club? The last parameter in this function does not allow an array.
There is method to retrieve a "distant relationship with an intermediary" and it is called Has Many Through.
There is also a concrete example on how to use it which includes Post, Country and User, but I think it will be sufficient to give you a hint on how to create games relationship inside of a Club model. Here is a link, but when you open it, search for hasManyThrough keyword and you will see an example.
P.S: With right keys naming you could achieve it with:
public function games()
{
return $this->hasManyThrough('App\Models\Games', 'App\Models\Teams');
}
EDIT #01
Since you have 2 types of teams, you can create 2 different relationships where each relationship will get you one of the type you need. Like this:
public function gamesAsHome()
{
return $this
->hasManyThrough('App\Models\Games', 'App\Models\Teams', 'club_id', 'home_id');
}
public function gamesAsGuests()
{
return $this
->hasManyThrough('App\Models\Games', 'App\Models\Teams', 'club_id', 'guest_id');
}
EDIT #02
Merging Relationships: To merge these 2 relationships, you can use merge() method on the Collection instance, what it will do is, it will append all the records from second collection into the first one.
$gamesHome = $model->gamesAsHome;
$gamesGuests = $model->gamesAsGuests;
$games = $gamesHome->merge($gamesGuests);
return $games->unique()->all();
Thanks to #HCK for pointing out that you might have duplicates after the merge and that unique() is required to get the unique games after the merge.
EDIT #03
sortBy also offers a callable instead of a attribute name in cases where Collection contains numerical indexing. You can sort your Collection like this:
$merged->sortBy(function($game, $key) {
return $game->created_at;
});
When you define that Club hasMany games you are indicating that game has a foreign key called club_id pointing to Club. belongsTo is the same but in the other way. These need to be coherent with what you have on your database, that means that you need to have defined those keys as foreign keys on your tables.
Try this...
Club model
public function games()
{
return $this->hasMany('App\Models\Games');
}
Game model
public function homeTeam()
{
return $this->belongsTo('App\Models\Team','home_id');
}
public function guestTeam()
{
return $this->belongsTo('App\Models\Team','guest_id');
}
Your Query like
Club::where('id',$id)->has('games.guestTeam')->get();

Laravel returns a Collection with duplicates of the first model

I'm developing a Laravel 5.7 (API) application with a PostgreSQL database behind it. The relevant Models are: User (customers and employees), Car, and Request.
An employee User creates a Request for a Car, that belongs to a customer User.
The relationships are:
Car (as customer) : User = n:m
Car : Request = 1:n
User : Request (as employee) = 1:n
(The data design is suboptimal, to put it mildly, but anyway, it's the given reality for now.)
Now to the actual issue. I want to display all Requests of a customer User:
Request::query()
->join('user_car', 'user_car.car_id', '=', 'request.car_id')
->join('user', 'user.id', '=', 'user_car.user_id')
->where('user.id', '=', $customer->id)
->select()
->get();
The customer with the given $customer->id has n Requests. And the length of the result Collection of the call above is correct. But all these n entries are duplicates of the first one. Means: I'm getting a list with n instances of Request#1.
Why does the first call return a list of references to the same Model object? Is it a (known) bug?
ADDITIONAL INFORMATION
Relationships:
class User extends \Illuminate\Foundation\Auth\User
{
// ...
public function cars()
{
return $this->belongsToMany('App\Car', 'user_car')->withTimestamps();
}
public function requests()
{
return $this->hasMany(Request::class, 'user_id');
}
}
class Car extends Model
{
// ...
public function users()
{
return $this->belongsToMany('App\User', 'user_car')->withTimestamps();
}
public function requests()
{
return $this->hasMany(Request::class);
}
}
class Request extends Model
{
// ...
public function car()
{
return $this->belongsTo(Car::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
The query is correct.
I logged the database requests, got the generated statement
SELECT *
FROM "request"
INNER JOIN "user_car" ON "user_car"."car_id" = "request"."car_id"
INNER JOIN "user" ON "user"."id" = "user_car"."user_id"
WHERE "user"."id" = 1;
..., and executed it manually. The result table contains as expected n different entries.
NOT just references
The result Collection's entries instances references to the different objects:
$test1 = $resultCollection->first();
$test2 = $resultCollection->last();
$test3 = spl_object_hash($test1);
$test4 = spl_object_hash($test2);
Xdebug output:
$test3 = "0000000077505ccd000000007964e0a8" <-- ccd0
$test4 = "0000000077505c33000000007964e0a8" <-- c330
Workaround
I found a workaround. This call
Request::whereIn('car_id', $customer->cars()->pluck('id')->toArray())->get();
... retrieves the correct/expected set of model.
First, note that your object hashes are not actually identical, and you're likely dealing with two separate instances.
What you're likely experiencing is an issue with ambiguous column names. When you JOIN together multiple tables, any matching/duplicate column names will contain the value of the last matching column. Your SQL GUI/client usually separates these. Unfortunately Laravel doesn't have a prefixing mechanism, and just uses an associative array.
Assuming all of your tables have a primary key column of id, every Request object in your result set will likely have the same ID - the User's ID you pass in the WHERE condition.
You can fix this in your existing query by explicitly selecting the columns you need to prevent ambiguity. Use ->select(['request.*']) to limit the returned info to the Request object data.

How to retrieve data through model?

I have Order model with another relation OrderPhoto:
public function OrderPhoto()
{
return $this->hasMany('App\OrderPhoto');
}
In turn OrderPhoto model has relation:
public function Photo()
{
return $this->belongsToMany('App\Photo');
}
So, how to get data from OrderModel with related data from third model Photo?
I guess this:
Order::with("OrderPhoto.Photo")->get();
to retrieve only data from Photo model for each Order
So, each Order has some OrderPhotos. Relationship is one to many.
But one item from OrderPhotos is related with primary key from table Photos. It is one to one relation.
My result query should be:
select `photos`.*, `ordersphoto`.`Orders_Id` from `photos` inner join `ordersphoto` on `ordersphoto`.`Photos_Id` = `photos`.`Id` where `ordersphoto`.`Orders_Id` in (1);
How to use hasManyThrough for this query?
Just having a quick look at your relationships it looks like you could create a hasManyThrough relationship on the order Model.
public function Photo {
return $this->hasManyThrough('App\OrderPhoto', 'App\Photo')
}
You may need to add the table keys to make it work
This will allow you to do:
Order::with("Photo")->get();
You can see more details here https://laravel.com/docs/5.5/eloquent-relationships#has-many-through
Update
Try this
public function Photo {
return $this->hasManyThrough('App\Photo', 'App\OrderPhoto', 'Order_id', 'Photos_id', 'id', 'id')
}
It is a little hard to get my head around your DB structure with this info but you should hopefully be able to work it out. This may also help
https://laravel.com/api/5.7/Illuminate/Database/Eloquent/Concerns/HasRelationships.html#method_hasManyThrough

Laravel Relationships: 3 Models

I have a problem with laravel relationships.
I have 3 models:
City —(hasMany) —>Clinic—(hasMany) —>Stock
Stock —(belongsTo) —>Clinic —(belongsTo) —>City
I need get all cities which has stocks, looks like «Stock->cities». I can write sql-query:
SELECT ct.id, ct.name
FROM CfgCity as ct
RIGHT JOIN lr_clinics AS cl ON(ct.id=cl.city_id)
RIGHT JOIN lr_clinic_stocks AS st ON(cl.id=st.clinic_id)
WHERE st.deleted_at IS NULL
GROUP BY ct.id
But I want decision in laravel-orm, because it’s more readable and I don’t need write names of rows and tables. Is it possible?
Thanks.
From your question it is clear that city has relation with clinic and clinic has relation with stock that means city has relation with stock through clinic. That you can define it using laravel relationship.
City Model
class City extends Model {
public function clinics(){
return $this->hasMany(Clinic::class);
}
public function stocks(){
return $this->hasManyThrough(Stock::class, Clinic::class);
}
}
Fetch data
$cities = City::whereHas('stocks')->get();
For details you can check https://laravel.com/docs/5.6/eloquent-relationships#has-many-through

HasManyThrough with extrafield

I have a relation:
Class Championship extends Model{
public function fights()
{
return $this->hasManyThrough(Fight::class, Tree::class);
}
}
When I do :
$championship->fights,
I get the fight Collection I need.
But I would also need to group them by area, and area belongs to Tree ( $tree->area ).
Is it posible to get this relation with an extra field from Tree????
Or should I add area field in Fight table ( but it sounds like data duplication )

Resources