Select specific column on lazy eager loading query Laravel - laravel

I am using Laravel 8.
Assume the show() function is called in a Controller, providing me with a specific Location.
The goal is to return a view with that location, but adding relation data to the $location variable beforehand. Since there already is a location but it is missing some data, I call the load function to lazy eager load the relation. Also, I need the soft Deleted customers as well, thus using a closure like so:
public function show(Location $location)
{
$location = $location->load([
'customers' => function($query) {
$query->withTrashed();
}]
);
return view('location.EditLocationView', ['location' => $location]);
}
Now, I face a problem. I only need the customers names stored inside of the $location variable, preferably in an array "['James', 'Christian' ... ]"
How do I do that? The code should probably look like something somilar to this:
public function show(Location $location)
{
$location = $location->load([
'customers' => function($query) {
$query->withTrashed()->select('names')->toArray();
}]
);
return view('location.EditLocationView', ['location' => $location]);
}
And $location should look like this:
$location =
name: ..
lat: ..
lng: ..
customers: ['James', 'Christian' ... ]
Thanks for your help.

The problem with eager load is you can't select only name column you will need to select the foreign key also in order to properly map the data with location like
$query->withTrashed()->select(['location_id','names'])
If its a single $location record then you could also perform lazy load because it will fire one query (same as above eager load case) and then you can store customers name in another variable and pass it to view.
$customers = $location->customers()->withTrashed()->pluck('name')->toArray();

Related

Laravel, eloquent, query: problem with retriving right data from DB

I'm trying to get the right data from the database, I'm retriving a model with a media relation via eloquent, but I want to return a photo that contains the 'main' tag stored in JSON, if this tag is missing, then I would like to return the first photo assigned to this model.
how i assign tags to media
I had 3 ideas:
Use orWhere() method, but i want more likely 'xor' than 'or'
$models = Model::with(['media' => function ($query) {
$query->whereJsonContains('custom_properties->tags', 'main')->orWhere();
}]);
return $models->paginate(self::PER_PAGE);
Raw SQL, but i don't really know how to do this i tried something with JSON_EXTRACT and IF/ELSE statement, but it was to hard for me and it was a disaster
Last idea was to make 2 queries and just add media from second query if there is no tag 'main'
$models = Model::with(['media' => function ($query) {
$query->whereJsonContains('custom_properties->tags', 'main');
}]);
$models_all_media = Model:: with(['media']);
return $models->paginate(self::PER_PAGE);
but i tried something like
for($i=0; $i<count($models); $i++) {
$models->media = $models_all_media
}
but i can't do this without get() method, beacuse i don't know how to change this to LengthAwarePaginator class after using get()
try using whereHas https://laravel.com/docs/9.x/eloquent-relationships
Model::with('media')
->whereHas('media',fn($media)=>$media->whereJsonContains('custom_properties->tags', 'main'))
->paginate(self::PER_PAGE);
as per your comment you can use
$models = Model::with(['media' => function ($query) {
$query->whereJsonContains('custom_properties->tags', 'main');
}])
->leftJoin('media', function ($join) {
$join->on('models.id', '=', 'media.model_id')
->whereNull('media.custom_properties->tags->main');
})
->groupBy('models.id')
->paginate(self::PER_PAGE);
return $models;

Eager load for collections on the fly

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.

How to remove fields from Laravel JSON Api response

I have an API returning an object with one-to-one relation to another object. As the models behind the objects, do have timestamps, these are also delivered when asking API.
// Get all transactions
Route::get('transaction', function() {
return Transaction::with('Personone','Persontwo')->get();
});
How do I prevent Laravel from returning the timestamps of the objects in the API?
I googled, but only found some hints to middleware or response macros but found no example pointing me into the right direction. Maybe you can help me.
You can make attributes "hidden" so that they do not show up in json.
docs
class Transaction extends Model
{
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = ['timestamp'];
}
I'm not sure if I get the question correctly but if you want to select from eager loads there are two ways
first one is inline selecting
Route::get('transaction', function () {
return Transaction::with('Personone:id,foo,bar', 'Persontwo:id,foo,bar,foobar')->get();
});
second one is to pass a closure
Route::get('transaction', function () {
return Transaction::with([
'Personone' => function ($query) {
$query->select('id', 'foo', 'bar');
},
'Persontwo' => function ($query) {
$query->select('id', 'foo', 'bar', 'foobar');
}])->get();
});
Eager Loading Specific Columns You may not always need every column
from the relationships you are retrieving. For this reason, Eloquent
allows you to specify which columns of the relationship you would like
to retrieve:
$users = App\Book::with('author:id,name')->get();
Constraining Eager Loads Sometimes you may wish to eager load a
relationship, but also specify additional query constraints for the
eager loading query. Here's an example:
$users = App\User::with(['posts' => function ($query) {
$query->where('title', 'like', '%first%'); }])->get(); In this example, Eloquent will only eager load posts where the post's title
column contains the word first. Of course, you may call other query
builder methods to further customize the eager loading operation:
$users = App\User::with(['posts' => function ($query) {
$query->orderBy('created_at', 'desc'); }])->get();

