Laravel HasManyThrough with 3-way pivot table - php

I have the following models
Recipe
public function ingredientRecipeUnits()
{
return $this->hasMany(IngredientRecipeUnit::class);
}
Ingredient
public function ingredientRecipeUnits()
{
return $this->hasMany(IngredientRecipeUnit::class);
}
Unit
public function ingredientRecipeUnits()
{
return $this->hasMany(IngredientRecipeUnit::class);
}
and a pivot table (in its own model) connecting all three:
IngredientRecipeUnit
public function ingredient()
{
return $this->belongsTo(Ingredient::class);
}
public function recipe()
{
return $this->belongsTo(Recipe::class);
}
public function unit()
{
return $this->belongsTo(Unit::class);
}
I would like to fetch all Recipes through the ingredient model.
For this I made the following relationship:
public function recipes() {
return $this->hasManyThrough(
Recipe::class,
IngredientRecipeUnit::class,
'ingredient_id',
'id'
);
}
This generates an incorrect query looking like this
select * from `recipes`
inner join `ingredient_recipe_units`
on `ingredient_recipe_units`.`id` = `recipes`.`id`
where `ingredient_recipe_units`.`ingredient_id` = ?
while in reality the query should look like this. (Notice subtle change of id -> recipe_id on line 3)
select * from 'recipes'
inner join `ingredient_recipe_units`
on `ingredient_recipe_units`.`recipe_id` = `recipes`.`id`
where `ingredient_recipe_units`.`ingredient_id` = ?
Besides sending a pull-request to the Eloquent repo to add an extra Parameter or using raw SQL; is there any way to solve this?

This ended up being a mistake in the way I thought up the relationship and it was solved by simply defining a belongsToMany from the individual related models directly to the IngredientRecipeUnit table.
ex: Ingredient Model
public function recipes() {
return $this->belongsToMany(Recipe::class, 'ingredient_recipe_units');
}
Depending on your model you might have the possibility to add multiple of the same ingredient or unit, in this case you should mark the query with the instinct method.
Like this:
public function recipes() {
return $this->belongsToMany(Recipe::class, 'ingredient_recipe_units')->distinct();
}
Which correclty generates the following desired query:
select distinct * from `recipes`
inner join `ingredient_recipe_units`
on `recipes`.`id` = `ingredient_recipe_units`.`recipe_id`
where `ingredient_recipe_units`.`ingredient_id` = ?

Related

How to merge hasMany relationships and return a relationship in Laravel?

I'm trying to merge 2 collections, because I need to search for records in multiple columns, team_one_id and team_two_id depending on if its an away game or home game.
This happens in function matches when trying to merge them. When I call merge on the first relationship, function matches doesn't return an actual relationship, but returns a collection.
Exception:
SQLSTATE[21000]: Cardinality violation: 1222 The used SELECT statements have a different number of columns (SQL: (select count(*) as aggregate from `matches` where `matches`.`team_one_id` = 1 and `matches`.`team_one_id` is not null and `winning_team_id` = 1) union (select * from `matches` where `matches`.`team_two_id` = 1 and `matches`.`team_two_id` is not null))
Code:
<?php
namespace App\Database;
use Illuminate\Database\Eloquent\Model;
class Team extends Model
{
protected $primaryKey = 'id';
protected $table = 'teams';
public $timestamps = false;
protected $guarded = ['id'];
public function homeMatches() {
return $this->hasMany('App\Database\Match', 'team_one_id');
}
public function awayMatches() {
return $this->hasMany('App\Database\Match', 'team_two_id');
}
public function matches() {
return $this->homeMatches()->union($this->awayMatches()->toBase());
}
}
Eloquent Relationships aren't designed to match one or the other field, they are designed to have one foreign key. You can run a query within your method to get the results you need without relying a relationship.
As discussed in the comments, if you want to re-use that query, contain that logic in a protected function.
protected function allMatchesQuery()
{
// This needs to be wrapped in a nested query or else your orWhere will not be contained in parentheses.
return Match::where(function($q) {
$q->where('team_one_id', $this->id)->orWhere('team_two_id', $this->id);
});
}
public function getMatches() {
return $this->allMatchesQuery()->get();
}
public function getRecentMatches() {
return $this->allMatchesQuery()->orderBy('date', 'DESC')->limit(10)->get();
}
You may try this:
public function matches() {
return $this->getRelationValue('homeMatches')->union($this->getRelationValue('awayMatches')->toBase());
}
Because of homeMatches() function return a Relation instance but the dynamic property returns a Collection.

