I've run into this issue a couple of times now and I can't seem to find a solution - I feel like I'm missing something obvious.
I am making a JSON API with Lumen, building up specific routes for specific use cases. To speed up load times, I only want to output the fields I will be using.
My model has a combination of fields, relations and attributes. I am struggling to specify the attribute when limiting output
For example:
A task has time_records. Each time_record has a value of time.
I want to output the task names, time record values plus an attribute of totalTime which I have set on the task like the below (this isn't the full code but hopefully it gets across the idea)
class Task extends Model {
protected $appends = [
'totalTime'
];
public function timeRecords() {
return $this->hasMany('TimeRecord');
}
public function getTotalTimeAttribute() {
$total = $this->timeRecords()->map(function($time_record){
return $time_record->value;
});
return array_sum($total);
}
}
If I then do the following:
Task::get();
And output that, I get all the tasks with the totalTime attribute. However, as mentioned I want to only output some attributes:
Task::select('id', 'name')
->with([
'timeRecord' => function($query){
$query->select('id');
}
])
No matter what I do, I can't seem to get the attribute output with that. The value is there in the array, but is null.
To resolve this, you have to select the fields that the attribute accesses.
Because I was only returning the ID of the time_record, the attribute was unable to calculate the total_time as the value field was not available.
(Was only while writing this question out did I discover the answer... ๐)
You can get that using with() method without callback. Callback method provide you to filter results..
Task::select('id', 'name')
->with("timeRecords:id") // use comma for multiple columns
->get();
Related
I was wondering about the best way to get the count of all the rows created before the selected one. Right now I have defined an accessor that looks like this:
// In the model
public function getPositionAttribute() {
return self::where([
// Some other condition
['created_at', '<', $this->created_at->toDateTimeString()]
])->count();
}
// In the code
$model->position
It works correctly, but I'm worried about 2 things:
Is it a bad practice to call self on the model? Looks somehow off to me.
When called in a foreach this obviously generates a query for each element which is far from optimal. Is there any way to refactor this so that it can be eager loaded in a single query?
Bonus: I have totally discarded the idea of keeping a column with some kind of index because that initially sounded impossible to maintain, eg. when a record is deleted all the others should somehow shift position. Should I reconsider it? Is there a better way?
Pretty sure that using self here is the "best practice" because that is how that keyword was designed to be used.
In regards to refactoring, i personally can't think of optimizing the query as is but instead you could create a function that preloads all the position then use it normally. Assuming your model has a unique key 'id' and you are passing in a collection of model then, you can try something like this:
public static function populateOrderPositions($modelCollection){
// Optimize this query to include your "other condition"
$idCollection = Model::orderBy('created_at') // this will make it in the order of creation
->pluck('id'); // this will only retrieve the id field
// This array will contain an array with the model object ids as key and a numeric position (starts at 0)
$positionArr = $idCollection->flip()->all();
// Then just load all the position into the object and return it.
return $modelCollection->map(function($modelObj) use ($positionArr){
return $modelObj->position = $positionArr[$modelObj->id] + 1; // +1 because array key starts at 0
};
}
You would also need to adjust your attribute code to use the loaded attribute instead of ignoring the loaded attribute like so:
public function getPositionAttribute() {
return $this->attributes['position'] ?? self::where([
// Some other condition
['created_at', '<', $this->created_at->toDateTimeString()]
])->count();
}
With these changes, you can then prepopulate the position then use it afterward without the need to query the database.
These code are untested as i don't know how your model and query will be structured and is more of an example. Also you would need to compare the performance vs your original code.
I have two tables user and post.
in my posts method I want to return users posts with custom fileds.
none of below solutions dose not works
class UserController
{
public function posts(User $user)
{
return $user->only(['username', 'name', 'posts.body' // solution one
return $user->only(['username', 'name', 'posts'=>function($q){
$q->select(['body']
}])// solution two
Does anyone have a work around?
in post only get one column value in one row then used value method
Post::where('user_id')->value('body')
if you want to get multiple value(rows) from single column used pluck method
Post::where('user_id')->pluck('body') //this will get on the array
otherwise used select method
Post::select('body')->where('user_id')->get(); // this will get on collection
Post::select('body')->where('user_id')->get()->toArray(); // this will get on array
Laravel has the option to add an $appends array to each model making additional values automatically available as if they are database attributes by adding accessors for each.
This is normally pretty handy, except in this case I need to ONLY get the fields I put into select() because DataTables is expecting only what I send to it.
Example:
Item::select(['image', 'name', 'color']);
Will return appended fields after color in the attributes.
How do I force the exclusion of the appends values when returning results?
Or alternatively, how do I get DataTables to ignore certain attributes?
Not sure which is the least time costly route.
Currently using yajra/laravel-datatables package to send data to the jQuery DataTables AJAX request.
You can call each function in the collection object and then use setHidden method to exclude the unwanted fields like this
$item= Item::select(['image', 'name', 'color'])->get()->each(function($row){
$row->setHidden(['appendedField1', 'appendedField2']);
});
And for the yajra/laravel-datatables you can use something like
$item= Item::select(['image', 'name', 'color']);
return Datatables::of($item)->remove_column('appendedField1');
To solve this I added this method to my Item model:
public static function getAppends()
{
$vars = get_class_vars(__CLASS__);
return $vars['appends'];
}
Then used the following code in the controller:
$items = Item::select(['image', 'name', 'color']);
$DT = Datatables::of($items);
call_user_func_array([$DT, 'removeColumn'], Item::getAppends()); // Has to be called this way with yajra/laravel-datatables-oracle v3.* if passing an array.
return $DT->make(true);
I want to set a certain attribute in all the models of a collection.
in plain SQL:
UPDATE table SET att = 'foo' WHERE id in (1,2,3)
the code i have:
$models = MyModel::findMany([1,2,3]);
$models->update(['att'=>'foo']);
taken from here
but doesn't work. I'm getting
Call to undefined method Illuminate\Database\Eloquent\Collection::update()
the only way i have found it's building a query with the query builder but i'd rather avoid that.
You are returning a collection, not keeping the query open to update. Like your example is doing.
$models = MyModel::whereIn('id',[1,2,3]);
$models->update(['att'=>'foo']);
whereIn will query a column in your case id, the second parameter is an array of the ids you want to return, but will not execute the query. The findMany you were using was executing it thus returning a Collection of models.
If you need to get the model to use for something else you can do $collection = $models->get(); and it will return a collection of the models.
If you do not just simply write it on one line like so;
MyModel::whereIn('id',[1,2,3])->update(['att'=>'foo']);
Another option which i do not recommend is using the following;
$models = MyModel::findMany([1,2,3]);
$models->each(function ($item){
$item->update(['att'=>'foo']);
});
This will loop over all the items in the collection and update them individually. But I recommend the whereIn method.
The best solution in one single query is still:
MyModel::whereIn('id',[1,2,3])->update(['att'=>'foo']);
If you already have a collection of models and you want to do a direct update you can use modelKeys() method. Consider that after making this update your $models collection remains outdated and you may need to refresh it:
MyModel::whereIn('id', $models->modelKeys())->update(['att'=>'foo']);
$models = MyModel::findMany($models->modelKeys());
The next example I will not recommend because for every item of your $models collection a new extra query is performed:
$models->each(function ($item) {
$item->update(['att'=>'foo']);
});
or simpler, from Laravel 5.4 you can do $models->each->update(['att'=>'foo']);
However, the last example (and only the last) is good when you want to trigger some model events like saving, saved, updating, updated. Other presented solutions are touching direct the database but models are not waked up.
Just use the following:
MyModel::query()->update([
"att" => "foo"
]);
Be mindful that batch updating models won't fire callback updating and updated events. If you need those to be fired, you have to execute each update separately, for example like so (assuming $models is a collection of models):
$models->each(fn($model) => $model->update(['att'=>'foo']) );
So I have a many to many relationship between Users and Photos via the pivot table user_photo. I use belongsToMany('Photo') in my User model. However the trouble here is that I have a dozen columns in my Photo table most I don't need (especially during a json response). So an example would be:
//Grab user #987's photos:
User::with('photos')->find(987);
//json output:
{
id: 987,
name: "John Appleseed",
photos: {
id: 5435433,
date: ...,
name: 'feelsgoodman.jpg',
....// other columns that I don't need
}
}
Is it possible to modify this method such that Photos model will only return the accepted columns (say specified by an array ['name', 'date'])?
User.php
public function photos()
{
return $this->belongsToMany('Photo');
}
Note: I only want to select specific columns when doing a User->belongsToMany->Photo only. When doing something like Photo::all(), yes I would want all the columns as normal.
EDIT: I've tried Get specific columns using "with()" function in Laravel Eloquent but the columns are still being selected. Also https://github.com/laravel/laravel/issues/2306
You can use belongsToMany with select operation using laravel relationship.
public function photos()
{
return $this->belongsToMany('Photo')->select(array('name', 'date'));
}
Im assuming you have a column named user_id. Then you should be able to do the following:
public function photos()
{
return $this->belongsToMany('Photo')->select(['id', 'user_id', 'date', 'name']);
}
You have to select, the foreign key, else it will have no way of joining it.
Specifying the exact columns you want for the Photos relationship will likely end up biting you in the butt in the future, should your application's needs ever change. A better solution would be to only specify the data you want to return in that particular instance, i.e. the specific JSON response you're delivering.
Option 1: extend/overwrite the toArray() Eloquent function (which is called by toJson()) and change the information returned by it. This will affect every call to these methods, though, so it may end up giving you the same problems as doing select() in the original query.
Option 2: Create a specific method for your JSON response and call it instead of the general toJson(). That method could then do any data building / array modifications necessary to achieve the specific output you need.
Option 3: If you're working with an API or ajax calls in general that need a specific format, consider using a library such as League/Fractal, which is built for just such an occasion. (Phil is also working on a book on building APIs, and it doesn't suck.)