I have troubleshoot all day with eager loading/n+1 issue, researched and read and watched tutorials about this issue, but haven't solved it yet. I have set up the relationships for the models, but when I passing in the data with a helper function I got this n+1 issue.
I want to grab an artist name from the url site.com/Artist/songs and get all its songs and display an url like this.
site.com/$Artist/songs/$id
My artists/index.blade.php view looks like this http://i61.tinypic.com/2nqzatk.jpg
I'm not sure what I'm missing here.
Thanks in advance!
My tables
songs
id, title, body, slug, hits, artist_id, created_at, updated_at
artists
id, name, body, created_at, updated_at
routes.php
Event::listen('illuminate.query', function($query)
{
var_dump($query);
});
...
Route::get('{artist}/songs', 'ArtistsController#index');
Route::get('{artist}/songs/{id}', ['as' => 'artist.songs.show', 'uses' => 'ArtistsController#show']);
Model: Song.php
class Song extends Eloquent {
protected $guarded = ['id'];
/**
* Setting up relationship for the artist model for easier usage
*
*/
public function artist()
{
return $this->belongsTo('Artist');
}
// Override find method
public static function find($id, $name = null)
{
$song = static::with('artist')->find($id);
// If the song is found
if ($song)
{
// If the song doesn't belong to that artist, throw an exception which will redirect to home, defined in global.php
if ($name and $song->artist->name !== $name)
{
throw new Illuminate\Database\Eloquent\ModelNotFoundException;
}
return $song;
}
// If the song is not found, throw an exception which will redirect to home, defined in global.php
else
{
throw new Illuminate\Database\Eloquent\ModelNotFoundException;
}
}
// Get songs from artist
public static function byArtist($name)
{
return Artist::byName($name)->songs;
}
}
Model Artist.php
class Artist extends Eloquent {
protected $fillable = [];
/**
* Setting up relationship with the song model for easier usage
* $artist->songs;
*/
public function songs()
{
return $this->hasMany('Song');
}
// Get artist by name
public static function byName($name)
{
return static::whereName($name)->first();
}
}
Controller: ArtistsController.php
class ArtistsController extends BaseController {
// Set default layout for this controller
protected $layout = 'layouts.master';
/**
* Display a listing of the resource.
* GET /artists
*
* #return Response
*/
public function index($name)
{
$this->data['songs'] = Song::byArtist($name);
$this->layout->content = View::make('artists.index', $this->data);
}
helpers.php
function link_to_artist_song(Song $song)
{
return link_to_route('artist.songs.show', $song->title, [$song->artist->name, $song->id]);
}
Index view for the artists
artists/index.blade.php http://i61.tinypic.com/2nqzatk.jpg
#extends('layouts.master')
#section('content')
#if(isset($songs))
<h1>All Songs</h1>
<ul class="list-group">
#foreach($songs as $song)
<li class="list-group-item">{{ link_to_artist_song($song) }}</li>
#endforeach
</ul>
#endif
#stop
You never eager load anything, that's why you could be facing n+1 issue.
If I get your right, what is a bit hard with the code you have here, you want all songs of given artist with $name from the url, right?
So here's everything you need to make it work:
// controller
public function index($name)
{
// with('songs') is eager loading related songs for you
$this->data['artist'] = Artist::with('songs')->whereName($name)->first();
$this->layout->content = View::make('artists.index', $this->data);
}
// the problem of your queries is in the helper:
function link_to_artist_song(Song $song)
{
return link_to_route('artist.songs.show', $song->title, [
$song->artist->name, // this is calling db query for each song to retrieve its artist (despite it is always the same)
$song->id]);
}
// so instead use this in your view
#foreach($artist->songs as $song)
<li class="list-group-item">
{{ link_to_route('artist.songs.show', $song->title, [$artist->name, $song->id]) }}
</li>
#endforeach
The n+1 problem exists when you have lots of songs with lots of artists. It has to get all the songs (1 query) and then for each song, get the artists (n queries).
In this case, you already know the song, so that's one query, then you need all the artists for that song, which is just one additional query.
The n+1 problem would only come into play if you were trying to find a song of a certain genre for example, which could return many songs. Then for each song, you would then have to make an additional query to get that song's artists. This would be where eager loading is most useful.
$song = Song::find($id);
$artist = $song->artist;
Every time you grab a song, and do $song->artist it will do a query.
You can also use Query scopes.
class Song extends Eloquent {
public function scopeByArtist($query, $name) {
return Artist::whereName($name)->first()->songs(); //->with('artist');
}
}
Like this the artist is already loaded.
Query with:
$songs = Song::byArtist($name)->get();
Related
What I am trying to do, is to fetch a row from a table, using a foreign key inside it, that links to another table, and then display a specific column of that row inside a view.
For some context, this is a website about sport events. Inside the view, the user should be able to see the details of the clicked event. Inside those details should be the sport category and the sport. However, I can't find how to do so, in Laravel 6.5.
What I have:
Database tables:
sport_categories (id, name)
sports (id, sport_category_id, name)
events (id, title, sport_category_id, sport_id)
EventsController
public function show(Event $event)
{
return view('events.show', ['event' => $event]);
}
View
<div id="event-sport-category">
<span>Sport Category:</span>
<span id="show-event-sport-category-label">{{$event->sport_category_id}}</span>
</div>
<div id="event-sport">
<span>Sport:</span>
<span id="show-event-sport-label">{{$event->sport_id}}</span>
</div>
SportCategory Model (empty)
class SportCategory extends Model
{
//
}
Sport Model
class Sport extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
Event Model
class Event extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
Of course at this point what is returned to the view are just the id columns of the events table. How can I return the corresponding name of each id?
Acoording to your database setup:
sport_categories (id, name)
sports (id, sport_category_id, name)
events (id, title, sport_category_id, sport_id)
So.. In this response I'm assuming that:
A Category has many Sports
A Sport has many Events
Notice: Given the fact that a Sport already belongs to a Category, you shouldn't need to specify a category_sport_id key in the events table: the related sport row should already have it.
Now, to your question..
How can I return the corresponding name of each id ?
Defining Relationships.
In your Category.php model:
class SportCategory extends Model
{
protected $guarded = []; // <---
// ...
public function sports() // <---
{
return $this->hasMany(Sport::class);
}
}
Your Sport.php model:
class Sport extends Model
{
protected $guarded = []; // <---
// ...
public function events() // <---
{
return $this->hasMany(Event::class);
}
public function category() // <---
{
return $this->belongsTo(SportCategory::class);
}
}
In your Event.php model:
class Event extends Model
{
protected $guarded = []; // <---
// ...
public function sport() // <---
{
return $this->belongsTo(Sport::class);
}
}
Notice: I added a protected $guarded = []; in each model, this is tell Laravel to include all the fields when returning it to the view. Read this.
Now that your relationships are been defined, you need to load the relationships before returning the variable to the view. Of course you could load the relationship in the view itself but to optimize your query you should eager load this objects like so (in this case, lazy eager loading because you are already resolving the object using Model Binding):
public function show(Event $event)
{
$event->load('sport.category'); // <---
return view('events.show', ['event' => $event]);
}
Now you should have this records in your $event variable: $event->sport and the nested $event->sport->category so in order to output them in your view just:
<p> {{ $event->sport->name }} </p>
<!-- ... -->
<p> {{ $event->sport->category->name }} </p>
Unfortunately, I don't have that much experience with Eloquent yet. I try to create a query from three tables which have two pivot tables.
My tables:
My Models:
Player
class Player extends Model
{
protected $table = 'players';
protected $fillable = [
'name'
];
public function layout(){
return $this->belongsToMany('App\Layout', 'layout_player', 'player_id', 'layout_id');
}
public function information(){
return $this->hasMany('App\Information', 'player_id');
}
}
Layout
class Layout extends Model
{
protected $table = 'layouts';
protected $fillable = [
'name'
];
public function player(){
return $this->belongsToMany('App\Player', 'layout_player', 'layout_id', 'player_id');
}
public function item(){
return $this->belongsToMany('App\Item', 'item_layout', 'layout_id', 'item_id');
}
}
Item
class Item extends Model
{
protected $table = 'items';
protected $fillable = [
'name'
];
public function layout(){
//return $this->hasOne(Layout::class);
return $this->belongsToMany('App\Layout', 'item_layout', 'item_id', 'layout_id');
}
}
Starting from the player, I want to retrieve the current player, all layouts and the corresponding items. Unfortunately I can't do it.
I call up the player and layouts as follows:
Player::where('id',1)->with('layout')->get();
How do I additionally get all items in the query?
You made a relationship perfectly. Now from Player to layout you're getting it with('layout'). Try it.
$players = Player::with('layout.item')->where('id',1)->get();
It'll give you players along with layouts with items.
If I understand your question, I think you are almost there.
Add this to Player model too, like other methods you did.
public function contents(){
return $this->belongsToMany('App\Content');
}
To get all contents regarding a player write these in controller and pass it to view file.
$player = Player::findOrFail(1);
return view('path.to.file_name',compact('player'));
In view file
//get all contents of a player
#foreach($player->contents as $content)
<p>{{ $content->text}}</p>
#endforeach
//get all layouts of a player
#foreach($player->layout as $layout)
<p>{{ $layout->name}}</p>
#endforeach
//get all items of a player
#foreach($player->layout as $layout)
<p>{{ $layout->name}}</p>
#foreach($layout->item as $item)
<p>{{ $item->name }}</p>
#endforeach
#endforeach
Thank you very much for the quick answer. Unfortunately this does not solve my problem.
I call the PlayerController via the api route and need all objects of the player in the form as return:
player
layout
item
public function show($id)
{
$player = Player::findOrFail($id);
//$player = Player::where('id',$id)->with('layout')->get();
return $player;
}
I get this response:
{"id":1,"name":"Testplayer","created_at":"2019-09-22 15:53:07","updated_at":"2019-09-22 15:53:07"}
But I need also the layouts and Items.
I hope you still understand my bad English.;)
I am trying to grasp the concept of Eloquent ORM by creating a ticketing system at the moment. What I am trying to achieve is:
The tickets with the user who posted the ticket
The feedback belonging to the ticket and the user who entered the
feedback
This is what I have right now:
// TicketController.php
public function index()
{
$tickets = Ticket::with('feedback')->with('user')->orderBy("created_at", "desc")->get();
//dd($tickets);
return View::make('modules.helpdesk.index')->withTickets($tickets);
}
And the following models
// Ticket.php
class Ticket extends Eloquent {
protected $table = 'helpdesk_tickets';
public function feedback()
{
return $this->hasMany('Feedback');
}
public function user()
{
return $this->belongsTo('User');
}
}
// Feedback.php
class Feedback extends Eloquent {
protected $table = 'helpdesk_tickets_feedback';
public function ticket()
{
return $this->belongsTo('Ticket');
}
}
// User.php
class User extends Eloquent {
protected $table = 'users';
public function ticket()
{
return $this->belongsTo('Ticket');
}
}
What I have now is the tickets, their related feedback and user who created the ticket. What I am trying to achieve now is to also get the user who created the feedback.
You need to fix the relation:
// User model
public function tickets()
{
return $this->hasMany('Ticket'); // adjust namespace if needed
}
Next add the relation:
// Feedback model
public function user()
{
return $this->belongsTo('User'); // namespace like above
}
then use eager loading:
// it will execute 4 queries:
// 1st for tickets
// 2nd for feedback
// 3rd for feedbacks' user
// 4th for tickets' user
$tickets = Ticket::with('feedback.user', 'user')->latest()->get();
you can then access the relations in a loop, like below:
#foreach ($tickets as $ticket)
{{ $ticket->title }} by {{ $ticket->user->name }}
#foreach ($ticket->feedback as $feedback)
{{ $feedback->content }}
#endforeach
#endforeach
What you want to do is create nested relations, just like Ticket add a belgonsTo relation on feeback
When you want to use it you can chain relations using the dot notation feedback.user
The code
// Feedback.php
class Feedback extends Eloquent {
protected $table = 'helpdesk_tickets_feedback';
public function ticket()
{
return $this->belongsTo('Ticket');
}
public function user()
{
return $this->belgonsTo('User')
}
}
// TicketController.php
public function index()
{
$tickets = Ticket::with('feedback')->with('user')->with('feedback.user')->orderBy("created_at", "desc")->get();
//dd($tickets);
return View::make('modules.helpdesk.index')->withTickets($tickets);
}
EDIT:
Even though this would work, it will execute more queries than needed. See Jareks answer.
Original Answer:
First of all you need to get your relationships straightened, in User.php you should call the user relationship with HasMany.
public function ticket() {
return $this->hasMany('Ticket');
}
In modules.helpdesk.index you should now have a Ticket Collection since your attaching the $ticket variable to the view.
If you loop through this collection with a foreach loop then what you should get is a model each loop:
foreach($tickets as $ticket) {
// Prints the name property of the Ticket model
print $ticket->name;
// Since a ticket only belongs to ONE user then that means that you are trying to fetch a model
// What we're doing here is getting the User model via the relationship you made in the model Ticket.php and then getting the name.
print $ticket->user()->first()->username;
// Since a ticket can have MANY feedbacks that means were fetching a collection
// which needs to be broken down to models so we do that looping the collection.
// Here we are doing the same thing as with the User model except with a collection.
foreach($ticket->feedback()->get() as $feedback) {
$feedback->text;
}
}
You should definitely check out the Laravel API and see Collection and Model there. http://laravel.com/api/ You get alot of help from there when you get stuck, trust me :)
I hope this answered your question.
I'm learning Laravel right now and i have following tables and resources (models, controllers, ect.):
tickets
- id
- title
- projectID
- statusID
projects
- id
- title
status
- id
- title
I have to make a list of my Tickets on the Startpage. Not nessesary to say that i need the Project- and Statustiltles and not the IDs. Currently i do:
Route::get('/', function()
{
$tickets = Ticket::all();
return View::make('layout')->with('tickets', $tickets);
});
My current output is:
tickets->id, tickets->title, tickets->projectID, tickets->statusID
The output i want is
tickets->id, tickets->title, tickets->projects->title, tickets->status->title
So i hope anyone can understand what i'm trying to ask here and maybe provide me some help. Thank you!
Resolution: I had to set the foreign_keys first in my DB. Then i used the relationships mentioned in the answers and it works fine.
My Model:
class Ticket extends \Eloquent {
protected $fillable = [];
public function project()
{
return $this->hasOne('Project', 'id', 'projectID');
}
public function status()
{
return $this->hasOne('Status', 'id', 'statusID');
}
}
My View:
#foreach($tickets as $key => $value)
...
<td>{{ $value->project->title }}</td>
<td>{{ $value->status->title }}</td>
...
#endforeach
If you configure you relationships correctly you can do that without problems using the Laravel Eager Loading feature, for example:
Eager Loading (Laravel docs)
Eager loading exists to alleviate the N + 1 query problem...
class Ticket extends Eloquent {
public function project()
{
return $this->belongsTo('Project', 'projectID', 'id');
}
public function status()
{
return $this->belongsTo('Status', 'statusID', 'id');
}
}
Now, just call the fields you want, for example:
foreach (Ticket::all() as $ticket)
{
echo $ticket->project->title;
echo $ticket->status->title;
}
Obs.: In your return object/array you can't see the relationships fields unless you do manual joins, etc. So, just configure your relationships and call the fields you want.
Sorry for my english
Define relationships specifying custom foreign keys (defaults would be status_id and project_id for your models):
// Ticket model
public function project()
{
return $this->belongsTo('Project', 'projectID');
}
public function status()
{
return $this->belongsTo('Status', 'statusID');
}
Then eager load related models:
$tickets = Ticket::with('project','status')->get();
// accessing:
foreach ($tickets as $ticket)
{
$ticket->status; // Status model with all its properties
$ticket->project; // Project model
}
Hi i thought i can handle this myself, but actually i don't know how to bite it.
I am trying to categorise my programs. There will be only 2 levels of categories:
1 CATEGORY
2 |-Subcategory
I want it to be as simple as possible.
- program can belong to only one subcategory,
- categories can have many subcategories,
- subcategories can have many programs,
Of course i would like to list all programs from subcategories, when someone choose a main category.
I am also not sure about my current database tables structure and relationship in models.
Tables in database:
programs: id, title, description, program_subcategory_id
programs_categories: id, name
programs_subcategories: id, name, program_category_id
Models:
Program.php
class Program extends Eloquent {
protected $table = 'programs';
public function user()
{
return $this->belongsTo('User');
}
public function subcategory()
{
return $this->belongsTo('ProgramSubcategory', 'program_subcategory_id');
}
}
ProgramCategory.php
class ProgramCategory extends Eloquent {
protected $table = 'programs_categories';
public function subcategories()
{
return $this->hasMany('ProgramSubcategory');
}
}
ProgramSubcategory.php
class ProgramSubcategory extends Eloquent {
protected $table = 'programs_subcategories';
public function programs()
{
return $this->hasMany('Program');
}
public function category()
{
return $this->belongsTo('ProgramCategory');
}
}
Actual controllers:
ProgramsController.php
class ProgramsController extends BaseController {
public function index()
{
$programs = Program::with('subcategory')->orderBy('programs.id', 'desc')->paginate(5);
$acategories = ArticleCategory::All();
$pcategories = ProgramCategory::All();
return View::make('programs.index', compact('programs', 'acategories', 'pcategories'));
}
}
ProgramsSubcatecories.php
class ProgramsSubcategories extends BaseController {
public function index($cname)
{
$programs = ProgramSubcategory::whereAlias($cname)->first()->programs()->orderBy('id', 'DESC')->paginate(10);
$pcategories = ProgramCategory::All();
$scategories = ProgramSubcategory::All();
$acategories = ArticleCategory::All();
return View::make('programs.index', compact('programs', 'pcategories', 'scategories ', 'acategories'));
}
public function show($cname, $id)
{
$category = ProgramSubcategory::whereAlias($cname)->first();
$program = $category->programs()->findOrFail($id);
$pcategories = ProgramCategory::All();
$acategories = ArticleCategory::All();
return View::make('programs.show', compact('program', 'category', 'pcategories', 'scategories ', 'acategories'));
}
}
It is not a problem for me to list all items from one category with eager loading. But i have problem how to do it with 2-levels categories.
Please advise how to start it.
You are not looking for eager loading, you need to solve how to manage hierarchical data in your database.
Nested sets model serves this purpose very well. You should read some theory on Wiki: http://en.wikipedia.org/wiki/Nested_set_model
Fortunately, there are Eloquent implementations already.
To mention some:
- Baum (the best free, imho), https://github.com/etrepat/baum
- Laravel Nested Set, https://github.com/atrauzzi/laravel-nested-set
- Laravel4-nestedset, https://github.com/lazychaser/laravel4-nestedset
and the paid one (surely highest quality as well)
from Cartalyst company - https://cartalyst.com/manual/nested-sets