How should make this relationship and access data in Laravel

I have 4 tables:
Table name: clients
Fields: id, name, slug
Table name: projects
Fields: id, slug
Table name: project_translation
Fields: id, locale, project_id, title
Table name: client_project
Fields: id, client_id, project_id
Relationships
In the Project model
public function clients()
{
return $this->belongsToMany(Client::class,'client_project')->withTimestamps();
//return $this->belongsToMany('Client')->withTimestamps()->orderBy('priority', 'desc');
}
In the Client model
public function projects()
{
return $this->belongsToMany(Project::class,'client_project')->withTimestamps();
}
public function translate()
{
return $this->belongsToMany(ProjectTranslation::class,'project_translations')->withTimestamps();
}
In Client_Project model
public function clients()
{
return $this->hasMany('App\Models\Project');
}
public function projects()
{
return $this->hasOne('App\Models\Client');
}
In ProjectTranslation model
public function client()
{
return $this->hasMany('App\Models\Client');
}
And I'm trying to access data in controller like this:
$client_project = Client::find($id)->translate;
return $client_project;
This give me the next error:
SQLSTATE[42000]: Syntax error or access violation: 1066 Not unique
table/alias: 'project_translations' (SQL: select
`project_translations`.*, `project_translations`.`client_id` as
`pivot_client_id`, `project_translations`.`project_translation_id` as
`pivot_project_translation_id`, `project_translations`.`created_at` as
`pivot_created_at`, `project_translations`.`updated_at` as
`pivot_updated_at` from `project_translations` inner join
`project_translations` on `project_translations`.`id` =
`project_translations`.`project_translation_id` where `project_translations`.`client_id` = 22)
I'm not sure, but I think something is wrong with Relationships.
I'm in Client blade, and I want to show the projectstranslations of the projects of this client.
Here I formatted you query so it's readable, as I said in the comments I am not a laravel user. But I know Sql
select
`project_translations`.*,
`project_translations`.`client_id` as `pivot_client_id`,
`project_translations`.`project_translation_id` as `pivot_project_translation_id`,
`project_translations`.`created_at` as `pivot_created_at`,
`project_translations`.`updated_at` as `pivot_updated_at`
from
`project_translations` <-- Duplicate Table with no alias
inner join
`project_translations` on `project_translations`.`id` = `project_translations`.`project_translation_id`
where
`project_translations`.`client_id` = 22
you don't need Client_Project model.
In the Project model
public function clients()
{
return $this->belongsToMany(Client::class,'client_project')->withTimestamps();
}
public function translates()
{
return $this->hasMany(ProjectTranslation::class);
}
In the Client model :
public function projects()
{
return $this->belongsToMany(Project::class,'client_project')->withTimestamps();
}
In the ProjectTranslation model :
public function project()
{
return $this->belongsTo('App\Models\Project');
}
In the controller :
$client_projects = Client::find($id)->projects; //all client projects
return $client_projects;
In the view after geting this $client_projects and looping over it you can get the translations of a project by :
$client_project->translates // for single projects you will get its translates :)
For Project you have many to many with clients and it has one to many translates => in the documentation links there are many examples :)

Laravel belongsToMany not returning results

