Eloquent belongsToMany and Three Tables (Including Pivot) - mysql

I am working on an Ecommerce project using the Laravel framework. I have a products table, a flavors table, and a flavor_product table (I believe this is referred to as a pivot table). I have a Product model and a Flavor model (from what I have read it is unnecessary to create a model for the pivot table).
In the Product model I have defined the following relationship:
public function flavors()
{
return $this->belongsToMany('App\Flavor');
}
In the Flavor model I have defined the following relationship:
public function products()
{
return $this->belongsToMany('App\Product');
}
Now, in my Product controller I have attempted the following:
$flavors = Product::select('id')->with('flavors')->get();
which gets sent to the applicable view (product.blade.php).
In product.blade.php I have the following:
#foreach ($flavors as $flavor)
<select id="product-flavor" class="bs-select">
<option value="{{ $flavor }}">{{ $flavor }}</option>
</select>
#endforeach
So, what is the giving me? Well, it's showing me the following:
{"id":1,"flavors":[{"id":1,"name":"Cake Batter","created_at":"2018-05-29 20:12:56","updated_at":"2018-05-29 20:12:56","pivot":{"product_id":1,"flavor_id":1}},{"id":2,"name":"Caramel Coffee","created_at":"2018-05-29 20:48:25","updated_at":"2018-05-29 20:48:25","pivot":{"product_id":1,"flavor_id":2}},{"id":3,"name":"Chocolate Milkshake","created_at":"2018-05-29 20:49:09","updated_at":"2018-05-29 20:49:09","pivot":{"product_id":1,"flavor_id":3}},{"id":4,"name":"Cookies & Cream","created_at":"2018-05-29 20:49:50","updated_at":"2018-05-29 20:49:50","pivot":{"product_id":1,"flavor_id":4}},{"id":5,"name":"Vanilla Milkshake","created_at":"2018-05-29 20:50:16","updated_at":"2018-05-29 20:50:16","pivot":{"product_id":1,"flavor_id":5}}]}
which I definitely don't want all of that. All I want is to retrieve the name of the flavors associated to that product via the pivot table.
How can I proceed on this?
Edit
The following code is in the ShopController.php (regarding how the product is retrieved and displayed):
/**
* Display the specified resource.
*
* #param string $slug
* #return \Illuminate\Http\Response
*/
public function show($slug)
{
$product = Product::where('slug', $slug)->firstOrFail();
$mightAlsoLike = Product::where('slug', '!=', $slug)->mightAlsoLike()->get();
return view('product')->with([
'product' => $product,
'mightAlsoLike' => $mightAlsoLike,
]);
}
From web.php:
Route::get('/shop/{product}', 'ShopController#show')->name('shop.show');

What you're doing here is telling Laravel to select id from all products and retrieve all of their associated flavours.
Product::select('id') // <-- Will only select `id` attribute from the `products` table
->with('flavors') // <-- Will attach associated flavors to the product id
->get(); // <-- End the query (get all product ids and their flavours)
In your case, you don't want to select all the products and you have the relation set up correctly. So what you should do is call ->with('flavors') on your original query (actually not needed in this case, since you won't run into n+1 problems but still good practice):
$product = Product::where('slug', $slug)->with('flavors')->firstOrFail();
$mightAlsoLike = Product::where('slug', '!=', $slug)->mightAlsoLike()->get();
return view('product')->with([
'product' => $product,
'mightAlsoLike' => $mightAlsoLike,
]);
And then just call the $product->flavors in the view:
#foreach ($product->flavors as $flavor)
<select id="product-flavor" class="bs-select">
<option value="{{ $flavor }}">{{ $flavor }}</option>
</select>
#endforeach
The above view code would work as-is without actually calling ->with('flavors') on the query. ->with(...) eager loads the relationship and is good practice to explicitly call when loading multiple parents, to avoid n+1 problem (performance!).
But in your case you're only showing the flavors from 1 product, so there's no real advantage to calling it explicitly (except clearer code).
Edit to address the comment:
<select id="product-flavor" class="bs-select">
#foreach ($product->flavors as $flavor)
<option value="{{ $flavor->id }}">{{ $flavor->name }}</option>
#endforeach
</select>
You're seeing multiple selects because you're printing the whole select inside the loop instead of just printing the options.
And to display any attribute from the current flavor, just call the attribute name: $flavor->name for example.

