Sort by key in Eloquent - laravel

This is my Matrix model:
Matrix -> Finding -> Norm -> Normtype
(finding_id) (norm_id) (normtype_id)
I'd like to fetch Matrixes, sorted first by finding_id, then by normtype_id.
From what I've read, orderBy only applies to keys in the actual model, such as:
public function findings() {
return $this->hasMany('App\Finding')->orderBy('finding_id');
}
How can I do this in Eloquent?
Just to clarify, a matrix has many findings, and each finding belongs to a norm, and each norm has a normtype.

When you put the orderBy call on a relation definition, you only change how the related models are sorted. If you want to sort by related model's attributes, you'll need to build a proper join yourself.
The following should do the trick in your case:
Matrix::join('findings', 'finding_id', 'findings.id')
->join('norms', 'findings.norm_id', 'norms.id)
->orderByRaw('finding_id, norms.normtype_id')
->get();

Related

Laravel: chained relationships in a verbose way

I have several models chained by 1:N relationships:
User (1 -> N) Asignatura (1 -> N) Clase (1 -> N) Asiento
I want to obtain a collection of Asiento models, containing all the Asientos of a User.
I tried the "obvious"
$user->asignaturas->clases->asientos;
but as $user->asignaturas is a collection, ->clases is not recognized as a valid method (all due relationships, asignaturas(), clases() and asientos() has been created in their respective models)
So I did the following to solve the problem:
$user = User::findorFail(Auth::user()->id);
$asignaturasArr = $user->asignaturas->pluck('id')->toArray();
$clases = Clase::whereIn('asignatura_id', $asignaturasArr)->get();
$clasesArr = $clases->pluck('id')->toArray();
$asientos = Asiento::whereIn('clase_id', $clasesArr)->get();
but I suspect that Laravel should have a less verbose way to do the work, and I am not being able to find it.
I will be grateful to have any advice from any expert.
Thank you very much in advance and best regards.
$user->asignaturas->clases->asientos; will not work as asignaturas is a collection, so you would have to do this to work: $user->asignaturas->first()->clases->first()->asientos; but you would have to iterate over each asignaturas and clases and that is for surely not ideal.
So, you could try using hasManyThrough but that would also not work 100%, as it only allows 3 level deep, it would only work for User -> Asignaturas -> Clases (so you get all User's Clases but you would still need to iterate each Clase to get every Asientos), or it would only work for Asignaturas -> Clases -> Asientos (but you would have to write this in Asignatura model, not in user, I still think this would work for you).
So, if we use the last one I mentioned, your Asignatura model should have this:
class Asignatura extends Model
{
public function asientos()
{
return $this->hasManyThrough(Asiento::class, Clase::class);
}
}
Have in mind that this must follow a standard convention, read the documentation about it, and if it is not working, share your models and tables' names so we can help you make it work.
Then, in your User model:
class User extends Model
{
public function asientos()
{
$asientos = collect();
$this->asignaturas->each(function ($asignatura) use ($asientos) {
$asignatura->asientos->each(function ($asiento) use ($asientos) {
$asientos->push($asiento);
});
});
return $asientos;
}
}
But, that code is not 100% good, as you are looping a lot, still will work.
Other solution will be to just do a normal query (manually joining) and get the data that way, but I will not do it here as I have no idea what your tables' names are, you can find good tutorials about this, you will have to join tables, really simple to do.
Other possible solution you could try using, but I have never used, is using a package that someone created because of this exact problem, it is called staudenmeir/eloquent-has-many-deep. It can handle "infinite" levels instead of just 3. This is the SO question related to it.
Al last, I think that I found the Eloquent (elegant) solution to the problem. Is the following:
$asientos = $user->asignaturas->map->clases->unique()->flatten()->map->asientos->unique()->flatten();
Using map and unique()->flatten() (and, of course, the necessary relationships in the models), you can chain as much relationships levels as you want.

Is Laravel sortBy slower or heavier to run than orderBy?

My concern is that while orderBy is applied to the query, I'm not sure how the sortBy is applied?
The reason for using sortBy in my case is because I get the collection via the model (i.e. $user->houses->sortBy('created_at')).
I'm just concerned about the performance: is sortBy simply looping each object and sorting them?, or is Laravel smart enough to simply transform the sortBy into an orderBy executed within the original query?
You need orderBy in order to perform a SQL order.
$user->houses()->orderBy('created_at')->get()
You can also eager load the houses in the right order to avoid N+1 queries.
$users = User::with(['houses' => function ($query) {
return $query->orderBy('created_at');
}])->get();
$orderedHouses = $users->first()->houses;
The sortBy method is applied to the Collection so indeed, it will looping each objects.
The orderBy() method is much more efficient than the sortBy() method when querying databases of a non-trivial size / at least 1000+ rows. This is because the orderBy() method is essentially planning out an SQL query that has not yet run whereas the sortBy() method will sort the result of a query.
For reference, it is important to understand the difference between a Collection object and a Builder object in Laravel.
A builder object is, essentially, an SQL query that has not been run. In contrast, a collection is essentially an array with some extra functionality/methods added. Sorting an array is much less efficient than pulling the data from the DB in the correct format on the actual query.
example code :
<?php
// Plan out a query to retrieve the posts alphabetized Z-A
// This is still a query and has not actually run
$posts = Posts::select('id', 'created_at', 'title')->orderBy('title', 'desc');
// Now the query has actually run. $posts is now a collection.
$posts = $posts->get();
// If you want to then sort this collection object to be ordered by the created_at
timestamp, you *could* do this.
// This will run quickly with a small number or rows in the result,
// but will be essentially unusable/so slow that your server will throw 500 errors
// if the collection contains hundreds or thousands or objects.
$posts = $posts->sortBy('created_at');

Eloquent Laravel sort paginate query by related model

I have the following Eloquent query.
Company::has('cars')->with([
'cars', 'location' => function ($q) {
$q->nearBy(); // *** See futher
}
])->take(40)->paginate(10);
How can I order the Companies by distance?
*** This is the NearBy Scope in the Location-model:
public function scopeNearBy($query)
{
$location = json_decode(request()->cookie('location'));
$query->distance($location->lat, $location->lng); // ** Using package
return $query->orderBy('distance');
}
** Which uses this package.
The calculation of the distances is OK and works when I call the following.
\App\Address::nearBy()->get()
You have to join location table to Company query, and then you can order by distance.
In my opinion, the only way is to store the distance of each location when they are added. Then, of course, you can order them by distance.
if you can find distance using that package . Append the distance to your Company Model.
Then you can easily sort the collection in controller
refer https://laravel.com/docs/5.8/collections#method-sortby

How do I define relation between users and points in Eloquent ORM

I have 3 tables:
Users - for storing users
User_point - for associacion between users and points(has only user_id and point_id)
Points for description of points(id, amount, description)
How do I define a relation between these? I tried
public function points(){
return $this->belongsToMany('\App\Point', 'user_point');
}
but when I do
return $user->points()->sum('amount');
it returns just one
Edit:
At first I tried making it like this as it makes more sense:
public function points(){
return $this->hasMany('\App\Point');
}
But it wouldn't work
SUM is an aggregate function and so it should only return one row.
$user->points will be a collection of points attached to that user.
$user->points() is a query that you can do additional work against (i.e. $user->points()->whereSomething(true)->get()).
As user ceejayoz pointed out, using user->points() is going to return a builder which you can do additional work on. I believe using sum() on that will look at the first row returned which is what you indicated is actually happening.
Likely, what you really want to do is $user->points->sum('amount');
That will get the sum of that column for the entire collection.

Is it possible to eager load arbitrary queries in Eloquent?

I'm working in Laravel 4, and I have a Child model with multiple EducationProfiles:
class Child extends EloquentVersioned
{
public function educationProfiles()
{
return $this->hasMany('EducationProfile');
}
}
If I wanted to get all the EducationProfiles for each kid under age 10 it would be easy:
Child::where('date_of_birth','>','2004-03-27')->with('educationProfiles')->all();
But say (as I do) that I would like to use with() to grab a calculated value for the Education Profiles of each of those kids, something like:
SELECT `education_profiles`.`child_id`, GROUP_CONCAT(`education_profiles`.`district`) as `district_list`
In theory with() only works with relationships, so do I have any options for associating the district_list fields to my Child models?
EDIT: Actually, I was wondering whether with('educationProfiles') generates SQL equivalent to:
EducationProfile::whereIn('id',array(1,2,3,4))
or whether it's actually equivalent to
DB::table('education_profiles')->whereIn('id',array(1,2,3,4))
The reason I ask is that in the former I'm getting models, if it's the latter I'm getting unmodeled data, and thus I can probably mess it up as much as I want. I assume with() generates an additional set models, though. Anybody care to correct or confirm?
Ok, I think I've cracked this nut. No, it is NOT possible to eager load arbitrary queries. However, the tools have been provided by the Fluent query builder to make it relatively easy to replicate eager loading manually.
First, we leverage the original query:
$query = Child::where('date_of_birth','>','2004-03-27')->with('educationProfiles');
$children = $query->get();
$eagerIds = $query->lists('id');
Next, use the $eagerIds to filterDB::table('education_profile') in the same way that with('educationProfiles') would filter EducationProfile::...
$query2 = DB::table('education_profile')->whereIn('child_id',$eagerIds)->select('child_id', 'GROUP_CONCAT(`education_profiles`.`district`) as `district_list`')->groupBy('child_id');
$educationProfiles = $query2->lists('district_list','child_id');
Now we can iterate through $children and just look up the $educationProfiles[$children->id] values for each entry.
Ok, yes, it's an obvious construction, but I haven't seen it laid out explicitly anywhere before as a means of eager loading arbitrary calculations.
You can add a where clause to your hasMany() call like this:
public function educationProfilesUnderTen() {
$ten_years_ago = (new DateTime('10 years ago'))->format('Y-m-d');
return $this->hasMany('EducationProfile')->where('date_of_birth', '>', $ten_years_ago)
}

Resources