I have the following schema set up:
users:
id
departments:
id
department_user:
id
department_id
user_id
I also have the following relationships set up:
User Model
public function departments()
{
return $this->belongsToMany('App\Resources\Eloquent\Models\Department', 'department_users');
}
Department Model
public function users()
{
return $this->belongsToMany(User::class, 'department_users');
}
For some reason, when I am trying to access through the user model $user->departments, it doesn't work - but $department->users does.
Outputting the eloquent query is as follows:
select `departments`.*, `department_users`.`user_id` as `pivot_user_id`, `department_users`.`department_id` as `pivot_department_id` from `departments` inner join `department_users` on `departments`.`id` = `department_users`.`department_id` where `department_users`.`user_id` is null
I can't seem to figure out why it is looking to see if department_users.user_id is null, when it should be looking for the user's id.
Any ideas?
Why don't you set up your models like it is suggested in the documentation here:
So your models would look something like this:
User Model
public function departments()
{
return $this->belongsToMany('path\to\your\model\Department');
}
Department Model
public function users()
{
return $this->belongsToMany(path\to\your\model\User);
}
Eloquent will join the two related model names in alphabetical order.So you don't need extra arguments when defining your relationship and Laravel also by default, makes model keys present on the pivot object. And then you can do something like this:
$department = path\to\your\model\Department::find(1);
foreach ($department->users as $user) {
echo $user;
}
For some reason, if I make the relationship the following - it works.
return $this->belongsToMany(Department::class, 'department_users')->orWhere('department_users.user_id', $this->id);
If anyone knows why, please let me know

How to set Eloquent relationship belongsTo THROUGH another model in Laravel?

I have a model Listing that inherits through its belongsTo('Model') relationship should inherently belong to the Manufacturer that its corresponding Model belongs to.
Here's from my Listing model:
public function model()
{
return $this->belongsTo('Model', 'model_id');
}
public function manufacturer()
{
return $this->belongsTo('Manufacturer', 'models.manufacturer_id');
/*
$manufacturer_id = $this->model->manufacturer_id;
return Manufacturer::find($manufacturer_id)->name;*/
}
and my Manufacturer model:
public function listings()
{
return $this->hasManyThrough('Listing', 'Model', 'manufacturer_id', 'model_id');
}
public function models()
{
return $this->hasMany('Model', 'manufacturer_id');
}
I am able to echo $listing->model->name in a view, but not $listing->manufacturer->name. That throws an error. I tried the commented out 2 lines in the Listing model just to get the effect so then I could echo $listing->manufacturer() and that would work, but that doesn't properly establish their relationship. How do I do this? Thanks.
Revised Listing model (thanks to answerer):
public function model()
{
return $this->belongsTo('Model', 'model_id');
}
public function manufacturer()
{
return $this->belongsTo('Model', 'model_id')
->join('manufacturers', 'manufacturers.id', '=', 'models.manufacturer_id');
}
I found a solution, but it's not extremely straight forward. I've posted it below, but I posted what I think is the better solution first.
You shouldn't be able to access manufacturer directly from the listing, since manufacturer applies to the Model only. Though you can eager-load the manufacturer relationships from the listing object, see below.
class Listing extends Eloquent
{
public function model()
{
return $this->belongsTo('Model', 'model_id');
}
}
class Model extends Eloquent
{
public function manufacturer()
{
return $this->belongsTo('manufacturer');
}
}
class Manufacturer extends Eloquent
{
}
$listings = Listing::with('model.manufacturer')->all();
foreach($listings as $listing) {
echo $listing->model->name . ' by ' . $listing->model->manufacturer->name;
}
It took a bit of finagling, to get your requested solution working. The solution looks like this:
public function manufacturer()
{
$instance = new Manufacturer();
$instance->setTable('models');
$query = $instance->newQuery();
return (new BelongsTo($query, $this, 'model_id', $instance->getKeyName(), 'manufacturer'))
->join('manufacturers', 'manufacturers.id', '=', 'models.manufacturer_id')
->select(DB::raw('manufacturers.*'));
}
I started off by working with the query and building the response from that. The query I was looking to create was something along the lines of:
SELECT * FROM manufacturers ma
JOIN models m on m.manufacturer_id = ma.id
WHERE m.id in (?)
The query that would be normally created by doing return $this->belongsTo('Manufacturer');
select * from `manufacturers` where `manufacturers`.`id` in (?)
The ? would be replaced by the value of manufacturer_id columns from the listings table. This column doesn't exist, so a single 0 would be inserted and you'd never return a manufacturer.
In the query I wanted to recreate I was constraining by models.id. I could easily access that value in my relationship by defining the foreign key. So the relationship became
return $this->belongsTo('Manufacturer', 'model_id');
This produces the same query as it did before, but populates the ? with the model_ids. So this returns results, but generally incorrect results. Then I aimed to change the base table that I was selecting from. This value is derived from the model, so I changed the passed in model to Model.
return $this->belongsTo('Model', 'model_id');
We've now mimic the model relationship, so that's great I hadn't really got anywhere. But at least now, I could make the join to the manufacturers table. So again I updated the relationship:
return $this->belongsTo('Model', 'model_id')
->join('manufacturers', 'manufacturers.id', '=', 'models.manufacturer_id');
This got us one step closer, generating the following query:
select * from `models`
inner join `manufacturers` on `manufacturers`.`id` = `models`.`manufacturer_id`
where `models`.`id` in (?)
From here, I wanted to limit the columns I was querying for to just the manufacturer columns, to do this I added the select specification. This brought the relationship to:
return $this->belongsTo('Model', 'model_id')
->join('manufacturers', 'manufacturers.id', '=', 'models.manufacturer_id')
->select(DB::raw('manufacturers.*'));
And got the query to
select manufacturers.* from `models`
inner join `manufacturers` on `manufacturers`.`id` = `models`.`manufacturer_id`
where `models`.`id` in (?)
Now we have a 100% valid query, but the objects being returned from the relationship are of type Model not Manufacturer. And that's where the last bit of trickery came in. I needed to return a Manufacturer, but wanted it to constrain by themodelstable in the where clause. I created a new instance of Manufacturer and set the table tomodels` and manually create the relationship.
It is important to note, that saving will not work.
$listing = Listing::find(1);
$listing->manufacturer()->associate(Manufacturer::create([]));
$listing->save();
This will create a new Manufacturer and then update listings.model_id to the new manufacturer's id.
I guess that this could help, it helped me:
class Car extends Model
{
public function mechanical()
{
return $this->belongsTo(Mechanical::class);
}
}
class CarPiece extends Model
{
public function car()
{
return $this->belongsTo(Car::class);
}
public function mechanical()
{
return $this->car->mechanical();
}
}
At least, it was this need that made me think of the existence of a belongsToThrough
You can do something like this (Student Group -> Users -> Poll results):
// poll result
public function studentGroup(): HasOneDeep
{
return $this->hasOneDeepFromRelations($this->user(), (new User())->studentGroup());
}

