I have two tables where I need to pluck the valid building ids (those that match the $buildingIds array) that exist in buildings AND must ensure they don't already exist in buildings_built (so we know which buildings need to be built). It can be done in two queries easily, but I am trying to do it with a single query to be more efficient:
$buildingIds = ['a1','b2','c3'];
Tables
// buildings (id, building_name) (a1, adam) (b2, barney) (c3, castor)
// buildings_built (id, building_id) (1, a1) (2, b2)
Here is my attempt at this that doesn't work properly:
$buildingsToBuildFromIdsArray = Buildings::whereIn('buildings.id', $ids)
->whereNotIn('buildings_built.building_id', $ids)
->pluck('buildings.id');
Ideally, the query should return ['c3'], since that building exists in buildings table and does not exist in buildings_built table (has not been built yet).
Any idea how to get it to work correctly?
What you could do is to create a relationship between two models - I will work with the default naming for models as Building and Build:
class Building extends Model
{
public function builds(): HasMany
{
return $this->hasMany(Build::class, 'building_id', 'id');
}
}
class Build extends Model
{
public function building(): BelongsTo
{
return $this->belongsTo(Building::class, 'id', 'building_id');
}
}
To fetch your records you could then use the doesntHave query builter method
Building::whereIn('id', $ids)->doesntHave('builds')->pluck('id');
Related
I got 3 tables. Table 1 & 2 has their ids as foreign keys in third one(pivot).
Relations for first one is
$this->hasMany("App\Pivot","game_id");
, second is
$this->belongsToMany("App\Pivot","army_id");
and pivot has relationships with both of them i.e belongsTo.
My schema:
I tried accessing it in controller of first one like this:
$games= Game::with("armies")->get();
Result that i get is array of games where instead of individual army data , i get collection from pivot table.
I can loop through it and get it that way, is there more elegant way of doing it?
If you are using pivot table this is the way how to do it.
Games Model
public function armies()
{
return $this->belongsToMany(App\Armies::class, 'pivot_table', 'game_id', 'army_id');
}
Armies Model
public function armies()
{
return $this->belongsToMany(App\Games::class, 'pivot_table', 'army_id', 'game_id');
}
Access the relationship like this..
Controller
App\Games::first()->armies()->get();
or
App\Games::first()->armies
or
App\Games::find(1)->armies
If you're going to use an intermediate table like that I'd probably do something like this:
Games model
public function armies()
{
return $this->belongsToMany('App\Armies');
}
Armies model
public function games()
{
return $this->belongsToMany('App\Games');
}
I'd keep the table structures all the same but rename the "pivot" table to armies_games since this is what Laravel will look for by default. If you want to keep it named Pivots, you'll need to pass it in as the second argument in belongsToMany.
With this, you don't really need the Pivot model, you should just be able to do:
$armies = Game::first()->armies()->get();
or
$armies = Game::find(3)->armies()->orderBy('name')->get();
or
$game = Game::first();
foreach ($game->armies as $army) {
//
}
etc.
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();
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
I have four tables in database: groups, specialties, lessons, group_lesson. It's structures:
groups
id
specialty_id
name
specialties
id
name
lessons
id
specialty_id
group_lesson (UNIQUE INDEX lesson_id, group_id)
lesson_id
group_id
date
My models look like that for now:
class Group extends Eloquent {
public function specialty() {
return $this->belongsTo('Specialty');
}
}
class Lesson extends Eloquent {
public function specialty() {
return $this->belongsTo('Specialty');
}
}
class Specialty extends Eloquent {
public function lessons() {
return $this->hasMany('Lesson');
}
public function groups() {
return $this->hasMany('Group');
}
}
I need get additional fields in Group model look like that
Group - Eloquent model
name - string
lessons - collection of Lesson models of Group Specialty
date - date from group_lesson table
I've tried different relationships and combinations, but it's doesn't work. Please help me to write correct relationships.
You can use eager-loading to access relational data through relationships, and can even chain relationships further. As a rule of thumb, if you can draw a path to from 1 model to another through a relationship, you can eagerload all the relevant and relational data for that with chained eager-loads.
Laravel Eager Loading
As an example
$speciality_group = Speciality::with('group','lessons')->find($id);
Even though you are only getting a single instance of the speciality model, the related data is hasMany, meaning multiple records. You need to loop through these records using a foreach loop to access the relevant data for them, or alternitavely add additional closures in your initial query to load only a single related model.
foreach($speciality_group->group as $group)
{
echo $group->name;
}
You will need to do this for both instances where you want to display related information.
In Laravel 4; I have model Project and Part, they have a many-to-many relationship with a pivot table project_part. The pivot table has a column count which contains the number of a part ID used on a project, e.g.:
id project_id part_id count
24 6 230 3
Here the project_id 6, is using 3 pieces of part_id 230.
One part may be listed multiple times for the same project, e.g.:
id project_id part_id count
24 6 230 3
92 6 230 1
When I show a parts list for my project I do not want to show part_id twice, so i group the results.
My Projects model has this:
public function parts()
{
return $this->belongsToMany('Part', 'project_part', 'project_id', 'part_id')
->withPivot('count')
->withTimestamps()
->groupBy('pivot_part_id')
}
But of course my count value is not correct, and here comes my problem: How do I get the sum of all grouped parts for a project?
Meaning that my parts list for project_id 6 should look like:
part_id count
230 4
I would really like to have it in the Projects-Parts relationship so I can eager load it.
I can not wrap my head around how to do this without getting the N+1 problem, any insight is appreciated.
Update: As a temporary work-around I have created a presenter method to get the total part count in a project. But this is giving me the N+1 issue.
public function sumPart($project_id)
{
$parts = DB::table('project_part')
->where('project_id', $project_id)
->where('part_id', $this->id)
->sum('count');
return $parts;
}
Try to sum in Collection,
$project->parts->sum('pivot.count');
This is best way I found. It's clean (easy to read) and able to re-use all of your scope, ordering and relation attribute caching in parts many-to-many defination.
#hebron No N+1 problem for this solution if you use with('parts') to eager load. Because $project->parts (without funtion call) is a cached attribute, return a instance of Collection with all your data. And sum('pivot.count') is a method of Collection which contains pure funcional helpers (not relative to database, like underscore in js world).
Full example:
Definition of relation parts:
class Project extends Model
{
public function parts()
{
return $this->belongsToMany('Part', 'project_part', 'project_id', 'part_id')
->withPivot('count')
->withTimestamps();
}
}
When you use it (note that eager load is important to avoid N+1 problem),
App\Project::with('parts')->get()->each(function ($project) {
dump($project->parts->sum('pivot.count'));
});
Or you can define the sum function in Project.php,
class Project extends Model
{
...
/**
* Get parts count.
*
* #return integer
*/
public function partsCount()
{
return $this->parts->sum('pivot.count');
}
}
If you want to avoid with('parts') on caller side (eager load parts by default), you can add a $with attribute
class Project extends Model
{
/**
* The relations to eager load on every query.
*
* #var array
*/
protected $with = ['parts'];
...
}
From the code source:
We need to alias all of the pivot columns with the "pivot_" prefix so we can easily extract them out of the models and put them into the pivot relationships when they are retrieved and hydrated into the models.
So you can do the same with select method
public function parts()
{
return $this->belongsToMany('Part', 'project_part', 'project_id', 'part_id')
->selectRaw('parts.*, sum(project_part.count) as pivot_count')
->withTimestamps()
->groupBy('project_part.pivot_part_id')
}
The best way that you can use is:
$project->parts->sum('pivot.count');
I faced the same problem, but this solved my issue.