Laravel OrderBy relationship count - php

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)

Related

Laravel 5.8 - How sort (orderBy) multiple relationship

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.

Laravel get only first related model

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;
});

Copying Data from table to another table and applying one to many relationship

This is my Report Model
protected $fillable = [
'site_url',
'reciepients',
'monthly_email_date'
];
public function site()
{
return $this->belongsTo('App\Site');
}
This is my Site Model
public function report()
{
return $this->hasMany('App\Report');
}
This is my ReportController
public function showSpecificSite($site_name)
{
$records = DB::table('reports')
->select('email_date','url','recipient')
->whereHas('sites', function($query){
$query->where('site_name',$site_name);
})
->get();
return view('newsite')->with('records',$records)
->with('site_name',$site_name);
}
My Controller is not yet working as well.
The thing is I would like to copy all the three files from sites table to reports table.
Is it possible in insertInto ?
My code on ReportController shows you that I'm selecting data from reports table but I am the one who puts data to reports table to see the output but it is not yet working because of the it cant reach out the value of site_name even though I already put a relationship between the two tables.
You're not actually using Eloquent in your controller you're just using the Query Builder (DB). This will mean that you don't have access to anything from your Eloquent models.
Try:
$records = \App\Report::whereHas('site', function($query) use($site_name) {
$query->where('site_name', $site_name);
})->get(['id', 'email_date', 'url', 'recipient']);
I've added id to the list of columns as I'm pretty sure you'll need that to use whereHas.
NB to use a variable from the parent scope inside a closure you need to pass it in using use().

use relationship in model accessor in laravel

Suppose I have a Course model like this :
class Course extends Model
{
public $primaryKey = 'course_id';
protected $appends = ['teacher_name'];
public function getTeacherNameAttribute ()
{
$this->attributes['teacher_name'] = $this->teacher()->first()->full_name;
}
public function teacher ()
{
return $this->belongsTo('App\User', 'teacher', 'user_id');
}
}
And in the other hand there is a User model like this :
class User extends Authenticatable
{
public $primaryKey = 'user_id';
protected $appends = ['full_name'];
public function getFullNameAttribute ()
{
return $this->name . ' ' . $this->family;
}
public function course ()
{
return $this->hasMany('App\Course', 'teacher', 'user_id');
}
}
As you can see there is a hasMany relationship between those.
There is an full_name accessor in User model.
Now I want to add a teacher_name accessor to Course model that uses it's teacher relations and gets full_name of teacher and appends to Course always.
In fact I want whenever call a Course model, it's related teacher name included like other properties.
But every time , when call a Course model , I got this error :
exception 'ErrorException' with message 'Trying to get property of non-object' in D:\wamp\www\lms-api\app\Course.php:166
That refers to this line of Course model :
$this->attributes['teacher_name'] = $this->teacher()->first()->full_name;
I do not know how can I solve that and what is problem exactly.
Yikes some interesting answers here.
FYI to those coming after me- getFooAttribute() should return the data, and not modify the internal attributes array.
If you set a new value in the attributes array (that doesnt exist in this model's db schema) and then attempt to save the model, you'll hit a query exception.
It's worth reading up the laravel docs on attribute accessors/mutators for more info.
Furthermore, if you need to access a related object from within the model (like in an accessor) you ought to call $related = $this->getRelation('foo'); - note that if the relation isnt loaded (e.g., if you didnt fetch this object/collection with eager loaded relations) then $this->getRelation() could return null, but crucially if it is loaded, it won't run the same query(ies) to fetch the data again. So couple that with if (!$this->relationLoaded('foo')) { $this->loadRelation('foo'); }. You can then interact with the related object/collection as normal.
$this->attributes['teacher_name'] = $this->teacher()->first()->full_name;
Should be
$this->attributes['teacher_name'] = $this->teacher->full_name;
First thing is that you want to reference the relationship, so loose the brackets (), and because the relationship is belongsTo, you will have one user / teacher returned. So you don't need the first().
We haven't seen your fields but probably you will have to change:
return $this->belongsTo('App\User', 'teacher', 'user_id');
to
return $this->belongsTo('App\User', 'foreign_key', 'other_key');
where foreign_key and other_key are the primary keys that you need to make the join on.
Check this link from the documentation for reference:
https://laravel.com/docs/5.4/eloquent-relationships#one-to-many-inverse
the right way to do this is:
COURSE
public function setTeacherNameAttribute ()
{
$this->attributes['teacher_name'] = $this->teacher->full_name;
}
100% working for me.
I have one to one relationship between Order and Shipment. I have to add the accessor of shipments table column from orders table.
function getOrderNoAttribute()
{
$appendText = "OR100";
if($this->orderShipment()->first()) {
$appendText = $this->orderShipment()->first()->is_shipping === 1 ? "ORE100" : "OR100";
}
return $appendText . $this->attributes['id'];
}
This error is only object data to array use or array data to object data use.
example::
$var->feild insted of $var[feild]
$var[feild] insted of $var->feild
You should use return for accessors . something like this :
public function getTeacherNameAttribute ()
{
return $this->teacher()->first()->full_name ?? '';
}
maybe a course hasn't teacher.

Laravel 5 hasMany relationship on two 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();

Categories