Grab relation of saved Eloquent model

After I save a Eloquent model, how am I able to retrieve a relationship with it?
So for example:
$model = new Foo;
$model->save();
dd($model); //return an object of Foo
Let's say I've set an relation to Baz, I should grab the relation like a normal Eloquent Object.
dd($model->with('baz')->get());
But this is returning every Foo record in de database with the relationship.
I just want to be able to get the current Baz model which is related to the saved Foo model.
I could do:
$result = Foo::with('baz')->find($model->id);
dd($result);
But this results in another query, which I want to prevent.
Simply just access it once like this:
$model->baz
And the relationship will be loaded.
Alternatively you can lazy eager load the relation:
$model->load('baz');
The effect is the same, although the first way allows you to actually use the result of the relation directly. If you just want to have the relationship in your array / JSON output I suggest you use the second method, because it's clearer what you're doing.
Depends on how you declare your relationships on your models. Assuming your relationship is declared as One to Many like this:
class Foo extends Eloquent {
public function bazes()
{
return $this->hasMany('Baz');
}
}
Then you can try this:
$results = $model->bazes;
or
$results = Foo::find($id)->bazes
$results is an iterable collection of bazes related directly with foo->id = x
But if you want eager loading with filters, then you can try like this:
$result = Foo::with(array('bazes' => function($query)
{
$query->where('id', $id);
}))->get();
I hope this works for you.
You should eager load on existing models using load().
Try
$model = new Foo;
$model->save();
$model->load('baz');

Laravel Relationships Conditions - 3 tables

I've got a situation where I've got Posts, Users and Comments.
Each comment stores a post_id and a user_id. What I want to do is get all of a user's comments on a particular post, so that I can do a call like this:
$comments = Auth::User()->comments(post_id=x)->text
(where I know what x is)
I have:
User->HasMany(comments)
Comments->HasOne(User)
Comments->HasOne(Project)
Project->HasMany(comments)
I feel like there needs to be a where or a has or a wherehas or something thrown in.. the best I can manage is that I pull Auth::User()->comments into an array and then search through the array until I find the matching post ID.. that seems wasteful.
with doesn't apply any join, so you can't reference other table.
You can use this:
// User model
public function comments()
{
return $this->hasMany('Comment');
}
// Comment model
public function scopeForPost($query, $postId)
{
$query->where('post_id', $postId);
}
// then you can do this:
Auth::user()->comments()->forPost($postId)->get();
Alternatively you can eager load comments with constraint:
User::with(['comments' => function ($q) use ($postId) {
$q->where('post_id', $postId);
}])->find($someUserId);
// or exactly the same as above, but for already fetched user:
// $user .. or
Auth::user()->load(['comments' => function ($q) use ($postId) {
$q->where('post_id', $postId);
}]);
// then you can access comments for $postId just like this:
Auth::user()->comments; // collection
When you need to filter your relations, you just have to do it in your Eloquent query:
$data = User::with('posts', 'comments')
->where('users.id', Auth::User()->id)
->where('posts.id', $postID)
->get();
Then you can
foreach($data->comments as $comment)
{
echo $comment->text;
}
Your Comments table would have foreign keys Post_Id and User_ID
To Access all the comments of a particular post from a particular user , can you try this way?
Comment::select('comments.*')
->where('comments.user_id', Auth::user()->id)
->leftJoin('posts','posts.id','=','comments.post_id')
->leftJoin('users','users.id','=','comments.user_id')
->get();
Am sure there is better way to achieve it, but this should give you desired results.
Note use aliases if you have conflicting column names
Let me know if this worked.

Resources