Duplicated query with uncapitalized Laravel relationships - php

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.

Related

I'm trying to load only the last 3 comments on every post

i want get all posts with last three comment on each post. my relation is
public function comments()
{
return $this->hasMany('App\Commentpostfeed','post_id')->take(3);
}
This would return only 3 comments total whenever I called it instead of 3 comments per post.
i use this way :
1 :
Postfeed::with(['comment' => function($query) {
$query->orderBy('created_at', 'desc')->take(3); }]);
2 :
$postings = Postfeed::with('comments')->get();
but getting same result. please help me out for this problem.
Can you try like that ?;
Postfeed::with('comment')->orderBy('id','desc')->take(3);
Using plain mysql (If using Mysql) query you can get 3 recent comments per post using following query which rejoins comment table by matching created_at
SELECT p.*,c.*
FROM posts p
JOIN comments c ON p.`id` = c.`post_id`
LEFT JOIN comments c1 ON c.`post_id` = c1.`post_id` AND c.`created_at` <= c1.`created_at`
GROUP BY p.`id`,c.`id`
HAVING COUNT(*) <=3
ORDER BY p.`id`,c.`created_at` DESC
Sample Demo
Using laravel's query builder you can write similar to
$posts = DB::table('posts as p')
->select('p.*,c.*')
->join('comments c', 'p.id', '=', 'c.post_id')
->leftJoin('comments as c1', function ($join) {
$join->on('c.post_id', '=', 'c1.post_id')->where('c.created_at', '<=', 'c1.created_at');
})
->groupBy('p.id')
->groupBy('c.id')
->having('COUNT(*)', '<=', 3)
->orderBy('p.id', 'asc')
->orderBy('c.created_at', 'desc')
->get();
You can create a scope in the BaseModel like this :
<?php
class BaseModel extends \Eloquent {
/**
* query scope nPerGroup
*
* #return void
*/
public function scopeNPerGroup($query, $group, $n = 10)
{
// queried table
$table = ($this->getTable());
// initialize MySQL variables inline
$query->from( DB::raw("(SELECT #rank:=0, #group:=0) as vars, {$table}") );
// if no columns already selected, let's select *
if ( ! $query->getQuery()->columns)
{
$query->select("{$table}.*");
}
// make sure column aliases are unique
$groupAlias = 'group_'.md5(time());
$rankAlias = 'rank_'.md5(time());
// apply mysql variables
$query->addSelect(DB::raw(
"#rank := IF(#group = {$group}, #rank+1, 1) as {$rankAlias}, #group := {$group} as {$groupAlias}"
));
// make sure first order clause is the group order
$query->getQuery()->orders = (array) $query->getQuery()->orders;
array_unshift($query->getQuery()->orders, ['column' => $group, 'direction' => 'asc']);
// prepare subquery
$subQuery = $query->toSql();
// prepare new main base Query\Builder
$newBase = $this->newQuery()
->from(DB::raw("({$subQuery}) as {$table}"))
->mergeBindings($query->getQuery())
->where($rankAlias, '<=', $n)
->getQuery();
// replace underlying builder to get rid of previous clauses
$query->setQuery($newBase);
}
}
And in the Postfeed Model :
<?php
class Postfeed extends BaseModel {
/**
* Get latest 3 comments from hasMany relation.
*
* #return Illuminate\Database\Eloquent\Relations\HasMany
*/
public function latestComments()
{
return $this->comments()->latest()->nPerGroup('post_id', 3);
}
/**
* Postfeed has many Commentpostfeeds
*
* #return Illuminate\Database\Eloquent\Relations\HasMany
*/
public function comments()
{
return $this->hasMany('App\Commentpostfeed','post_id');
}
}
And to get the posts with the latest comments :
$posts = Postfeed::with('latestComments')->get();
Ps :
Source
For many to many relationships
You can do it like this,
Postfeed::with('comments',function($query){
$query->orderBy('created_at', 'desc')->take(3);
})
->get();

Need to 'convert' an MySQL-query to Eloquent; where to start / how to think?

