Is there a way to pass multiple where conditions into the Laravel's collection? I mean something like:
$filters = [['col1', '=', 'val1'], ['col2', '=', 'val2']];
$found = $collection->where($filters)->first();
I know it works with the eloquent query builder, but not quite with the collections. I know I can chain multiple ->where statements (even within the foreach loop) but I need to have the original collection object, and cloning it works, but isn't so fast.
$localCollectionObject = $localCollection->first(function ($value, $key) use ($remoteCollectionObject, $compareColumnNames) {
foreach ($compareColumnNames as $compareColumnName) {
if ($value->{$compareColumnName} != $remoteCollectionObject[$compareColumnName]) {
return false;
}
}
return true;
});
This works fine also, but is even slower than clone $localCollection.
Or maybe I can "reset" where statements somehow?
I'm searching it within foreach loop with different conditions and that's a problem.
You can always use your own closure with filter:
$collection->filter(function ($item) {
return $item->col1 == 'val1' && $item->col2 == 'val2';
});
Related
I have a slow performance issue in comparing multiple arrays.
The algorithm that I've made is:
I'll be looping first the arrays by foreach
foreach($branches as $index => $branch) {
if (!$identical) {
continue;
}
// Basically this $permissions will be fetching data from the database.
$permissions = $this->get()
->where('user_id', $userId)
->where('branch_id', $userBranchId)
->sortBy('permission_id')
->pluck(['permission_id']);
// if index = 0 then I'll initialize the temporary variable (ARRAY) $initial otherwise I'll compare two variable (ARRAY)
if ($index === 0) {
$initial = $permissions;
} else {
if ($initial != $permissions) {
$identical = false;
}
}
}
So to compare multiple arrays, I use only this if condition and comparative symbol (==). But I'm thinking if
Don't query database in loop, you can use array_column to export branches array to columns "userId" and "userBranchId"
finaly build query using "whereIn"
I agree with #thaiha, you could reduce your queries to just one, and after make the comparison of the arrays. For example in Mysql you could do something like.
select * from `permissions` where (`user_id `, `branch_id `) in (('1', '2'), ('2', '4'));
then for comparing the arrays, you could do your loop, or use one of the php functions to compare arrays like array_diff is you are expecting all to match a specific set of permission. Or something like that.
Instead of looping array. I suggest that use eager loading instead.
Branch model
public function permissions(){
return $this->hasMany('App\Permission', 'foreign_key', 'local_key');
}
and in your controller
Branch::with('permissions')->where($condition)->get();
Read document here: https://laravel.com/docs/6.x/eloquent-relationships
I have one similar (area) value in two tables one and two and these two tables has relation with main table master. At a time, the master table will be having data in one relation only and the other one will be null.
With this architecture, I have a search page where user can search any values related to these tables and the search fields are placed with AND condition.
Here, if user enters some value for area I need to check the area value exists in any one of the tables (one or two) without breaking the AND condition. Tried the below code but it is breaking AND rule and considering OR. Any suggestions to fix?
$result = Master::where(function ($query) use ($request) {
if ($request->filter == true) {
$query->where('user_id', Auth::user()->id);
}
// other conditions here
if (!empty($request->area_from) && !empty($request->area_to)) {
$query->whereHas('one', function ($query) use ($request) {
$query->whereBetween('area', [$request->area_from, $request->area_to]);
});
$query->orWhereHas('two', function ($query) use ($request) {
$query->whereBetween('area', [$request->area_from, $request->area_to]);
});
}
// other conditions here
})->with(['one', 'two'])->paginate($request->item);
You are wrapping all of your where statements in brackets. I think what you want to do is pull your first part of the query out of the where clause so that you can easily wrap the whereHas part in brackets.
// Initialise the model
$query = new Master;
// Start building the query
if ($request->filter == true) {
$query->where('user_id', Auth::user()->id);
}
if (!empty($request->area_from) && !empty($request->area_to)) {
// Wrap these in brackets so we don't interfare with the previous where
$query->where(function($query2) use ($request) {
$query2->whereHas('one', function ($query3) use ($request) {
$query3->whereBetween('area', [$request->area_from, $request->area_to]);
});
$query2->orWhereHas('two', function ($query3) use ($request) {
$query3->whereBetween('area', [$request->area_from, $request->area_to]);
});
}
}
$query->with(['one', 'two'])->paginate($request->item);
You can create a merged relationship refer this link
public function mergedOneAndTwo($value)
{
// There two calls return collections
// as defined in relations.
$onedata= $this->one;
$twodata= $this->two;
// Merge collections and return single collection.
return $onedata->merge($twodata);
}
and use whereHas('mergedOneAndTwo')
Using a closer where and making the conditional inside may work fine
$master = Master::with('one')->with('two');
$result = $master->where(function($subQuery)
{
$subQuery->whereHas('one', function ( $query ) {
$query->whereBetween('area', [$request->area_from, $request->area_to] ); //assuming $request->area_from, $request->area_to is range of value
})
->orWhereHas('two', function ( $query ) {
$query->whereBetween('area', [$request->area_from, $request->area_to] );
});
});
i am filtering data using collections. But i need to use like method. I had tried to write like this : ('name', 'LIKE', '%value%') but it did not work.
Here is my method :
protected function filterData(Collection $collection, $transformer) {
foreach (request()->query() as $query => $value) {
$attribute = $transformer::originalAttribute($query);
if (isset($attribute, $value)) {
$collection = $collection->where($attribute, $value);
}
}
return $collection;
}
The 1st question is whether you really know what you are doing. If you take data from database and then filter it just to take some elements it's definitely not the best way because you can take from database for example 100000 records just to finally have only 2 elements and it will kill your application performance.
But assuming you really want to filter using Support collection, there is no where together with LIKE because you are just filtering an array. If you want to use something similar to like instead of:
$collection = $collection->where('name', $value);
you can use:
$collection = $collection->reject(function($element) use ($value) {
return mb_strpos($element->name, $value) === false;
});
depending on what you really have in collection instead of $element->name you might need to use $element['name']
I wonder is there a way to get an only item properties without a foreach loop. Since I have a query where in most of the cases there will be only one item in collection, and I need to change the status in the pivot table for only that case, I wonder is there some elegant way of doing this without the foreach loop. This is the case I am talking about:
$opponents = $quiz
->players()
->where('id', '!=', $player->id)
->get();
if ($opponents->count() < 2) {
$quiz->status = 'finished';
$quiz->save();
foreach ($opponents as $opponent) {
$quiz->players()->updateExistingPivot($opponent->id, ['status' => 'dropped']);
}
}
You can use the function first() like this:
$quiz->players()->updateExistingPivot($opponents->first()->id, ['status' => 'dropped']);
Say I have a user object (which belongsToMany groups) and I'm doing a whereIn with an array of their respected ids like so:
whereIn('user_id', $group->users->modelKeys())
I need to, however, set a condition where I only pull data from each array item based on a condition of the group_user pivot table, "created_at" (which is basically a timestamp of when that user was added to the group).
So I need something like this:
whereIn('user_id', $group->users->modelKeys())->whereRaw('visits.created_at > group_user.created_at')
That doesn't work though because it's not doing the whereRaw for each array item but it's doing it once for the query as a whole. I might need to do something like a nested whereIn but not sure if that'll solve it either. Thoughts?
My full query as it is now:
$ids = $group->users->modelKeys();
return DB::table('visits')->whereIn('user_id', function($query) use ($ids) {
$query->select('user_id')->from('group_user')->whereIn('group_user.user_id', $ids)->whereRaw('visits.created_at > group_user.created_at');
})->sum("views");
Ok got it to work using nested loops instead:
$visits = DB::table('visits')->whereIn('user_id', $group->users->modelKeys())->get();
$sum = 0;
foreach($group->users as $user) {
foreach($visits as $visit) {
if($visit->user_id == $user->id) {
if($visit->created_at >= $user->pivot->created_at) {
$sum += $visit->views;
}
}
}
}
return $sum;
Would still like to see if it's possible to do it in a single query, no array looping.
Solved it! The foreach loop approach was making calls take waaaay too long. Some queries had over 100k records returning (that's a lot to loop through) causing the server to hang up. The answer is in part a big help from Dharmesh Patel with his 3rd edit approach. The only thing I had to do differently was add a where clause for the group_id.
Here's the final query (returns that 100k results query in milliseconds)
//Eager loading. Has overhead for large queries
//$ids = $group->users->modelKeys();
//No eager loading. More efficient
$ids = DB::table('group_user')->where('group_id', $group->id)->lists('user_id');
return DB::table('visits')->join('group_user', function ($query) use ($ids) {
$query->on('visits.user_id', '=', 'group_user.user_id')->on('visits.created_at', '>=', 'group_user.created_at');
})->whereIn('group_user.user_id', $ids)->where('group_id', $group->id)->sum('views');
Have you considered using a foreach?
$users = whereIn('user_id', $group->users->modelKeys());
foreach ($users as $user) {
// do your comparison here
}
I guess you need to use JOINS for this query, following code may take you in right direction:
$ids = $group->users->modelKeys();
return DB::table('visits')->join('group_user', function ($query) use ($ids) {
$query->on('visits.user_id', '=', 'group_user.user_id')
->whereIn('group_user.user_id', $ids)
->whereRaw('visits.created_at > group_user.created_at');
})->sum("views");
EDIT
$ids = $group->users->modelKeys();
return DB::table('visits')->join('group_user', function ($query) use ($ids) {
$query->on('visits.user_id', '=', 'group_user.user_id');
})->whereIn('group_user.user_id', $ids)
->whereRaw('visits.created_at > group_user.created_at')->sum("views");
EDIT
$ids = $group->users->modelKeys();
return DB::table('visits')->join('group_user', function ($query) use ($ids) {
$query->on('visits.user_id', '=', 'group_user.id') // group_user.id from group_user.user_id as per the loop
->on('visits.created_at', '>=', 'group_user.created_at');
})->whereIn('group_user.user_id', $ids)
->sum("views");