What is difference between $this->Products and $this->Products() in laravel model? - laravel

I got different result from getReward1 and getReward2:
Model:
class User extends Authenticatable
{
public function Products()
{
return $this->hasMany('App\Product', 'user_id');
}
public function getReward1()
{
return $this
->Products
->where('reward', '>', 0)
->where('status', 0)
->sum('reward'); // sum = 7,690,000
}
public function getReward2()
{
return $this
->Products()
->where('reward', '>', 0)
->where('status', 0)
->sum('reward'); // sum = 7,470,000
}
}
getReward1 return 7,690,000 and getReward2 return 7,470,000 (Two different values)
What is difference between $this->Products and $this->Products() ?

$this->products;
// Returns a Collection
$this->products();
// Returns a Relation instance, which is a query builder and can be of type HasMany, BelongsTo...
$this->products()->get();
// Is EXACTLY like doing $this->products for the first time.
The main difference is that products() is just a query that hasn't been executed yet, whereas products are the actual results of this query.
Honestly, even if the name is the same and can be confusing, there are no other similarities between them.
A simple analogy:
DB::table('products')->where('user_id', 18); //could be the $user->products()
DB::table('products')->where('user_id', 18)->get(); //could be $user->products
It's just an analogy, it's not exactly like this internally, but you get the point.
To add more confusion on top of it, Collection methods are ofter similar to those you find in queries; both have where(), first()...
The main thing to remember is that with parentheses, you are still building a query. Until you call get or first, you remain in a query builder.
Without, you already have your results, you are in a Collection (https://laravel.com/docs/8.x/collections).
About the difference you get between getReward1 and getReward2, it's hard to tell exactly what's happening without seeing your database structure.
It can be a lot of things, but when you are calling the sum method, you are calling it on a Collection instance in getReward1 and on a query builder in getReward2 (you are actually executing a query with SELECT SUM(reward)...).

$this->Products() will return an instance of the query builder. The subsequence where clauses will constrain the DB query and then return only the product that you want. These will not be stored in the model instance.
$this->Products will get all of the products from the DB and store them in the model instance as an Eloquent Collection. The subsequent where clauses will then be performed on the Eloquent Collection.
Essentially, the method is doing everything in the DB, whereas, the property is fetching all of the rows and then limiting it with PHP.

Related

How to use eager loading on Laravel model with SUM of a relationship - currently getting multiple queries (N+1)

In my model I am appending a SUM calculation of a relationship which causes 80 queries to be run for the projectHours attributes.
If I don't append projectHours I get 6 queries.
I believe this is N+1 issue within the model for a relationship.
Is there some way to use eager loading within the model to reduce my queries?
Or should I be going about this a different way? I was suggested to refactor this into a Resource and wrapping the eager-load query in a scope so you could do this in your controller, but I thought resources were more for API endpoints.
Appreciated the help.
class Project extends Model
{
protected $appends = ['projectHours'];
public function jobs()
{
return $this->hasMany('App\JobItem', 'project_id', 'id');
}
public function getProjectHoursAttribute()
{
return $this->jobs()->sum('hours');
}
}
So your (N + 1) is coming from right here:
$this->jobs()->sum('hours');
It is caused accessing by your jobs relationship as a query builder instance jobs()
If you want to preload the relationship and then sum the results, you can do it like this:
$this->jobs->sum('hours');
This then uses the Eloquent Collection sum method
Example
$project = Project::with('jobs')->find(1);
$hours = $project->projectHours;

Why does groupBy() work but Count() does not in laravel eloquent model function?

I need to get counts of all the records based on belongsToMany relationship. normally I can use groupBy() in a function inside the model. but if I use count() or withCount() inside a model function, i get the error as followed:
function code:
public function TaskCount(){
return $this->belongsToMany(User::class)->count();
}
Error message:
Symfony\Component\Debug\Exception\FatalThrowableError: Call to a member function addEagerConstraints() on int in file /Users/dragonar/Dev/iyw/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php on line 560
If I do the following...
public function TaskCount(){
return $this->belongsToMany(User::class)->Count();
}
//expected record is 4(int)
//output is 4(array) user records.
...it gives me data but like 4 records of the user instead of a number 4. The user data is useless. The only thing needed is totalCount for those records.
Relationship methods have to return Relation type objects. You are returning the result of a query, count() returns a number not the Relation object / Builder. Remove the count from that statement you are returning. Renamed the relationship tasks here.
public function tasks()
{
return $this->belongsToMany(User::class);
// this returns a Relation type object a BelongsToMany object
}
Where you need to use that relationship you can then use count:
$something->tasks()->count();
Or you can load the count of the relationship using loadCount:
$something->loadCount('tasks');
$something->tasks_count;
Or via eager loading for a collection:
$results = Something::withCount('tasks')->get();
foreach ($results as $model) {
echo $model->tasks_count;
}
If you really wanted to you could create an accessor to get the count as well, you just may want to avoid the N+1 issue by preloading the relationship and using the dynamic property to access it in the accessor.
These relation objects are Builders. When you called groupBy on it previously that is returning the Builder, it isn't executing the query. You can add where conditions and order by statements because they are just building the query, not executing it, they return the builder you are calling the method on.
Laravel 6.x Docs - Eloquent - Relationships - Counting Related Models withCount loadCount
Why not use: Task::all()->count(); ?
you can use the withCount method while calling relation like this
User::withCount('images')->get();
You can add get the data and just count it.
public function TaskCount(){
return $this->belongsToMany(User::class)->get()->count();
}
You can call it like
$taskCount = $task->TaskCount();

Laravel sortBy not having any affect

I have a custom attribute on my User model that's calculates the length of some other tables and returns an integer value:
public function GetCurrentQueueLengthAttribute()
{
// return int
}
I then have an API endpoint that returns a "Team" with all its users (simple Spark pivot)
public function show($teamId)
{
$query = Team::query();
$query->with('users')->where('id', $teamId);
$team = $query->first();
return $team->users->sortBy('currentQueueLength');
return $team;
}
The issue is that the returned data doesn't change order. There are no errors, just the same order of the users every time.
Is there something I'm missing?
The sortBy function is not to be mistaken by the orderBy function, the first one sorts a collection, the second one alters the sql of the query builder.
To be able to use the sortBy function one first needs to retrieve the collection. These functions can still be chained by using:
return $team->users()->sortBy('currentQueueLength');
optionally one could also use orderByRaw if you are willing to write a custom sql query for the sorting.

Laravel Query Relationship on One Model Instance

I am aware that I can use count() to query for Eloquent relationships in Laravel, like so:
if(count($question->answers()))
Where answers() is a hasMany relationship:
public function answers()
{
return $this->hasMany('App\Models\Answer', 'question_id');
}
My question is, how do I do this when $question is not an entire collection but one Model instance?
$question = Question::where('id',$key)->first();
How do I query the above question, and only that question, for a potential relationship using count()?
I always am getting a count() of greater than zero, even when the selected question has no associated answers, which means my if block always runs and returns unwarranted null values:
if(count($question->answers()))
{
//returns nulls
}
Since calling $question->answers() is returning a QueryBuilder instance, calling count() on that will most likely always return 1. If you access $question->answers (as a property and not a method), or use the full logic $question->answers()->get(); it should properly return a Collection, which count() will function correctly on:
$question = Question::where('id',$key)->first();
if(count($question->answers) > 0){
// Do something
}
// OR
if(count($question->answers()->get()) > 0){
...
}
As suggested by #maraboc, you could also eager load your $question with answers using a ->with() clause:
$question = Question::with(["answers"])->where('id',$key)->first();
But even in this case, $question->answers() would still be returning a QueryBuilder instance, so access it as a property for count() to function correctly.
As already pointed count($question->answers()) has no meaning because $question->answers() is a Relation instance, you can call dynamic query method on that but if you want to count elements you need a collection, i.e $question->answers.
So you have two choice:
count the collection: count($question->answers)
ask the database to do the count: $question->answers()->count()
Parentheses matters

Putting eloquent results in another table to doing where queries in Laravel 5.4

For some special reasons I used append attributes in my model and now when I want to do where queries on custom attributes, for example "category", I face an error with this meaning that eloquent could not found column with "category" name!
To solve this problem I guess if I put my query's result into a temp table, I could do what I want!
Have someone any Idea about that? If it's useful to me, How can I transfer my results to the temp table?
You won't be able to limit the database query using a Model accessor's dynamic field, since that field obviously doesn't exist in the database.
However, the Collection object has fairly robust filtering capabilities, so you could filter the Collection results using the dynamic fields after the database has been queried. This is not as performant as filtering out the results before they are retrieved from the database, but you may be a situation where the performance isn't that critical or the code cleanliness/maintenance cost outweighs the performance cost.
As an example, given the following Model:
class Book extends Model
{
public function getCategoryAttribute()
{
if ($this->targetAge < 13) {
return 'child';
}
if ($this->targetAge < 18) {
return 'teen';
}
return 'adult';
}
}
The following query will not work because the category field doesn't actually exist in the table:
$childrenBooks = Book::where('category', 'child')->get(); // error: no category field
However, the following will work, because you're calling where() on the Collection of Models returned from the database, and the Models do have access to the dynamic field:
$childrenBooks = Book::get()->where('category', 'child');
The problem in this case is that, while it does work, it will get all the books from the database and create a Model instance for each one, and then you filter through that full Collection. The benefit, however, is that you don't have to duplicate the logic in your accessor method. This is where you need to weigh the pros and cons and determine if this is acceptable in your situation.
An intermediate option would be to create a Model scope method, so that your accessor logic is only duplicated in one place (if it can be duplicated for a query):
class Book extends Model
{
public function getCategoryAttribute()
{
if ($this->targetAge < 13) {
return 'child';
}
if ($this->targetAge < 18) {
return 'teen';
}
return 'adult';
}
public function scopeCategory($query, $category)
{
if ($category == 'child') {
return $query->where('target_age', '<', 13);
}
if ($category == 'teen') {
return $query->where(function ($query) {
return $query
->where('target_age', '>=', 13)
->where('target_age', '<', 18);
});
}
return $query->where('target_age', '>=', 18);
}
}
Then you can use this query scope like so:
$childrenBooks = Book::category('child')->get();
The benefit here is that the logic applies to the actual query, so the records are limited before they are returned from database. The main problem is that now your "category" logic is duplicated, once in an accessor and once in a scope. Additionally, this only works if you can turn your accessor logic into something that can be handled by a database query.
You can create temporary tables using raw statements. This post goes fairly in depth over it:
https://laracasts.com/discuss/channels/laravel/how-to-implement-temporary-table

Resources