Related

Laravel JOIN Problem with SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'album_id' in field list is ambiguous

I have referenced other questions like mine, but I cannot figure out why I am getting the error in my example.
I have 3 table. nn_album , nn_song and pivot table nn_song_album_song
My table ID names are nn_album.album_id , nn_song.song_id , pivot table has id as a primary key and same column name "song_id" and "album_id"
I can add an album but when i want to add a new song to an album, I'm getting this error on save function...
SQLSTATE[23000]: Integrity constraint violation: 1052 Column 'album_id' in field list is ambiguous (SQL: select album_id from nn_album inner join nn_song_album_song on nn_album.album_id = nn_song_album_song.album_id where nn_song_album_song.song_id = 21)
My Song Model
public function albums()
{
return $this->belongsToMany('App\Albums', 'nn_song_album_song', 'song_id', 'album_id');
}
My Album Model
public function songs()
{
return $this->belongsToMany('App\SongDetail', 'nn_song_album_song', 'song_id', 'album_id');
}
My controller Save Function
$albums = request('albums');
if ($song_id>0)
{
$entry = SongDetail::where('song_id', $song_id)->firstOrfail();
$entry->update($data);
$entry->albums()->sync($albums);
}
else
{
$entry = SongDetail::create($data);
$entry->albums()->attach($albums);
}
My View on Admin side for choose an album input (i can choose multiple album to add)
<div class="form-group form-float mainalbum" style="margin-bottom: 45px;border-bottom: solid 1px;">
<div class="form-line">
<select class="form-control show-tick" name="albums[]" id="albums" data-live-search="true" >
#foreach($albums as $album)
#if($album->album_id != 1)<option value="{{ $album->album_id }}" {{ collect(old('albums', $album))->contains($album->album_id) ? 'selected': '' }}>
{{ $album->album_name }}</option>#endif
#endforeach
</select>
<select class="albums2" name="albums2[]" id="albums2" multiple>
#foreach($albums as $album)
#if($album->album_id != 1)<option value="{{ $album->album_id }}" {{ collect(old('albums', $album))->contains($album->album_id) ? 'selected': '' }}>
{{ $album->album_name }}</option>#endif
#endforeach
</select>
</div>
</div>
Do you have any idea what's going on here? What is my fault? If you share with me your thoughts, i appreciate you.
Thank you.
Well you have really made things far more complicated than you need to, by naming everything against Laravel conventions. If this were set up as expected, you would have models named Album and Song, stored in database tables albums and songs, with a pivot table called album_song. Things would be much easier then.
But these conventions can be worked around if one reads the documentation:
As mentioned previously, to determine the table name of the relationship's joining table, Eloquent will join the two related model names in alphabetical order. However, you are free to override this convention. You may do so by passing a second argument to the belongsToMany method.
In addition to customizing the name of the joining table, you may also customize the column names of the keys on the table by passing additional arguments to the belongsToMany method. 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.
So, fixing your arguments we get:
class SongDetail extends Model
{
public function albums()
{
return $this->belongsToMany('App\Albums', 'nn_song_album_song', 'song_id', 'album_id');
}
}
class Albums extends Model
{
public function songs()
{
return $this->belongsToMany('App\SongDetail', 'nn_song_album_song', 'album_id', 'song_id');
}
}
Which should define the relationship properly. Your controller again defies convention, as Laravel resource controllers typically define separate methods for storing and updating. But I can make one suggestion to clean up the code you've shown:
$albums = request('albums');
try {
$entry = SongDetail::findOrFail($song_id);
$entry->update($data);
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
$entry = SongDetail::create($data);
}
$entry->albums()->sync($albums);
And in your view, if you want to pass an array of values you need to specify multiple on the <select> element.
Well what the sql error message is pointing you to this that album_id exists in both tables but you are selecting album_id without specifying what one you want hence it is ambiguous
You can likely resolve this by instead specify the exact column on the line
return $this->belongsToMany('App\Albums', 'nn_song_album_song', 'song_id', 'album_id');
by changing it to
return $this->belongsToMany('App\Albums', 'nn_song_album_song', 'nn_song_album_song.song_id', 'nn_song_album_song.album_id');
I am not 100% on this solution as this is not the most familiar syntax to me but specifying the table for the output parameters should resolve the issue. You may need to apply tho to both models. The actual solution may be a variation of this but you will solve this by more specifically specifying table names for the fields in your queries using the dot syntax mentioned here and in other answers.
When you joining two tables it joins column names also. You have to describe from which table you want to take album_id column. e.g. nn_album.album_id or nn_song_album_song.album_id
Your sql query has to look like this: select nn_album.album_id from nn_album inner join nn_song_album_song on nn_album.album_id = nn_song_album_song.album_id where nn_song_album_song.song_id = 21

