Laravel relationship problems - php

I've got 4 tables:
My relationships should work like this:
Items can only have one size, color and category.
This should be working but it's really not. The query generated returns wrong results.
Here are my model files:
<?php
class Shop_Item extends Eloquent
{
public static $table = 'items';
public static $timestamps = false;
public function scategory() {
return $this->has_one('Shop_Category','id');
}
public function ssize() {
return $this->has_one('Shop_Size','id');
}
public function scolor() {
return $this->has_one('Shop_Color','id');
}
}
The rest of the model files for the remaining tables are the same (except table name and model name).
<?php
class Shop_Category extends Eloquent
{
public static $table = 'item_categories';
public static $timestamps = false;
}
So when I try to access the extra values (size->name, color->name, category->name), I get wrong results.
I have two test records in my database:
Item 1 and Item 2 with different color, size and category.
Item 1 is blue and have the size of M,
Item 2 is green and have the size of XL, but not in the returned query. Which shows me, that Item 2 is red and have the size of S.
Controller:
<?php
class Admin_Shop_Controller extends Base_Controller {
public function action_index() {
$items = Shop_item::order_by('name')->paginate(10,array('id','name','price','sex','visible','color','size','category'));
return View::make('admin.shop.index')->with('items', $items);
}
View:
#forelse($items->results as $i)
{{ $i->name }}
{{ $i->price }}
{{ $i->sex }}
{{ $i->scategory->name }}
{{ $i->scolor->name }}
{{ $i->ssize->name }}
Edit
Delete
#empty
There are no items in the shop.
#endforelse
Queries generated:
0.23ms
SELECT COUNT(`id`) AS `aggregate` FROM `items`
0.28ms
SELECT `id`, `name`, `price`, `sex`, `visible`, `color`, `size`, `category` FROM `items` ORDER BY `name` ASC LIMIT 10 OFFSET 0
0.25ms
SELECT * FROM `item_categories` WHERE `id` IN ('1', '2')
0.21ms
SELECT * FROM `item_sizes` WHERE `id` IN ('1', '2')
0.36ms
SELECT * FROM `item_colors` WHERE `id` IN ('1', '2')
Note that in the view if I access these values from the other table like this:
{{ Shop_Color::find($i->color)->name }}
It gets me the right result, but I really don't want to query the database n+3 times because of this. Any suggestions what am I doing wrong?
Edit: Still no luck. :( I've done the changes you listed, experimented with them but this thing still not working. Current error is :
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'id' in 'where clause'
SQL: SELECT * FROM `item_colors` WHERE `id` IN (?)
Bindings: array (
0 => 0,
)
I don't know why it looks for an id, I've changed all references to the child tables and renamed columns appropriately. :(

Number of queries !== Performance. In your case, you're doing nearly-atomic queries (the where id in are all done vs. primary indices, which will make them fast - as fast as a JOIN if you exclude the returning-result process time). If you nevertheless want to solve the n+1 query problem, Eloquent has a solution for this: eager-loading.
Instead of this:
Shop_item::order_by('name')
Use this:
Shop_item::with(array("scategory", "ssize", "scolor"))->order_by('name')
You should see only one query by using this. The doc for this feature is at: http://laravel.com/docs/database/eloquent#eager

There are quite a few things going on here.
I would name your reference columns thing_id... so that's color_id, size_id and category_id. This will allow you to set up relationships named 'color', 'size' and 'category', instead of 'sthing'.
You need belongs_to() instead of has_one(). Laravel assumes that the ID will be like thing_id, so if you've updated as 1 above then you can update your references like $this->belongs_to('Shop_Size'). If not, then you should use the reference column here, like $this->belongs_to('Shop_Size', 'size').
When you use Eloquent models it's best not to restrict the columns - you might have logic in your model that depends on them all being there.
You can use eager loading to improve the queries, but the way Eloquent works it will still need a query per relationship. Have a look at this line for action_index()
$items = Shop_Item::with(array('color', 'size', 'category'))
->order_by('name')->paginate(10);
After all of the edits above you will be able to write code like this in your view...
#forelse ($items->results as $item)
<p>Color: {{ $item->color->name }}</p>
#else
<p class="no-results">There are no items in the shop.</p>
#endforelse
Most of this is covered in the Eloquent docs, particularly Relationships and Eager Loading.

Related

Laravel sort child relationship collection

I have the following 3 tables:
Movie
- id
Series
- id
- status_id
- movie_id
Status
- id
- order_number
this is the code on the controller:
$movie = Movie::where('slug', $slug)->with(['series'])->first();
this is the code for view:
#foreach ($movie->series as $series)
{{ $series->name}}
#endforeach
how to sort $movie->series based on status->order_number? if it can be written on the model, so every order is only written once the controller?
is there a code that I can use for example like this:
$movie->series->sortBy('status.order_number');
Yes, but you will need to join status with series:
$movie = Movie::where('slug', $slug)->with([
'series' => function ($query) {
// Subquery on `series` table
return $query
// select whatever you'll need
->select('series.*')
// join with status
->join('status', 'series.status_id', '=', 'status.id')
// order by order number
->orderBy('status.order_number')
// * you can drop this if you select all the fields you need and use those
->with('status');
},
])->first();
Edit this ^ method will sort on SQL level, but you could also do this with collections:
#foreach ($movie->series->sortBy('status.order_number') as $series)
{{ $series->name}}
#endforeach
In that case also add .status to your with to avoid n + 1 problem: ->with(['series.status'])
The reason your attempt didn't work is because ->sortBy(..) doesn't mutate the collection, it just returns a new sorted one. This would work:
$movie->series = $movie->series->sortBy('status.order_number');

Eloquent ORM Many-to-many relationship

I've just started using Eloquent ORM (Without Laravel) and I am having issues with the many to many relationships.
I have a table where I store Families (Article categories), another one for the Articles, and a third one as a "pivot". I want to be able to get all the articles a Family has, and all the families an article belongs to. So I have coded this models.
Families
class Families extends Model {
public $table = 'Families';
public function Articles() {
return $this->belongsToMany('Articles', 'articles_families', 'families_id', 'articles_id');
}
}
Articles
class Articles extends Model {
public $table = 'Articles';
public function Families() {
return $this->belongsToMany('Families', null, 'articles_id', 'families_id');
}
}
Then I am trying to retrieve the data like this:
$families = Families::all();
echo $families[1]->Articles;
However, it just returns an empty array, when it should return a couple of articles. I have tripled checked that all the values are correct in the three tables. If I echo the Eloquent query debugger I can see that it is looking for a null value and I'm pretty sure that's the problem, but I don't quite know how to fix it. Here:
{"query":"select * from `Families`","bindings":[],"time":49.13},{"query":"select `Articles`.*, `articles_families`.`families_id` as `pivot_families_id`, `articles_families`.`articles_id` as `pivot_articles_id` from `Articles` inner join `articles_families` on `Articles`.`id` = `articles_families`.`articles_id` where `articles_families`.`families_id` is null","bindings":[],"time":38.93}
The null value is at the end of the last query.
I just found the solution myself. As my primary key columns are called Id, and Eloquent by default assumes the primary key is called id, I needed to override that by adding a class property protected $primaryKey = "Id"; and it now retrieves the data properly.

Query an object via multiple pivot tables with Laravel's ORM Eloquent

What I want to do, is to retrieve data through multiple relationships (pivot tables).
I have three database tables
users, cities, and jobs.
They are built like this (They aren't, just to give you a glimpse)
Users table
id int(11) PK, Autoincrement,
name varchar(255)
Cities table
id int(11) PK, Autoincrement,
name varchar(255)
Jobs table
id int(11) PK, Autoincrement,
name varchar(255)
Now I have pivot tables, because these are many to many relationships. I got the tables city_user and job_user.
CityUser table
city_id int(11),
user_id int(11)
JobUser table
job_id int(11),
user_id int(11)
In every Class (User, City, Job), I got the relationships defined with a belongsToMany method.
City class
/**
* Defines the relation between the city and its users
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function users()
{
return $this->belongsToMany(User::class, 'city_user');
}
User class
/**
* Defines the relation between the user and its cities
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function cities()
{
return $this->belongsToMany(City::class, 'city_user');
}
The same goes for the jobs table. What I now want to do is to get all users that are in a specific city and have a specific job.
Let's imagine I want to query all users who live in City with the ID 5, and have the job with the ID 10.
Going for one relationshiop is fairly easy
$users = City::find(5)->users;
or
$users = Job::find(10)->users;.
But, how do I achieve that for multiple relationships in this case? I tried to do it like this
$users = City::find(10)->with(['users' => function($query) use ($jobId) {
// Here I'm stuck. I wouldn't know how to query further? Maybe like this
return $query->with('jobs')->whereJobId($jobId);
}]);
But, when I do it like this, I'm getting 0 results, so there must be something wrong. Any ideas?
I'm getting an error though
Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[42S22]: Column not found: 1054 Unknown column 'job_id' in 'where clause'' in
I also tried it like this (inside the with(['consultants']))
$query->with(['jobs' => function ($query) use ($jobId) {
$query->whereJobId($jobId);
}]);
but that doesn't look right. Also I'm getting a Collection of Cities, but I need one of Users.
Then I tired it (temporary) like this
$users = User::with(['cities' => function ($query) use($cityId) {
/** #var \Illuminate\Database\Eloquent\Relations\BelongsToMany $query */
$query->wherePivot('city_id', $cityId);
}])->get();
But, then I'm getting ALL users, instead of just the ones, which is even more confusing, because the documentation says
Sometimes you may wish to eager load a relationship, but also specify additional query constraints for the eager loading query. Here's an example:
$users = App\User::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%');
}])->get();
In this example, Eloquent will only eager load posts that if the post's title column contains the word first. Of course, you may call other query builder to further customize the eager loading operation:
But why doesn't it work then?
You can solve your fatal error this way:
$users = City::find(10)->with(['users.jobs', function($query) use ($jobId) {
$query->whereJobId($jobId);
});
}]);
But this will not filter the users for a specific job ID. For example we have two users with IDs 1 and #2. User 1 is related to job 10 while user 2 is not. The code above will give you both users for $jobId = 10, because the users query will be executed first without any filters for jobs and the jobs query will be executed after the users query.
You have to use the join() method of the query builder:
$users = User::join('job_user', 'users.id', '=', 'job_user.user_id')
->join('city_user', 'users.id', '=', 'city_user.user_id')
->where('job_user.job_id', $jobId)
->where('city_user.city_id', $cityId)
->get();

