I am trying to sort the serials by video views.
Relations:
The Serial has a hasMany relationship to series.
The Series has a hasMany relationship to episodes.
The Episodes has a hasOne relationship to video.
The Video has a hasMany relationship to viewcounts.
<?php
//sort method:
public function mostPopular()
{
$serials = Serial::with(['series.episodes.video' => function ($query) {
$query->withCount(['videoViews' => function($query) {
}])->orderBy('video_views_count', 'desc');
}])->get();
return $serials;
}
//Serial model:
public function series()
{
return $this->hasMany(Series::class);
}
//Series model:
public function episodes()
{
return $this->hasMany(Episode::class);
}
public function serial()
{
return $this->belongsTo(Serial::class);
}
//Episode model:
public function video()
{
return $this->hasOne(Video::class);
}
public function series()
{
return $this->belongsTo(Series::class);
}
//Video model:
public function videoViews()
{
return $this->hasMany(VideoView::class);
}
public function episode()
{
return $this->belongsTo(Episode::class);
}
?>
I expect the sorted serials by video views (series.episodes.video.videoViews), but the actual output is not sorted.
Laravel 5.8
PHP 7
This is a silly one actually but I've learnt that multiple ->sortBy on collections actually are possible with no workarounds. It's just that you need to reverse the order of them. So, to sort a catalogue of artists with their album titles this would be the solution...
Instead of :
$collection->sortBy('artist')->sortBy('title');
Do this :
$collection->sortBy('title')->sortBy('artist');
Because "With" queries run as seperate queries (not subqueries as previously suggested), exposing extrapolated fuax-columns from one query to the other gets rather tricky. I'm sure there's non-documented solution in the API docs but I've never come across it. You could try putting your with and withCount in the orderBy:
Serial::orderBy(function($query) { some combo of with and withCount })
But that too will get tricky. Since either approach will hit the database multiple times, it would be just as performant to do the separation yourself and keep your sanity at the same time. This first query uses a left join, raw group by and raw select because I don't want laravel running the with query as a separate query (the problem in the first place).
$seriesWithViewCounts = VideoView::leftJoin('episodes', 'episodes.id', '=', 'video_views.episode_id')
->groupBy(DB::raw('episodes.series_id'))
->selectRaw("episodes.series_id, count(video_views.id) as views")
->get();
$series = Series::findMany($seriesWithViewCounts->pluck('series_id'));
foreach($series as $s) {
$s->view_count = $seriesWithViewCounts->first(function($value, $key) use ($s) {
return $value->series_id = $s->id
})->video_views_count;
});
$sortedSeries = $series->sortBy('video_views_count');
This will ignore any series that has no views for all episodes, so you may want to grab those and append it to the end. Not my definition of "popular".
I'd love to see a more eloquent way of handling this, but this would do the job.
Related
I want to get the first related model. But this works only for the first model in the collection. The 2nd is empty.
I've found this answer, but I didn't find a solution.
How can I only get the first related model?
$querybuilder->with([
'messages' => function ($query) {
$query->orderBy("created_at", "DESC");
$query->limit(1);
}
]);
You can use a HasOne relationship:
class Conversation extends Model {
public function latestMessage() {
return $this->hasOne(Message::class)->latest();
}
}
$querybuilder->with('latestMessage');
Be aware that this will still fetch all messages from the database. It then discards the "old" ones.
If you want to improve the performance by really only fetching the latest message, you can use this package I created: https://github.com/staudenmeir/eloquent-eager-limit
The package allows you to apply limit() to the relationship:
class Conversation extends Model {
use \Staudenmeir\EloquentEagerLimit\HasEagerLimit;
public function latestMessage() {
return $this->hasOne(Message::class)->latest()->limit(1);
}
}
Your with() actually creates several queries, the last query has the limit, hence the behavior (which is correct). you can use \DB::enableQueryLog();, run your query and then \DB::getQueryLog(); to see how the queries are built.
If you instead want to apply a limit to each model item you could fetch all items and iterate over them to fetch one or more related model items
This is not done i sql but in php (laravel collection method), if you need it in sql you could just join your related model and set it up however you want.
This will cause performance issues if you have large amount of data, but if you don't it's quite convenient.
$result = \App\YourModel::all()
->map(function ($item) {
return $item->YourRelatedModel()
->orderBy('someField')
->first();
});
I forgot ... the above only returns the related model's items, if you also want the parent model you can
$result = \App\YourModel::all()
->map(function ($item) {
$item->YourRelatedModelName = $item
->YourRelatedModel()
->orderBy('someField')
->first();
return $item;
});
I've got two models, User and Seminar. In English, the basic idea is that a bunch of users attend any number of seminars. Additionally, exactly one user may volunteer to speak at each of the seminars.
My implementation consists of a users table, a seminars table, and a seminar_user pivot table.
The seminar_user table has a structure like this:
seminar_id | user_id | speaking
-------------|-----------|---------
int | int | bool
The relationships are defined as follows:
/** On the Seminar model */
public function members()
{
return $this->belongsToMany(User::class);
}
/** On the User model */
public function seminars()
{
return $this->belongsToMany(Seminar::class);
}
I am struggling to figure out how to set up a "relationship" which will help me get a Seminar's speaker. I have currently defined a method like this:
public function speaker()
{
return $this->members()->where('speaking', true);
}
The reason I'd like this is because ultimately, I'd like my API call to look something like this:
public function index()
{
return Seminar::active()
->with(['speaker' => function ($query) {
$query->select('name');
}])
->get()
->toJson();
}
The problem is that since the members relationship is actually a belongsToMany, even though I know there is only to ever be a single User where speaking is true, an array of User's will always be returned.
One workaround would be to post-format the response before sending it off, by first setting a temp $seminars variable, then going through a foreach and setting each $seminar['speaker'] = $seminar['speaker'][0] but that really stinks and I feel like there should be a way to achieve this through Eloquent itself.
How can I flatten the data that is added via the with call? (Or rewrite my relationship methods)
Try changing your speaker function to this
public function speaker()
{
return $this->members()->where('speaking', true)->first();
}
This will always give you an Item as opposed to a Collection that you currently receive.
You can define a new relation on Seminar model as:
public function speaker()
{
return $this->belongsToMany(User::class)->wherePivot('speaking', true);
}
And your query will be as:
Seminar::active()
->with(['speaker' => function ($query) {
$query->select('name');
}])
->get()
->toJson();
Docs scroll down to Filtering Relationships Via Intermediate Table Columns
Is it possible to have a hasMany relationship on two columns?
My table has two columns, user_id and related_user_id.
I want my relation to match either of the columns.
In my model I have
public function userRelations()
{
return $this->hasMany('App\UserRelation');
}
Which runs the query: select * from user_relations where user_relations.user_id in ('17', '18').
The query I need to run is:
select * from user_relations where user_relations.user_id = 17 OR user_relations.related_user_id = 17
EDIT:
I'm using eager loading and I think this will affect how it will have to work.
$cause = Cause::with('donations.user.userRelations')->where('active', '=', 1)->first();
I don't think it's possible to do exactly what you are asking.
I think you should treat them as separate relationships and then create a new method on the model to retrieve a collection of both.
public function userRelations() {
return $this->hasMany('App\UserRelation');
}
public function relatedUserRelations() {
return $this->hasMany('App\UserRelation', 'related_user_id');
}
public function allUserRelations() {
return $this->userRelations->merge($this->relatedUserRelations);
}
This way you still get the benefit of eager loading and relationship caching on the model.
$cause = Cause::with('donations.user.userRelations',
'donations.user.relatedUserRelations')
->where('active', 1)->first();
$userRelations = $cause->donations[0]->user->allUserRelations();
Compoships adds support for multi-columns relationships in Laravel 5's Eloquent.
It allows you to specify relationships using the following syntax:
public function b()
{
return $this->hasMany('B', ['key1', 'key2'], ['key1', 'key2']);
}
where both columns have to match.
I'd prefer doing it this way:
public function userRelations()
{
return UserRelation::where(function($q) {
/**
* #var Builder $q
*/
$q->where('user_id',$this->id)
->orWhere('related_user_id',$this->id);
});
}
public function getUserRelationsAttribute()
{
return $this->userRelations()->get();
}
If anyone landed here like me due to google:
As neither merge() (as suggested above) nor push() (as suggested here) allow eager loading (and other nice relation features), the discussion is still ongoing and was continued in a more recent thread, see here: Laravel Eloquent Inner Join on Self Referencing Table
I proposed a solution there, any further ideas and contributions welcome.
You can handle that things with this smart and easy way .
$cause = Cause::with(['userRelations' => function($q) use($related_user_id) {
$q->where('related_user_id', $related_user_id);
}])->where('active', '=', 1)->first();
I'm trying to get the most popular hackathons which requires ordering by the respective hackathon's partipants->count(). Sorry if that's a little difficult to understand.
I have a database with the following format:
hackathons
id
name
...
hackathon_user
hackathon_id
user_id
users
id
name
The Hackathon model is:
class Hackathon extends \Eloquent {
protected $fillable = ['name', 'begins', 'ends', 'description'];
protected $table = 'hackathons';
public function owner()
{
return $this->belongsToMany('User', 'hackathon_owner');
}
public function participants()
{
return $this->belongsToMany('User');
}
public function type()
{
return $this->belongsToMany('Type');
}
}
And HackathonParticipant is defined as:
class HackathonParticipant extends \Eloquent {
protected $fillable = ['hackathon_id', 'user_id'];
protected $table = 'hackathon_user';
public function user()
{
return $this->belongsTo('User', 'user_id');
}
public function hackathon()
{
return $this->belongsTo('Hackathon', 'hackathon_id');
}
}
I've tried Hackathon::orderBy(HackathonParticipant::find($this->id)->count(), 'DESC')->take(5)->get()); but I feel like I made a big mistake (possibly the $this->id), because it doesn't work at all.
How would I go about trying to get the most popular hackathons which is based on the highest number of related hackathonParticipants?
This works for me in Laravel 5.3, using your example:
Hackathon::withCount('participants')->orderBy('participants_count', 'desc')->paginate(10);
This way it is ordered on the query and the pagination works nicely.
Edit: If using Laravel 5.2 or greater, use kJamesy's answer. It will likely perform a bit better because it's not going to need to load up all the participants and hackathons into memory, just the paginated hackathons and the count of participants for those hackathons.
You should be able to use the Collection's sortBy() and count() methods to do this fairly easily.
$hackathons = Hackathon::with('participants')->get()->sortBy(function($hackathon)
{
return $hackathon->participants->count();
});
Another approach can be by using withCount() method.
Hackathon::withCount('participants')
->orderBy('participants_count', 'desc')
->paginate(50);
Ref: https://laravel.com/docs/5.5/eloquent-relationships#querying-relations
I had similar issue and using sortBy() is not suitable because of pagination, exactly as Sabrina Gelbart commented in previous solution.
So I used db raw, here's simplified query:
Tag::select(
array(
'*',
DB::raw('(SELECT count(*) FROM link_tag WHERE tag_id = id) as count_links'))
)->with('links')->orderBy('count_links','desc')->paginate(5);
You can also use join operator. As Sabrina said, you can not use sortby at the db level.
$hackathons = Hackathon::leftJoin('hackathon_user','hackathon.id','=','hackathon_user.hackathon_id')
->selectRaw('hackathon.*, count(hackathon_user.hackathon_id) AS `count`')
->groupBy('hackathon.id')
->orderBy('count','DESC')
->paginate(5);
But this code takes all records from database. So you should paginate manually.
$hackathons = Hackathon::leftJoin('hackathon_user','hackathon.id','=','hackathon_user.hackathon_id')
->selectRaw('hackathon.*, count(hackathon_user.hackathon_id) AS `count`')
->groupBy('hackathon.id')
->orderBy('count','DESC')
->skip(0)->take(5)->get();
Referred from : https://stackoverflow.com/a/26384024/2186887
I needed to sum multiple counts and then use it to set order. Following query worked for me in Laravel 8.
$posts = Post::withCount('comments','likes')->orderBy(\DB::raw('comments_count + likes_count'),'DESC')->get();
You can use below code
Hackathon::withCount('participants')->orderByDesc("participants_count")->paginate(15)
Or if you even want ASC/DESC with single method
Hackathon::withCount('participants')->orderBy("participants_count", 'asc')->paginate(15)
I am working on a eloquent query to compile a newsletter but I have hit a brick wall.
What I'm trying to do is create a UI where the user can select a publication and date. Ideally it would then compile a list of that publication's categories (where stories > 0) and stories belonging to it.
Here are my 3 models:
Story
public function user()
{
return $this->belongsTo('User', 'user_id');
}
public function publication()
{
return $this->belongsTo('Workbench\Dailies\Publication', 'publication_id');
}
public function category()
{
return $this->belongsTo('Workbench\Dailies\Category', 'category_id');
}
Publication
public function story()
{
return $this->belongsTo('Workbench\Dailies\Story');
}
public function stories()
{
return $this->hasMany('Workbench\Dailies\Story', 'publication_id');
}
public function category()
{
return $this->belongsTo('Workbench\Dailies\Category', 'publication_id');
}
Category
public function story()
{
return $this->belongsTo('Workbench\Dailies\Story', 'category_id');
}
public function publications()
{
return $this->belongsTo('Workbench\Dailies\Publication', 'publication_id');
}
public function stories()
{
return $this->hasMany('Workbench\Dailies\Story', 'category_id');
}
Here is how my tables look:
Story
content
user_id
publication_id
category_id
publish_date
Publication
id
name
Category
id
name
publication_id
Here is what I currently have in my Repository.
public function compileStories($input)
{
return Category::has('stories', '>', 0)
->with('publications')
->whereHas('stories', function ($query) use ($input)
{
$query->where('publish_date', $input['publish_date']);
$query->where('publication_id', $input['publication_id']);
});
}
Am I headed in the right direction here or is there any way to improve the code above? It is not currently functioning as expected.
There are a couple of things I see here that may help straighten you out.
First - Some of the models have strange relationships without knowing more about your whole application. The Story model does not need the publication relationship since it can be retrieved through the category relationship, unless you have need of it otherwise. The Category model does not need both a story and a stories relationship, again, unless there's more to the story I don't know. In your example, you should only need the hasMany relationship. The Publication model only needs the categories relationship.
Now, after some cleanup of the models, let's look at your query. Using the category model to return your results seems completely appropriate for your desired results. You can check for the publication without having to dive into your stories, though. I haven't tested it, but you may not need the use $input line since $input is in the larger scope. You're also missing a conditional check in your where statments in the whereHas clause. The query should be able to be simplified as follows:
Category::where('publication_id', '=', $input['publication_id'])
->whereHas('stories', function($query)
{
$query->where('publish_date', '=', $input['publish_date']);
})
->get()