We have a laravel 5.5 app in production and recently we noticed that eager loading relationships seem to be failing.
The query that is failing is:
$allClients = Client::with([
'tasks',
'tasks.task_files',
'resume_files',
])
->leftjoin('users', 'users.id', 'clients.client_id')
->where([
['status', 0],
['assigned_to', $user->id],
])
->orderBy('name', 'ASC')
->get();
The tasks.task_files relationship is not loading in production.
If I run the same on my local, it loads data fine.
If I run the same in a tinker shell on production, it loads data fine.
But when the controller is hit via a request, it doesn't load the task_files data.
I've tried logging the queries being run through QueryLog and they seem correct and running the same SQL queries gives the correct data.
The relationships are:
Model: Client
/**
* This is used to get the tasks for the current client.
*
* #return Task $tasks[]
*/
public function tasks()
{
return $this->hasMany('App\Task', 'client_id', 'client_id');
}
Model: Task
/**
* This is used to get the task files for the current task.
*
* #return TaskFile $files[]
*/
public function task_files()
{
return $this->hasMany('App\TaskFile', 'task_id', 'task_id')->whereIn('type', ['type1', 'type2'])->latest('updated_at');
}
I have no clue why would this suddenly stop working in production.
EDIT:
I verified that the schema is correct and the data is clean.
Related
Question:
I Noticed interesting behavior in Laravel 7.x where eager loaded relationships don't always have bindings. Is this expected behavior and why would that be the case?
Code:
Actual Queries Laravel Runs:
select top 100 * from task_view
select id, name from task_view where active = ? and student_id in (?, ?, ?)
select id, name from task_view where active = ? and teacher_id in (1 ,2 ,3)
Relationships on Model:
public function studentTasks()
{
return $this->hasMany(StudentTasks::class, 'student_id', 'id');
}
public function teacherTasks()
{
return $this->hasMany(TeacherTasks::class, 'teacher_id', 'teacher_id');
}
Calling Code:
TaskView::query()->with(['studentTasks', 'teacherTasks']);
Additional Points:
I think it may have to do with that where the localkey of the relationship (the 3rd argument) is 'id' then the values aren't bound.
My assumption is that bindings are to prevent sql injection and the Docs seem to confirm that. If that's the case then why would id's of the model that the relationship is on not need to be bound? I would assume there's still an issue of SQL Injection there.
I have not seen anyone discussing this from my searching around, (Stackoverflow, Laracasts, Laravel docs)
(I printed out the queries using the below code in AppServiceProvider:boot)
$counter = 0;
\DB::listen(function ($query) use (&$counter) {
echo 'count: '.++$counter.PHP_EOL;
// echo memory_get_usage();
echo $query->sql.PHP_EOL;
echo implode(',', $query->bindings).PHP_EOL;
});
This is a change introduced into Laravel 5.7.14. The initial pull request can be found here. From there you can find more pull requests making updates to the functionality.
It was done as a performance enhancement when needing to eager load a large number of records (many thousands). Instead of having thousands of bound parameters, it puts the raw ids directly in the query. Initially it was done to work around a MySQL PDO bug, but really all database drivers can benefit with not having thousands of bound parameters.
The reason why it does not introduce a SQL injection vulnerability is that:
It only replaces the bindings with the raw values when the ids are integers, and
It runs all the ids through an integer conversion before adding them to the query.
This is the function that ultimately determines if parameters will be used or if raw ids will be used (https://github.com/laravel/framework/blob/7.x/src/Illuminate/Database/Eloquent/Relations/Relation.php#L310-L323):
/**
* Get the name of the "where in" method for eager loading.
*
* #param \Illuminate\Database\Eloquent\Model $model
* #param string $key
* #return string
*/
protected function whereInMethod(Model $model, $key)
{
return $model->getKeyName() === last(explode('.', $key))
&& in_array($model->getKeyType(), ['int', 'integer'])
? 'whereIntegerInRaw'
: 'whereIn';
}
And here is the whereIntegerInRaw() function that shows the keys are int cast before being added into the raw query (https://github.com/laravel/framework/blob/7.x/src/Illuminate/Database/Query/Builder.php#L961-L985):
/**
* Add a "where in raw" clause for integer values to the query.
*
* #param string $column
* #param \Illuminate\Contracts\Support\Arrayable|array $values
* #param string $boolean
* #param bool $not
* #return $this
*/
public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = false)
{
$type = $not ? 'NotInRaw' : 'InRaw';
if ($values instanceof Arrayable) {
$values = $values->toArray();
}
foreach ($values as &$value) {
$value = (int) $value;
}
$this->wheres[] = compact('type', 'column', 'values', 'boolean');
return $this;
}
Is it possible to use a custom query or maybe just modify the query builder used by Symfony when doing this?
public function profileAction(Request $request)
{
/** #var User */
$user = $this->getUser(); // <=== this
return $this->getUser();
}
I would like to modify the query builder in order to add some left joins and get additional info from the database. Something like this:
public function findOneByUsernameOrEmail($username)
{
$qb = $this->createQueryBuilder('u');
return $qb
->addSelect('c', 'ai', 'pp')
->leftJoin('u.country', 'c')
->leftJoin('u.avatarImage', 'ai')
->leftJoin('u.profilePicture', 'pp')
->where($qb->expr()->orX(
'u.username = :username',
'u.email = :username'
))
->setParameter('username', $username)
->getQuery()
->getOneOrNullResult()
;
}
I would like to be able to do this on demand, not every time.
The UserProvider is responsible of fetching the User from your persistence source. The configuration is done in the security.yaml configuration file.
You can find how to use your custom query in this page of the documentation https://symfony.com/doc/current/security/user_provider.html#using-a-custom-query-to-load-the-user
Unfortunately you can't do it on demand.
I am using a database mysql with a project table, an activity table and a user table.
A project has many activities and an activity is assigned to one person so I have the following links in my models:
Project model:
public function activities()
{
return $this->hasMany(\App\Activity::class, 'project_id');
}
Then I have
Activity model:
public function user()
{
return $this->belongsTo(\App\User::class, 'user_id');
}
I would like to get for a project, the list of users on it.
I can do
$project = App\Project::find(1);
$activities = $project->activities;
And this works, I get all the activities on this project.
I can do
$activity = App\Activity::find(43113);
$name = $activity->user->name;
But how can I get all the users name on a project?
I tried linking this but it is not working, it tells me I have a bad exception.
For example, this below doesn't work and generates an error:
$names = $project->activities->user;
or
$names = $project->activities->with('users');
I am on Laravel 7.10.
You can use with as below.
$project = App\Project::with('activities.user')->where('id',1)->first();
echo '<pre>';
print_r($project);
exit;
By using project you can get activities and per activities you've user.
I have this, user has many to many property via pivot property_users.
I am making somehow reusable classes in my webapp.
These are the models with their eager loading functions:
//User model
public function properties()
{
return $this->belongsToMany(Property::class, 'property_users', 'user_id', 'property_id');
}
//Property model
public function property_users()
{
return $this->hasMany(PropertyUser::class, 'property_id', 'id');
}
//PropertyUser model
public function user()
{
return $this->belongsTo(User::class);
}
//GetProperties class
public function handle()
{
return auth()->user()->properties()->get();
}
//somewhere in a feature
$properties = $this->run(GetProperties::class);
//this returns valid properties under the logged in user
I now need to get the chat_username in property_users that belongs to this user
I manage to make it work if I loop through the properties and then doing it on the fly.
$properties = $properties->map(function($property) {
$propertyUsers = $property->property_users()->get();
$chatUsername = null;
foreach($propertyUsers as $propertyUser) {
if($propertyUser->property_id == $property->id) {
$chatUsername = $propertyUser->chat_username;
}
}
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $chatUsername
];
});
But I am trying to reduce query on loop to reduce hits especially when they are on multiple properties in the database.
The other way is that I can add the property_users in the eager loading under GetProperties class by updating it to:
$query = Property::query();
$query->with(['property_users']);
$query->whereHas('property_users', function($qry) {
$qry->where('user_id', Auth::user()->id);
});
$properties = $query->get();
return $properties;
But I do not want to rely on adding more eager loading to the original GetProperties class as the GetProperties will get fat and I do not really need those data (let's say adding property_invoices, property_schedules, etc but not really needing it in some area).
Rather, I want to do the eager loading on the fly but with a twist! This is how I would imagine it:
Collect all the ids from the properties, do the fetch using wherein and apply all the users to the properties in a single query. This way it will be even more beautiful.
Maybe something like this: (using the original GetProperties class)
$properties = $this->run(GetProperties::class);
//this does not work. The error is: Method Illuminate\Database\Eloquent\Collection::property_users does not exist.
$property->property_users = $properties->property_users()->get();
Would be great if someone can show me how to do it.
What about eager loading only the fields you actually need?
$query->with('property_users:id,user_id');
The model will not get fat, and you will not need to do separate queries in the loop.
It is documented in the official documentation: https://laravel.com/docs/5.8/eloquent-relationships#eager-loading , see Eager Loading Specific Columns
Edit: if you want to perform the query after the GetProperties class, you will need to collect all the ids and perform a second query. I honestly don't like this second approach, because it is far slower, less performant and I consider it less elegant then adding a single line in the GetProperties class, but it is gonna work:
$properties = $this->run(GetProperties::class);
$ids = $properties->pluck('id'); // Get all the ids as an array
$propertyUsers = propertyUsers::whereIn('property_id', $ids)->get(); // Get the propertyUsers model
foreach($properties as $property) {
$property->property_users = $propertyUsers->where('property_id', $property->id); // Not even sure you can do that, proerty_users may not been writable
}
Alright, after reading here and there.
I found this article: https://laravel-news.com/eloquent-eager-loading
The solution is really nice. Simply:
$properties = $this->run(GetProperties::class);
//lazy load
$properties->load('property_users');
$properties = $properties->map(function($property) {
$user = $property->property_users->first(function($user) {
if($user->user_id == Auth::user()->id) {
return $user;
}
})->only('chat_username');
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $user['chat_username']
];
});
After checking query logs:
//auth query done by middleware
[2019-05-21 07:59:11] local.INFO: select * from `users` where `auth_token` = ? or `temporary_auth_token` = ? limit 1 ["token_removed_for_security_purpose","token_removed_for_security_purpose"]
//These are the queries made:
[2019-05-21 07:59:11] local.INFO: select `properties`.*, `property_users`.`user_id` as `pivot_user_id`, `property_users`.`property_id` as `pivot_property_id` from `properties` inner join `property_users` on `properties`.`id` = `property_users`.`property_id` where `property_users`.`user_id` = ? [8]
[2019-05-21 07:59:11] local.INFO: select * from `property_users` where `property_users`.`property_id` in (2, 4)
This way, I can keep my GetProperties as small as possible, then just lazy load it whereever I need it.
Looking for some advice on creating an eloquent query.
I have 3 tables
Projects
Tasks
UserTasks
A project has tasks, and in usertasks we save the task and user id so we know it has been completed by the user. Now I need to query all projects that have been completed by the user, so projects that have tasks where all tasks for that project have been completed.
I can't seem to wrap my head around it, I am trying to create a scope for this.
public function scopeisCompleted($query)
{
$query = $query->whereHas('userTasks', function ($query) {
$query->where('user_id', '=', Auth::user()->id);
})->get();
return $query;
}
I can't seem to get my head around how to make sure all tasks for that project are completed by the user.
Any advice on moving forward?
I think you'd need something like this:
public function scopeisCompleted($query)
{
$query = $query->whereHas('userTasks', function ($query) {
$query->where('user_id', '=', Auth::user()->id)
->whereHas('task', function ($query) {
$query->where('completed', 1);
});
})->get();
return $query;
}
So as long as the task relationship is setup for the UserTask model and the column is completed, if not change it to the correct one.
I just read your question in more detail, if you need to check that all tasks are completed then that would be a bit more complicated, the query would need to find how many tasks there are and if that number matches the total completed tasks.
Update
I think I understand a bit more now on what you're trying to do:
public function scopeisCompleted($query)
{
$query = $query->with('tasks', 'userTasks')->whereHas('userTasks', function ($query) {
$query->where('user_id', '=', Auth::user()->id);
})->get();
return $query;
}
as mentioned in the comment, I'm not sure what model this scope is being used on so this might still be incorrect, but here I've added the ->with('tasks') so that once the query results have been returned, the tasks for the project can be compared to the user completed tasks for the project, although it should still be possible to do this in the query rather than in PHP.
if (count($results->tasks)) == count($results->userTasks)) {
// complete
}
I will need to think about it later, I might have the wrong syntax for what you're trying to do and the with might need to be something like ->with('tasks.userTasks').