Not being able to pass values into database from eloquent relationships in laravel 6

I am working on my first laravel project, which is a library application.
I am now working on a form to create a book, and I had to create many eloquent relationships here. I created the relationship languages to books, which was easy, because one book can only have one language (in this case), but one language can be used for several books.
The problem I am facing now is, that I want to be able to select the genres as well on the form, so I connected the genres table with my book table with a belongsToMany relationship and of course I have also created a pivot table.
Now, I think that the problem has to be in my BookController's store function, but I just can't figure out what it is.
Here is some of my code: (sorry if I'm missing something this is my first time asking a question here)
My BookController's store function:
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|max:255',
'author_id' => 'required',
'year' => 'required|numeric',
'publisher_id' => 'required',
'genres' => 'exists:genres,id',
'language_id' => 'required',
'isbn' => 'required|numeric',
'pages' => 'required|numeric',
]);
$book = Book::create($validatedData);
$book->genres()->attach(request('genre_id'));
return redirect('books')->with('success', 'Book was added!');
}
If I submit my form right now I get this message:
SQLSTATE[HY000]: General error: 1364 Field 'genre_id' doesn't have a default value (SQL: insert into books (title, author_id, year, publisher_id, language_id, isbn, pages, updated_at, created_at) values (Spanish for cats, 1, 1994, 1, 1, 1234567891128, 2337, 2020-02-17 16:37:07, 2020-02-17 16:37:07))
my create.blade.php
<div class="form-group">
<label for="genre_id">Genre(s):</label>
#foreach($genres as $genre)
<input type="checkbox" name="genre_id[]" value="{{ $genre->id }}">{{ $genre->name }}
#endforeach
</select>
#error('genre_id')
<div class="alert alert-danger">{{ $message }}</div>
#enderror
</div>
The value of the genres show up in my form, but I can't seem to input them into my database.
Any help would be highly appreciated!
First from your example I cannot be sure whether you intend to pass a
single genre via the request data or an array of genres as using the word genres leads me to believe that there could be more than one genre but the validation key is singular, use genres.* if you intend to pass an array of genre_id values.
This is one of the downsides to Laravel's magic methods, since you are requesting $book->genres()->attach(request('genre_id')) and request('genre_id') does not appear to exist in your validation you will find that you are actually passing a null value like so $book->genres()->attach(null) which is causing the error.
To resolve this issue you should change the ->attach() call to $book->genres()->attach($validatedData['genres']) which will ensure that you are passing your validated data to the attach method.

laravel - How to get data by pivot table

