Using the polymorphic relationship likeable, I have setup authors and books as likeable_type in likeable_items table.
Here are the models:
class Like extends Model {
public function likeable(){
return $this->morphTo();
}
}
class Author extends Model {
public function likes(){
return $this->morphMany('App\Like', 'likeable');
}
}
class Book extends Model {
public function likes(){
return $this->morphMany('App\Like', 'likeable');
}
}
I want to use one efficient query to pull them both in with their respective data, paginated by 10, something like this does not work (I commented the code to show what is needed in each step).
$likeableData =
DB::table('likeable_items')
// We want to fetch additional data depending on likeable_type
->select(['books.title', 'books.author_name', 'book_counts.like_count']) // when likeable_type = 'book'
->select(['authors.name', 'authors.country', 'authors.age', 'author_counts.like_count']) // when likeable_type = 'author'
->leftJoin('books', 'books.id', '=', 'likeable_items.likeable_id') // when likeable_type = 'book'
->leftJoin('book_counts', 'book_counts.book_id', '=', 'likeable_items.likeable_id') // when likeable_type = 'book'
->leftJoin('author_counts', 'author_counts.author_id', '=', 'likeable_items.likeable_id') // when likeable_type = 'author'
// We want to have distinct results, based on unique id of book/author
->distinct()
// We want to order by the highest like_count, regardlress of likeable_type
->orderBy('book_counts.like_count', 'desc') // order by highest like_count when likeable_type = 'book'
->orderBy('author_counts.like_count', 'desc') // order by highest like_count when likeable_type = 'author_counts'
// We want to paginate the mixed results
->paginate(10);
return $likeableData;
How can I get the mixed results back of the highest liked author/book by likes_count, with their respective data, paginated by 10?
UPDATE:
Here is the table schema:
Table: likeable_items
- id
- likeable_id (book_id or author_id)
- likeable_type (book or author)
Table: books
- id
- title
- author_name
Table: book_counts
- book_id
- like_count
Table: authors
- id
- name
- country
- age
Table: author_counts
- author_id
- like_count
You could do it from your Like model without the need for the counts tables with some sub selects and grouping like:
$likes = Like::select('*')
->selectSub('COUNT(*)', 'total_likes')
->with('likeable')
->whereIn('likeable_type', [Book::class, Author::class])
->groupBy('likeable_type', 'likeable_id')
->orderBy('total_likes', 'desc')
->paginate(10);
Then to access the values:
foreach($likes as $likedItem) {
$likedItem->total_likes;
if($likedItem->likeable_type === Book::class) {
// Your book logic here
$likedItem->likeable->title;
$likedItem->likeable->author_name;
} else {
// Your author logic here
$likedItem->likeable->name;
$likedItem->likeable->country;
$likedItem->likeable->age;
}
}
If you are willing to change the schema and have more than one query a possible solution would be:
Update Schema:
Table: likeable_items
- id
- likeable_id (book_id or author_id)
- likeable_type (book or author)
- like_count
Table: books
- id
- title
- author_name
Table: authors
- id
- name
- country
- age
Update Model:
class Like extends Model
{
/**
* #return bool
*/
public function isBook()
{
return ($this->likeable_type === Book::class);
}
/**
* #return bool
*/
public function isAuthor()
{
return ($this->likeable_type === Author::class);
}
}
Except you need book_counts and author_counts for a special piece of logic on your system, I would just remove them and move like_count to likeable_items.
like_count is a property common to books and authors so I think that it's reasonable to place it on likeable_items.
Querires:
$likes = Like::orderBy('like_count', 'desc')
->paginate(10);
$likeable_id = $likes->pluck('likeable_id');
$books = Book::whereIn('id', $likeable_id)->get()->groupBy('id');
$authors = Author::whereIn('id', $likeable_id)->get()->groupBy('id');
So, at this point get the best 10 is trivial.
To fetch the data instead we use pluck and then groupBy with whereIn.
About this point, please consider that if you are using likeable_items to dispatch the id for books and authors the two whereIn will return just the data you need.
Instead, if you use likeable_items just to cache books and authors ids, you will potentially have some data you don't need.
How to display the content:
#forelse ($likes as $item)
#if ($item->isBook())
$books[$item->likeable_id][0]->title;
$books[$item->likeable_id][0]->author_name;
#else
$authors[$item->likeable_id][0]->name;
$authors[$item->likeable_id][0]->country;
$authors[$item->likeable_id][0]->age;
#endif
#empty
#endforelse
Final Considerations:
Using this approach you will:
- change your schema and update your model
- you won't use morphTo,
- you do 3 queries instead of 1 but are not complex
- you can directly use paginate
- you always use id for the secondary queries (which is good for performance)
The only "dark side" as I said is that you may have some content you don't need in $books and $authors in case you are not dispatching the ids from likeable_items but I think is a reasonable trade-off.
Related
I have this database structure
table users table office_user table offices
----------- ----------------- -------------
id * id * id *
full_name user_id name
office_id
joined_at
So in my project every office has many users and user can be joined to many offices in date (joined_at)
User.php model
public function offices()
{
return $this->belongsToMany('App\Office)->withPivot('joined_at');
}
Office.php model
public function users()
{
return $this->belongsToMany('App\User)->withPivot('joined_at');
}
OfficeController.php
public function show(Office $office)
{
$users = User::with(array('phones', 'offices' , function($query)
{
$query->orderBy('joined_at', 'desc');
}))->get();
return view('dashboard.offices.show', compact(['office', 'users']));
}
I need two things :
1- Get current users list for every office
2- Count of current users in every office
I already achieve this:
<h3>{{ $office->name }}</h3><span>{{ $office->users->count() }}</span>
#foreach ($office->users as $user)
<li>{{ $user->full_name }}</li>
#endforeach
But the result is not as expected it gives me all users in certain office and count of them regardless there joined date
I want the list of last joined users to this office and count of them according joined_at field in pivot table
Thank you and Sorry for my english
But the result is not as expected it gives me all users in certain office and count of them regardless there joined date
When you do $office->users->count() that is the expected behavior because you are retrieve all the associated users of every office at any time, so given that you returned all this users, the count() executed in the collection will count all of them.
Your pivot attribute is just a timestamp, so how would you reduce the number of users returned? users that joined the office today/in the last hour/in the last 15 min maybe?
If so, you can add constrains to your count() method to get the results you want.
As an example, in the following lines we are gonna constraint the associated offices that has a joined_at that belongs to today:
public function show(Office $office)
{
$users = User::with([
'phones',
'offices' => function ($offices) {
$offices->whereDate('joined_at', '>', now()->startOfDay());
},
])->get();
return view('dashboard.offices.show', compact([office, 'users']));
}
Check this section of the documentation:
Constraining Eager Loads
Sometimes you may wish to eager load a relationship, but also specify
additional query conditions 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 where the post's
title column contains the word first. You may call other query
builder methods to further customize the eager loading operation:
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc');
}])->get();
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');
I have two tables, say "users" and "users_actions", where "users_actions" has an hasMany relation with users:
users
id | name | surname | email...
actions
id | id_action | id_user | log | created_at
Model Users.php
class Users {
public function action()
{
return $this->hasMany('Action', 'user_id')->orderBy('created_at', 'desc');
}
}
Now, I want to retrieve a list of all users with their LAST action.
I saw that doing Users::with('action')->get();
can easily give me the last action by simply fetching only the first result of the relation:
foreach ($users as $user) {
echo $user->action[0]->description;
}
but I wanted to avoid this of course, and just pick ONLY THE LAST action for EACH user.
I tried using a constraint, like
Users::with(['action' => function ($query) {
$query->orderBy('created_at', 'desc')
->limit(1);
}])
->get();
but that gives me an incorrect result since Laravel executes this query:
SELECT * FROM users_actions WHERE user_id IN (1,2,3,4,5)
ORDER BY created_at
LIMIT 1
which is of course wrong. Is there any possibility to get this without executing a query for each record using Eloquent?
Am I making some obvious mistake I'm not seeing? I'm quite new to using Eloquent and sometimes relationship troubles me.
Edit:
A part from the representational purpose, I also need this feature for searching inside a relation, say for example I want to search users where LAST ACTION = 'something'
I tried using
$actions->whereHas('action', function($query) {
$query->where('id_action', 1);
});
but this gives me ALL the users which had had an action = 1, and since it's a log everyone passed that step.
Edit 2:
Thanks to #berkayk looks like I solved the first part of my problem, but still I can't search within the relation.
Actions::whereHas('latestAction', function($query) {
$query->where('id_action', 1);
});
still doesn't perform the right query, it generates something like:
select * from `users` where
(select count(*)
from `users_action`
where `users_action`.`user_id` = `users`.`id`
and `id_action` in ('1')
) >= 1
order by `created_at` desc
I need to get the record where the latest action is 1
I think the solution you are asking for is explained here http://softonsofa.com/tweaking-eloquent-relations-how-to-get-latest-related-model/
Define this relation in User model,
public function latestAction()
{
return $this->hasOne('Action')->latest();
}
And get the results with
User::with('latestAction')->get();
I created a package for this: https://github.com/staudenmeir/eloquent-eager-limit
Use the HasEagerLimit trait in both the parent and the related model.
class User extends Model {
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}
class Action extends Model {
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
}
Then simply chain ->limit(1) call in your eager-load query (which seems you already do), and you will get the latest action per user.
My solution linked by #berbayk is cool if you want to easily get latest hasMany related model.
However, it couldn't solve the other part of what you're asking for, since querying this relation with where clause would result in pretty much the same what you already experienced - all rows would be returned, only latest wouldn't be latest in fact (but latest matching the where constraint).
So here you go:
the easy way - get all and filter collection:
User::has('actions')->with('latestAction')->get()->filter(function ($user) {
return $user->latestAction->id_action == 1;
});
or the hard way - do it in sql (assuming MySQL):
User::whereHas('actions', function ($q) {
// where id = (..subquery..)
$q->where('id', function ($q) {
$q->from('actions as sub')
->selectRaw('max(id)')
->whereRaw('actions.user_id = sub.user_id');
})->where('id_action', 1);
})->with('latestAction')->get();
Choose one of these solutions by comparing performance - the first will return all rows and filter possibly big collection.
The latter will run subquery (whereHas) with nested subquery (where('id', function () {..}), so both ways might be potentially slow on big table.
Let change a bit the #berkayk's code.
Define this relation in Users model,
public function latestAction()
{
return $this->hasOne('Action')->latest();
}
And
Users::with(['latestAction' => function ($query) {
$query->where('id_action', 1);
}])->get();
To load latest related data for each user you could get it using self join approach on actions table something like
select u.*, a.*
from users u
join actions a on u.id = a.user_id
left join actions a1 on a.user_id = a1.user_id
and a.created_at < a1.created_at
where a1.user_id is null
a.id_action = 1 // id_action filter on related latest record
To do it via query builder way you can write it as
DB::table('users as u')
->select('u.*', 'a.*')
->join('actions as a', 'u.id', '=', 'a.user_id')
->leftJoin('actions as a1', function ($join) {
$join->on('a.user_id', '=', 'a1.user_id')
->whereRaw(DB::raw('a.created_at < a1.created_at'));
})
->whereNull('a1.user_id')
->where('aid_action', 1) // id_action filter on related latest record
->get();
To eager to the latest relation for a user you can define it as a hasOne relation on your model like
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
class User extends Model
{
public function latest_action()
{
return $this->hasOne(\App\Models\Action::class, 'user_id')
->leftJoin('actions as a1', function ($join) {
$join->on('actions.user_id', '=', 'a1.user_id')
->whereRaw(DB::raw('actions.created_at < a1.created_at'));
})->whereNull('a1.user_id')
->select('actions.*');
}
}
There is no need for dependent sub query just apply regular filter inside whereHas
User::with('latest_action')
->whereHas('latest_action', function ($query) {
$query->where('id_action', 1);
})
->get();
Migrating Raw SQL to Eloquent
Laravel Eloquent select all rows with max created_at
Laravel - Get the last entry of each UID type
Laravel Eloquent group by most recent record
Laravel Uses take() function not Limit
Try the below Code i hope it's working fine for u
Users::with(['action' => function ($query) {
$query->orderBy('created_at', 'desc')->take(1);
}])->get();
or simply add a take method to your relationship like below
return $this->hasMany('Action', 'user_id')->orderBy('created_at', 'desc')->take(1);
How can I fetch data with belongsToMany relation models.
Table tags
- id
- name
...
Table photo_entries
- id
...
Table photo_entry_tag
- id
- tag_id
- photo_entry_id
Tag.php
public function photo_entry()
{
return $this->belongsToMany('PhotoEntry');
}
PhotoEntry.php
public function tags()
{
return $this->belongsToMany('Tag');
}
Now I need to fetch photo entries from photo_entries table with their tags where tag_id=1.
I have tried this:
$photos = PhotoEntry::orderBy('votes', 'desc')->with(array('tags' => function($query)
{
$query->where('tag_id', 1);
}))->paginate(50);
But its not giving me the proper result, its returning all photos. I am not getting what is the issue.
Can any one please help me.
You need to select all records form photo_entries table that has a relation in photo_entry_tag table so to do that your query should be like this
$tag_id = 1;
$query = "SELECT * FROM `photo_entries` WHERE `id` IN (SELECT `photo_entry_id` FROM `photo_entry_tag` WHERE `tag_id` = {$tag_id})";
Also the equivalence code in eloquent will be like the code below
$tag_id = 1;
$photos = PhotoEntry::whereIn('id', function($query) use ($tag_id) {
$query->select('photo_entry_id')->from('photo_entry_tag')->whereTagId($tag_id
);
})->get();
Also you can add orderBy('votes', 'desc') just before get() to sort the result
I have 3 Models... Category, Post, Vote
When viewing a category, I am showing an index of all Posts in that category. So I'm doing something like foreach ($category->posts as $post) in my view.
The question I have now is how can I order the posts based on the sum of votes they have?
I have standard relationships setup, so that a post hasMany votes.
You can do it either by defining a helper relation on the Post model and sorting the collection after the relation is loaded OR simply by joining the votes and ordering in the query.
1 RELATION
// Post model
public function votesSum()
{
return $this->hasOne('Vote')->selectRaw('post_id, sum(votes) as aggregate')->groupBy('post_id');
}
// then
$category->posts->load('votesSum'); // load relation on the collection
$category->posts->sortByDesc(function ($post) {
return $post->votesSum->aggregate;
});
// access votes sum like this:
$category->posts->first()->votesSum->aggregate;
2 JOIN
$category->load(['posts' => function ($q) {
$q->leftJoin('votes', 'votes.post_id', '=', 'posts.id')
->selectRaw('posts.*, sum(votes.votes) as votesSum')
->groupBy('posts.id')
->orderBy('votesSum', 'desc');
}]);
// then access votes sum:
$category->posts->first()->votesSum;
You can use scope for that:
// Post model
public function scopeOrderByVotes($query)
{
$query->leftJoin('comments','comments.post_id','=','posts.id')
->selectRaw('posts.*,sum(comments.id) as commentsSum')
->groupBy('posts.id')
->orderBy('commentsSum','desc');
}
// then
$category = Category::with(['posts' => function ($q) {
$q->orderByVotes();
}])->whereSlug($slug)->firstOrFail();