Laravel - What's the benefit of calling query() method on an Eloquent model - laravel

In the course "Eloquent Performance Patterns" by Jonathan Reinink I've seen him calling query() method on his Eloquent models before writing the actual query. Let me give an example:
User::query()
->with('company')
->paginate()
As far as I know, he could write:
User::with('company')
->paginate()
I've seen this practice over and over in his course. It raised a question in my head: Is there any benefit in doing so or is it just a personal preference?

There is no difference, you don't have to add query(). It was practical to instantiate the query for the cases similar to this;
$user = User::query();
if (true) { // check some condition and append query condition(s)
$user->with('company');
}
return $user->paginate();
but then when method came and it became easier to do that;
return User::query()
->when(true, function (Builder $builder) {
$builder->with('company');
})
->paginate();

Related

Eloquent - apply value from relationship to where in scope

So I have this need to check if a customer needs to be called. Customers has to be called at intervals depending on a value days_between_calls in a BelongsTo model called SubscriberType. I got it to work but I don't like it, maybe there is a cleaner way.
So I have a model Subscription with relations :
public function subscriberType()
{
return $this->belongsTo(SubscriberType::class);
}
public function calls()
{
return $this->hasMany(Call::class);
}
and a (very simplified) scope :
public function scopeNeedsCall(Builder $query) {
$query->join('subscriber_types', 'subscriber_types.id', '=', 'subscriptions.subscriber_type_id')
->whereDoesntHave('calls', function(Builder $query) {
$query->whereRaw('calls.created_at > DATE_SUB(NOW(), INTERVAL days_between_calls DAY)');
});
}
Is there any cleaner way to use this days_between_calls field's value without manually joining its table and without writing raw sql?
Thanks ahead.
So it looks like there is not much that can be improved, and I do need a rawsql part here. I improved it a little anyway using https://laravel.com/docs/9.x/eloquent-relationships#has-one-of-many but that's about it.

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

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.

Laravel 5 hasMany relation returning incorrect relations

I have a Yard model and Treatment model, I am using the following hasMany relationship to return treatments that are currently active:
public function activeTreatments() {
return $this->hasMany('App\Models\Treatment')
->where(function ($q) {
$q->where('expires_at','>=', Carbon::now('Pacific/Auckland'))
->where('completed_at','>', Carbon::now('Pacific/Auckland'));
})
->orWhere('completed',false);
}
For some reason when I add the ->orWhere('completed',false) the query returns all treatments not just the treatments associated with the specific yard. What am I doing wrong here?
It's hard to say exactly what is going on without inspecting the SQL being generated.
Wherever you are using this code, you could chain a toSql() on the end to see what the query looks like (use this where you would use a get()). Or you could enable the query log to see what is being queried.
Given the symptoms, it is likely that the orWhere() is negating a condition used to filter the models.
Try nest the orWhere() inside the current where() statement:
public function activeTreatments() {
return $this->hasMany('App\Models\Treatment')
->where(function ($q) {
$q->where(function($q), {
$q->where('expires_at','>=', Carbon::now('Pacific/Auckland'))
->where('completed_at','>', Carbon::now('Pacific/Auckland'));
})->orWhere('completed',false);
});
}

Overriding Laravel get and first methods

I need to override above mentioned methods to skip some database records. Using where is not an option since I would have to use it every single time as there are records in database that I do not need most of the time and I am not allowed to delete them from DB. Here is my attempt of doing this:
class SomeTable extends BaseModel {
public static function first() {
$query = static::query();
$data = $query->first();
if($data && $data->type == 'migration_type') return null;
return $data;
}
public static function get() {
$query = static::query();
$data = $query->get();
foreach($data as $key => $item) {
if($item->type == 'migration_type') unset($data[$key]);
}
return $data;
}
}
The problem with this code is that it works only when direct called on model. If I am using some other functions, like where, before get or first methods, it just skips my overridden method.
What would be the right way to do this and should I put this code within model?
My question is not duplicate as in the answer from mentioned question it is said:
all queries made from Models extending your CustomModel will get this new methods
And I need to override those two functions only for specific model, not for each one in application as not all tables have type column. That's the reason why I have written them within model class.
I need to override above mentioned methods to skip some database records.
Consider a global query scope on the model.
https://laravel.com/docs/5.8/eloquent#global-scopes
Global scopes allow you to add constraints to all queries for a given model. Laravel's own soft delete functionality utilizes global scopes to only pull "non-deleted" models from the database. Writing your own global scopes can provide a convenient, easy way to make sure every query for a given model receives certain constraints.
The issue here is that the where() method on the model returns a QueryBuilder instance where get() will return a Collection instance.
You should be able to override collection's default methods by adding a macro in it's place and can be done like so...
Collection::macro('toUpper', function () {
return $this->map(function ($value) {
return Str::upper($value);
});
});
Extending the query builder instance is not so easy but a good tutorial exists here and involves overriding the application's default connection class, which is not great when it comes to future upgrades.
Because after calling where you're dealing with the database builder and theses methods inside your model aren't being called .. about the issue you might overcome it by using select instead of first directly so will deal with the builder ..
example:
SomeTable::select('col1','col2')->take(1)->get();
another thing overriding these kind of methods is not a good idea if you're working with other developer on the same project.
good luck

How to fetch two related objects in Laravel (Eloquent) with one SQL query

I am trying to get two related objects in Laravel using eager loading as per documentation.
https://laravel.com/docs/5.4/eloquent-relationships#eager-loading
My models are:
class Lead extends Model {
public function session() {
return $this->hasOne('App\LeadSession');
}
}
class LeadSession extends Model {
public function lead() {
return $this->belongsTo('App\Lead');
}
}
I want to get both objects with one SQL query. Basically I want to execute:
select * from lead_sessions as s
inner join lead as l
on l.id = s.lead_id
where s.token = '$token';
and then be able to access both the LeadSession and Lead objects. Here is the php code I am trying:
$lead = Lead::with(['session' => function ($q) use ($token) {
$q->where('token','=',$token);
}])->firstOrFail();
print($lead->session->id);
I have also tried:
$lead = Lead::whereHas('session', function($q) use ($token) {
$q->where('token','=',$token);
})->firstOrFail();
print($lead->session->id);
and
$session = LeadSession::with('lead')->where('token',$token)->firstOrFail();
print($session->lead->id);
In all three cases I get two queries executed, one for the leads table, and another for the lead_sessions table.
Is such a thing possible in Eloquent? In my view it should be a standard ORM operation, but for some reason I am struggling a whole day with it.
I don't want to use the Query Builder because I want to use the Eloquent objects and their functions afterwards.
I am coming from Python and Django and I want to replicate the behavior of select_related function in Django.
Try this and see if it makes more than one query
$session = LeadSession::join('leads', 'leads.id', '=', 'lead_sessions.lead_id')
->where('token',$token)
->firstOrFail();
I hope it only runs a single query. I didnt test this. Not sure if you have to add a select() to pick the columns. But yeah, try this first.
Updates
Just adding how to use both session and lead data. Try a select and specify the data you need. The reason being that if both tables have similar columns like 'id', one of them will be overwritten. So you have to alias your select like
$session = LeadSession::join('leads', 'leads.id', '=', 'lead_sessions.lead_id')
->where('token',$token)
->select(
'lead_sessions.*',
'leads.id as lead_id',
'leads.name',
'leads.more_stuff'
)
->firstOrFail();
Now all this data belongs to $session variable. For testing you were doing
print($lead->session->id);
//becomes
print($session->lead_id); //we aliased this in the query

Resources