MySQL relation with multiple tables

I have 4 mysql tables, as the following:
Makes:
id - make_name
Models:
id - model_name - make_id
Trims:
id - trim_name - model_id
Forsale_Cars:
id - trim_id - year - price
in the Makes table I have ~700 records, so my question is, how can get a list of Makes which only have a child trim in the forsale table?
I have 20 records in the forsale table, I want to get the list of Makes for these cars.
I am using Laravel, so if anybody has achieved that previously using eloquent it will be great
Eloquent way:
// Makes that have forsale nested relation
Make::whereHas('models', function ($q) {
$q->whereHas('trims', function ($q) {
$q->has('forsales');
});
})->get(); // returns Eloquent Collection
Models with correct relations (hasMany can be replaced with hasOne if that's actual relation somewhere):
// Make model
public function models()
{
return $this->hasMany('CarModel');
}
// CarModel (as you can't use Model name)
public function trims()
{
return $this->hasMany('Trim');
}
public function make()
{
return $this->belongsTo('Make');
}
// Trim model
public function forsales()
{
return $this->hasMany('Forsale');
}
public function carModel()
{
return $this->belongsTo('CarModel');
}
// Forsale model
public function trim()
{
return $this->belongsTo('Trim');
}
You could use this query for the first question you asked. Not sure if the this completely answers your question.
SELECT * FROM Makes m left join Foresale_Cars fc on (m.id = fc.id) left join Trims t on (fc.trim_id = t.id) where t.trim_name = "child trim"
I assume you have all your models set up, e.g.:
class Makes extends Eloquent {
protected $table = 'makes';
public function models() {
return $this->hasMany('Model', 'makes_id');
}
}
and so on.. (you have to do this with all your models, of course)
Now, if you want to get all the cars for sale you'd simply chain some foreach loops:
foreach( $makes->models as $model ) {
foreach( $model->trims as $trim ) {
{{ $trim->forsale_cars }}
...
}
}
Edit: Yes you can use raw queries, of course, but using models and the power of eloquent is much more elegant and useful...
For more information on this topic: http://laravel.com/docs/eloquent#relationships

Categories