I have a problem with laravel. I have created relationships between tables and relationships at the model level in laravel but if I want to display data to the view, I get an error: Property [category] does not exist on this collection instance.
the table with the films is called: videos
the table with the categories is called: categories
the pivot table is called: category_video
Video model code:
public function categories()
{
return $this->belongsToMany('App\Category');
}
Categories model code:
public function videos()
{
return $this->belongsToMany('App\Video');
}
In controller i have this:
$slider_videos = Video::->orderBy('rating','desc')
->limit(5)
->get();
In view i try this:
#foreach($slider_videos as $slider_video)
{{$slider_video->category->name}}
#endforeach
I will add that when I use this: {{$slider_video->category}} it's displaying all contents of single row
By the way, how can I specify the name of the pivot table in the model?
Categories are a collection you need to loop over it :
#foreach($slider_videos as $slider_video)
{{ $slider_videos->name }} categories are :
#foreach($slider_video->categories as $category)
{{$category->name}}
#endforeach
#endforeach
belongsToMany returns a collection even if there is only a single record linked with the relationship.
You need to access it in a foreach loop.
Like so.
#foreach($slider_video->categories as $category)
{{$category->>field_name}}
#endforeach

Accessing the table fields with relation yii2

Model users has onetomany relation with category
public function getCategory()
{
return $this->hasMany(Category::className(), ['user_id' => 'user_id']);
}
I am not being able to access the fields of category using this relation.
public function actionGetResults()
{
$results = users::find()->where(['user_id' =>8])
->with('category')
->all();
echo "<pre>"; print_r($results);
}
$results here shows an array with fields of category. i.e category_name, category_id etc. but if i do this:
echo "<pre>"; print_r($results->category_name);
it fails.
First of all, since relation type is has many, category name is misleading and it's better to use categories.
Second, you are accessing related field wrong. $results is an array of User models and categories property of each model contains an array of related Category models to that user.
Use nested foreach loop:
foreach ($results as $user) {
foreach ($user->categories as $category) {
var_dump($category->name);
}
}
Or to get the name of the first user's first category:
$results[0]->categories[0]->name
Note that the second approach is only for demonstration purposes and can fail with Undefined index error if user or / and category does not exist.
Read Working with Relational Data for more info.

Laravel Fluent queries - How do I perform a 'SELECT AS' using Fluent?

I have a query to select all the rows from the hire table and display them in a random order.
DB::table('hire_bikes')->order_by(\DB::raw('RAND()'))->get();
I now want to be able to put
concat(SUBSTRING_INDEX(description, " ",25), "...") AS description
into the SELECT part of the query, so that I can select * from the table and a shortened description.
I know this is possible by running a raw query, but I was hoping to be able to do this using Fluent or at least partial Fluent (like above).
How can I do it?
You can actually use select AS without using DB::raw(). Just pass in an array into the select() method like so:
$event = Events::select(['name AS title', 'description AS content'])->first();
// Or just pass multiple parameters
$event = Events::select('name AS title', 'description AS Content');
$event->title;
$event->content;
I tested it.
Also, I'd suggest against using a DB:raw() query to perform a concatenation of your description field. If you're using an eloquent model, you can use accessors and mutators to perform this for you so if you ever need a limited description, you can simply output it in your view and not have to use the same query every time to get a limited description. For example:
class Book extends Eloquent
{
public function getLimitedDescriptionAttribute()
{
return str_limit($this->attributes['description'], $limit = 100, $end = '...');
}
}
In your view:
#foreach($books as $book)
{{ $book->limited_description }}
#endforeach
Example Output (not accurate to limit):
The description of this book is...
I'd also advise against using the DB facade because it always utilizes your default connection. If you're querying a secondary connection, it won't take this into account unless you actively specify it using:
DB::connection('secondary')->table('hire_bikes')->select(['name as title'])->get();
Just to note, if you use a select AS (name AS title) and you wish to update your the model, you will still have to set the proper attribute name that coincides with your database column.
For example, this will cause an exception because the title column does not exist in your database table:
$event = Events::select('name AS title')->first();
$event->title = 'New name';
$event->save(); // Generates exception, 'title' column does not exist.
You can do this by adding a DB::raw() to a select an array in your fluent query. I tested this locally and it works fine.
DB::table('hire_bikes')
->select(
array(
'title',
'url',
'image',
DB::raw('concat(SUBSTRING_INDEX(description, " ",25),"...") AS description'),
'category'
)
)
->order_by(\DB::raw('RAND()'))
->get();
select(array(DB::raw('latitude as lat'), DB::raw('longitude as lon')))