I'd like to 'convert' a raw SQL-query to Eloquent, so I can have eager loaded models attached too, so I don't have to edit some templates I got. Problem is, the query got some subqueries and I do not know how to 'convert' the query into Eloquent's format. The query in question is:
SELECT
e_eh.id,
s.name as serie,
s.id as serie_id,
e_eh.season,
e_eh.episode,
e_eh.name,
eh1.prog_trans,
eh1.prog_check,
eh1.prog_sync,
eh1.avi
FROM (
SELECT
e.*
, (
SELECT
eh.id
FROM episode_histories AS eh
WHERE 1
AND eh.episode_id = e.id
ORDER BY
eh.id DESC
LIMIT 1
) AS eh_id
FROM episodes AS e
WHERE 1
AND e.completed = 0
AND e.deleted_at IS NULL
) AS e_eh
INNER JOIN episode_histories AS eh1 ON e_eh.eh_id = eh1.id
INNER JOIN series as s ON s.id = e_eh.serie_id
ORDER BY prog_trans DESC, prog_check DESC, prog_sync DESC
I've tried a few things already, but none have worked. I'm a bit stuck in how to "think" this into Laravel / Eloquent. Documentation from Laravel itself is also not much helpful.
In a nutshell:
I've got two models, one is episodes, other is episode_histories, whichs stores some history on related episode. A third model is the show model, the related show for it. I need to get an episode, with related show model (is a relation in my model already). but I also need to get the latest episode_histories model for given episode.
What I currently have in my models:
Episode:
`class Episode extends Model
{
use SoftDeletes;
use App\History; // The history model
protected $table = 'episodes';
protected $primaryKey = 'id';
public $timestamps = true;
/**
* The attributes that should be mutated to dates.
*
* #var array
*/
protected $dates = ['deleted_at'];
/* Eloquent relations */
public function show() {
return $this->belongsTo('App\Serie', 'serie_id', 'id');
}
public function history() {
return $this->hasMany('App\History', 'episode_id', 'id')->orderBy('id', 'desc');
}
public static function getEpisodes2() {
return DB::select();
}
}
And my history model looks like this:
class History extends Model
{
use SoftDeletes;
protected $table = 'episode_histories';
protected $primaryKey = 'id';
public $timestamps = true;
/**
* The attributes that should be mutated to dates.
*
* #var array
*/
protected $dates = ['deleted_at'];
/* Eloquent relations */
public function episode() {
return $this->belongsTo('App\Episode');
}
public function user() {
return $this->belongsTo('App\User', 'user_id', 'id');
}
/* Custom functions */
}`
I hope someone can help me out on this. In the event of missing info, please let me know, so I can add that.
If you want to do this query types I would recommend you to use the Laravel Query Builder. Take a look at the documentation
The best advice when for dealing with complex queries is to keep it raw. Other than that, you have models for the helping hand because when a change comes to this query, there is a lot of editing and reshuffling again to blend it to a perfect state.
DB:statement("your query")
If you have procedures, then:
DB::statement('CALL PROCEDURE_NAME(:id,#name,#email);',array($id);
$result = DB:select('select #name as alias_name, #email as alias_name');

How do you order a Laravel Builder by its one-to-one relationship?

I have two models, Book and Author. Book belongsTo Author (I've trimmed some of the crud from other parts):
class Book extends Model {
protected $table = 'books';
protected $fillable = ['title'];
public function author(){
return $this->belongsTo('Author', 'author');
}
}
class Author extends Model {
protected $table = 'authors';
protected $fillable = ['name'];
}
If I want to get all books ordered by their title, I would use this:
Books::with(['author'])->orderBy('title', 'asc')->get();
Is it possible to order those books by the author's name? I've tried many combinations:
Books::with(['author' => function($query){
$query->orderBy('name', 'asc');
}])->get();
Books::with(['author'])->orderBy('name')->get();
Books::with(['author'])->orderBy('author.name')->get();
Books::with(['author'])->orderBy('authors.name')->get();
But none worked. The first, using the with() query, orders all authors before joining them into the books collection, I think. The others threw 500 errors.
If this were plain MySQL, I'd write something like this:
select * from books join authors on books.author_id = authors.id order by authors.name asc;
Is this possible using Builder/Eloquent in laravel 5.1? Do I need to use a DB query?
Enabling eager load by calling with('author') will load authors in a separate query, that's why author's columns are not available and sorting by them does not work. Explicit join is needed.
In order to sort by a relation you need to join your books with their authors:
Book::select('books.*')
->join('authors', 'authors.id', '=', 'books.author_id')
->orderBy('authors.name')
->get();
eloquent return a collection its very powerful see the docs for things you can do with that
so to order the books by author name you can do:
function orderBooksByAutherName($books){
return $books->sortBy(function ($book, $key) {
return $book->author->name;
});
}
if you really want to use join the only option is to use the query builder

Laravel - sync with extra column

I have 3 tables
type
type_id
person
person_id
category
category_id
table_name
table_id
person_id
In category I have connections of different tables/models with Type model, So if I have want to get type_id connected with person with person_id = 23 the query should look like this:
SELECT * FROM category WHERE table_name='person' AND table_id = 23
In my Person model I defined relationship with Type this way:
public function groups()
{
return $this->belongsToMany('Type', 'category',
'table_id', 'type_id')->wherePivot( 'table_name', '=', 'person' );
}
When I want to get those types and I use:
$person->groups()->get()
The query looks like this:
select `type`.*, `category`.`table_id` as `pivot_table_id`, `category`.`type_id` as `pivot_type_id` from `type` inner join `category` on `type`.`type_id` = `category`.`type_id` where `category`.`table_id` = '23' and `category`.`table_name` = 'person';
so it seems to be correct.
But I would like to use sync() for synchronizing types with persons and here's the problem.
When I use:
$person->groups()->sync('1' => ['table_name' => 'person']);
I see the query that gets all records from category to use for sync looks like this:
select `type_id` from `category` where `table_id` = '23';
so it doesn't use
`category`.`table_name` = 'person'
condition so synchronization won't work as expected.
Is there any simple way to solve it or should I synchronize it manually?
You should use Eloquents polymorphic relations (http://laravel.com/docs/4.2/eloquent#relationships)
class Category extends Eloquent {
public function categorizable()
{
return $this->morphTo();
}
}
class Person extends Eloquent {
public function categories()
{
return $this->morphMany('Category', 'categorizable');
}
}
Now we can retrieve catgories from person:
$person = Person::find(1);
foreach ($person->categories as $category)
{
//
}
and access person or other owner from category:
$category = Category::find(1);
$categorizable_model = $category->categorizable; //e.g. Person
I can confirm that it was a bug in Laravel 5 commit I used. I've upgraded for Laravel 5 final version and now query is generated as it should.

Laravel relationship problems

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.

Categories