Two models into one paginated query with eager loaded relations using Laravel

I have two Models that I would like to merge into one timeline. I have been able to do this by creating a View in mysql that normalizes and unions the tables. I created a Model for this view, NewsFeed. This works well if I do not want related Comment model. I have gotten close to this by overriding the getMorphClass method on the model. This allows me to get the related comments for the pictures, but not the posts, because when getMorphClass is called the model doesn't have any data.
I am open to any approach on how to solve this, not just the way I am proposing, but I don't want to pull more data than I have to from the database.
NewsFeed
<?php
namespace App\Users;
use App\Pictures\Picture;
use App\Social\Comments\CommentableTrait;
use App\Posts\Post;
use App\Users\User;
use Illuminate\Database\Eloquent\Model;
class UserFeed extends Model
{
use CommentableTrait;
public function user()
{
return $this->belongsTo(User::class);
}
public function getMorphClass(){
if ($this->type == 'post'){
return Post::class;
}
return Picture::class;
}
}
MySQL View
CREATE VIEW
`user_feeds`
AS SELECT
`posts`.`id` AS `id`,
`posts`.`user_id` AS `user_id`,
'post' AS `type`,
NULL AS `name`,
NULL AS `thumbnail`,
`posts`.`body` AS `body`,
`posts`.`updated_at` AS `updated_at`,
`posts`.`created_at` AS `created_at`
FROM
`posts`
UNION SELECT
`pictures`.`id` AS `id`,
`pictures`.`user_id` AS `user_id`,
'picture' AS `type`,
`pictures`.`name` AS `name`,
`pictures`.`thumbnail` AS `thumbnail`,
`pictures`.`description` AS `body`,
`pictures`.`updated_at` AS `updated_at`,
`pictures`.`created_at` AS `created_at`
FROM
`pictures`;
pictures table
id
user_id
title
img
img_width
img_height
img_other
description
created_at
updated_at
posts
id
user_id
title
body
created_at
updated_at
You are really close with your idea to build a view. In fact, if you create an actual table instead of a view, the solution becomes quite simple.
With a 'FeedItem' polymorph object that points to your Post class or Picture class, you can attach the comments directly to the FeedItem with a hasMany relationship.
class FeedItem extends Model {
use CommentableTrait;
public function feedable()
{
return $this->morphTo();
}
}
class Post extends Model {
public function feeditem()
{
return $this->morphOne('FeedItem', 'feedable');
}
}
class Picture extends Model {
public function feeditem()
{
return $this->morphOne('FeedItem', 'feedable');
}
}
This solution may require some refactoring on your forms since you will need to create a FeedItem entry for each Post entry and Picture entry. Event listeners for Picture::created and Post::created should do the trick (http://laravel.com/docs/5.1/eloquent#events).
Once it's set up, you can use:
FeedItem::with('comments')->orderBy('created_at','desc')->paginate(15);
Although I am not familiar with Laravel nor Eloquent for that matter, Here's my input on this.
Assuming you're getting the output from that SQL view into $Entries
As I understand Eloquent allows you to set the values for yourself, therefore something like this might work for you (I am not sure about the syntax or usage for that matter).
$Collection = [];
foreach( $Entries as $Entry ) {
if( $Entry->type === 'post' ) {
$Collection[] = new Post($Entry->toArray())->with('comments');
}else{
$Collection[] = new Picture($Entry->toArray())->with('comments');
}
}
return $Collection;

Duplicated query with uncapitalized Laravel relationships

I'm experiencing a strange behavior with Laravel 5 and a simple relationship query:
I have an Issues MySQL table and another Articles table. Every Issue can have one or more articles, so it's a plain OneToMany relationship.
Issue.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Issue extends Model
{
protected $fillable = ['status', 'volume', 'number', 'date'];
public function articles()
{
return $this->hasMany('App\Article');
}
}
Article.php
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
protected $fillable = ['status', 'authors', 'bibliography', 'references', 'notes', 'topic', 'issue_id'];
protected $with = ['Articles_contents'];
public function Articles_contents()
{
return $this->hasMany('App\Articles_content');
}
public function issue()
{
return $this->belongsTo('App\Issue');
}
}
(Articles have an another relationship with Articles_contents, but I don't think that this can have connections with my problem).
When I edit an issue, I want to list all the articles that are inside it. The articles have to be sorted by a numeric field into Articles table, named "sort":
public function edit($id)
{
$issue = Issue::with(array(
'Articles' => function ($query) {
$query->orderBy('sort', 'asc');
}
))->find($id);
$this->data['issue'] = $issue;
return view('admin_issues_edit', $this->data);
}
My problem is in the view: When I do this, to list the articles:
#foreach( $issue->Articles as $article )
<li id="article_{{ $article->id }}">{!! link_to_route('admin.articles.edit', $article->Articles_contents->first()->title, array($article->id)) !!}</li>
#endforeach
... I get the right, minimal, queries:
select * from issues where issues.id = '8' limit 1
select * from articles where articles.issue_id in ('8') order by sort asc
select * from articles_contents where articles_contents.article_id in ('20', '14', '5')
But if I do the same thing, with lowercase ->articles, like the documentation suggests:
#foreach( $issue->articles as $article )
<li id="article_{{ $article->id }}">{!! link_to_route('admin.articles.edit', $article->Articles_contents->first()->title, array($article->id)) !!}</li>
#endforeach
I get a duplicated query, with a is not null statement:
select * from issues where issues.id = '8' limit 1
select * from articles where articles.issue_id in ('8') order by sort asc
select * from articles_contents where articles_contents.article_id in ('20', '14', '5')
select * from articles where articles.issue_id = '8' and articles.issue_id is not null
select * from articles_contents where articles_contents.article_id in ('5', '14', '20')
Of course the two queries to articles_contents are normal because I did the automatic eager loading in the model, but I'm getting two queries to the Articles table: the first with the correct sorting I'm requesting, and the second one with a strange issue_id is not null part.
What's wrong? :)
It's because, when do this:
#foreach( $issue->Articles as $article )
<li id="article_{{ $article->id }}">{!! link_to_route('admin.articles.edit', $article->Articles_contents->first()->title, array($article->id)) !!}</li>
#endforeach
You loop the already (egerly loaded) available Articles because you have used:
array(
'Articles' => function ($query) {
$query->orderBy('sort', 'asc');
}
)
So Articles is present but when you use articles it just loads the articles right then because there is no articles key available in the relations array. So that is another query gets executed at the run time and this is dynamic behavior of Eloquent relationships and yes is not null is used by Laravel when it executes the query.

Categories