Algolia Laravel Scout complex where clauses and eager loading - laravel

Since Laravel Scout doesn't support more complex where clauses than simple numeric comparisons.
I checked the source code and I found the following lines
if (!empty($models = $model->getScoutModelsByIds($builder, $modelKeys))) {
$instances = $instances->merge($models->load($searchable->getRelations($modelClass)));
}
The instances is what is returned from Algolia search, so for example the following search essentially returns the $instances variable.
Mode::search('something')->get();
the $model is the searchable model and the getScoutModelsByIds what It basically does is a query to the database like
public function getScoutModelsByIds(){
$model->whereIn('id', $modelKeys)->get();
}
I was wondering if I apply any kind of where clauses or addSelect, or with eager loading, on the model before actually retrieving the data from the database, is it a good idea ?
For example
$model->where('some condition')->whereIn('id', $modelKeys)->get();
and instead of using lazy loading
$instances = $instances->merge($models->load($searchable->getRelations($modelClass)));
use the with function before retrieving the data from db.
For example
$model->where('some condition')->whereIn('id', $modelKeys)->with('relationships')->get();

Related

How to access one relation inside another relation in laravel?

I have a query in which I have eagar loaded two models using with function like this:
ModelA::with(['relationB', 'relationC.relationC.A'])->where(condition)->get();
So, ModelA has two relations like this:
public function B(){ return $this->blongsTo(B::class);}
public function C(){ return $this->blongsTo(C::class);}
Now, my requirement is that I want to add a condition in B() function like this:
public function C() {
if($this->B->status) {
return $this->blongsTo(C::class)->withTrashed();
}
return $this->blongsTo(C::class);
}
But it return null on line this statement:
if($this->B->status)
Here is the error message
Trying to get property 'status' of non-object
My ultimate requirement is that using one relation function I want to fetch deleted records and non deleted based on the condition, but somehow it is not working.
My laravel application version is 7.30.4.
A relational function (such as your public function C()) works a bit of magic under the hood. This is because really it is designed to be called in a query way like you show already with the ::with(['relationB', ...]).
However, because of this, if you were to eager load C, then $this is not yet loaded as the full model, and therefore B is not defined (this is assuming that modelA always has a B relation). If you were to dd($this) while performing your query, you'd see that the result would be a model without any attributes.
Getting this to work from within a relational function (with the goal of eager loading) is very difficult. You're probably better off doing the logic elsewhere, with a second query for example. This is because within the relational function, there is no way to know who or what the potential target is. However, if you only use it after modelA is loaded, then it works without issues.
You can do some things with a whereHas, but then you'd still have to do 2 queries, or you can try and see if you can get it done with an SQL IF statement, but that will not result in a relation.

Is Laravel sortBy slower or heavier to run than orderBy?

My concern is that while orderBy is applied to the query, I'm not sure how the sortBy is applied?
The reason for using sortBy in my case is because I get the collection via the model (i.e. $user->houses->sortBy('created_at')).
I'm just concerned about the performance: is sortBy simply looping each object and sorting them?, or is Laravel smart enough to simply transform the sortBy into an orderBy executed within the original query?
You need orderBy in order to perform a SQL order.
$user->houses()->orderBy('created_at')->get()
You can also eager load the houses in the right order to avoid N+1 queries.
$users = User::with(['houses' => function ($query) {
return $query->orderBy('created_at');
}])->get();
$orderedHouses = $users->first()->houses;
The sortBy method is applied to the Collection so indeed, it will looping each objects.
The orderBy() method is much more efficient than the sortBy() method when querying databases of a non-trivial size / at least 1000+ rows. This is because the orderBy() method is essentially planning out an SQL query that has not yet run whereas the sortBy() method will sort the result of a query.
For reference, it is important to understand the difference between a Collection object and a Builder object in Laravel.
A builder object is, essentially, an SQL query that has not been run. In contrast, a collection is essentially an array with some extra functionality/methods added. Sorting an array is much less efficient than pulling the data from the DB in the correct format on the actual query.
example code :
<?php
// Plan out a query to retrieve the posts alphabetized Z-A
// This is still a query and has not actually run
$posts = Posts::select('id', 'created_at', 'title')->orderBy('title', 'desc');
// Now the query has actually run. $posts is now a collection.
$posts = $posts->get();
// If you want to then sort this collection object to be ordered by the created_at
timestamp, you *could* do this.
// This will run quickly with a small number or rows in the result,
// but will be essentially unusable/so slow that your server will throw 500 errors
// if the collection contains hundreds or thousands or objects.
$posts = $posts->sortBy('created_at');

Laravel retrieve only specific fields from each item of collection

I may be missing something extremely trivial, but is it possible to retrieve specific columns/fields from models when grabbing a collection rather then returning the entire item's fields?
Here is my query:
$items = Items::where('visible', true)->take(10)->get();
This obviously returns each item in there entirety, including unique id's, and other fields i dont want to be fetched... how can i refine this query to just select specific fields from the models?
Laravel Query Builder get() function receives array of columns which you need to fetch.
$items = Items::where('visible', true)->take(10)->get(['column_1', 'column_2']);
Use select() method to do this:
$items = Items::select(['column_1', 'column_2']'])->where('visible', true)->take(10)->get();
Source: Latavel Database Query Builder
Laravel Query Builder gives a huge flexibility to write this types of query.
You can use select(), get(), all() methods.
Items::where('visible', true)->take(10)->get('col_1', 'col_2');
OR
Items::select('col_1', 'col_2')->where('visible', true)->take(10)->get();
Items::select(['col_1', 'col_2'])->where('visible', true)->take(10)->get();

Faster pagination with Doctrine 2.1 EXTRA_LAZY associations

Doctrine 2.1 brings a new feature EXTRA_LAZY loading for associations: https://www.doctrine-project.org/projects/doctrine-orm/en/latest/tutorials/extra-lazy-associations.html
This feature creates a new method slice($offset, $length) to query just a page of the association and is very useful for pagination of large data sets.
However, behind the scene the SQL query uses the classic LIMIT XX OFFSET XX syntax which is slow for large data sets (https://www.eversql.com/faster-pagination-in-mysql-why-order-by-with-limit-and-offset-is-slow/)
Is there a way to use the pagination with a WHERE clause?
If not, how may I extend the instance of Doctrine\ORM\PersistentCollection to create a method sliceWithCursor($columnName, $cursor, $length)?
My main goal is to implement a faster pagination while using the very convenient magic of Doctrine for associations.
Thanks !
You can use the matching function of Doctrine\ORM\PersistentCollection, providing the criteria to filter, e.g.:
use Doctrine\Common\Collections\Criteria;
$group = $entityManager->find('Group', $groupId);
$userCollection = $group->getUsers();
$criteria = Criteria::create()
->where(Criteria::expr()->eq("birthday", "1982-02-17"))
->orderBy(array("username" => Criteria::ASC));
$birthdayUsers = $userCollection->matching($criteria);
matching() returns a Doctrine\ORM\LazyCriteriaCollection, if your association is defined as EXTRA_LAZY.
You can paginate with the latter:
$birthdayUsers->slice($offset, $length);
Using cursor pagination
In some cases, it is required to use cursor pagination. You could do this by extending Doctrine\ORM\PersistentCollection, as suggested:
use Doctrine\Common\Collections\Criteria;
public function sliceWithCursor($criteria, $cursorEntity, $limit) {
$orderBy = $criteria->getOrderings();
foreach ($orderBy as $columnName => $direction) {
if ($direction === Criteria::ASC) {
$criteria->andWhere(Criteria::expr()->gte($columnName, $cursorEntity->{$columnName}));
} else {
$criteria->andWhere(Criteria::expr()->lte($columnName, $cursorEntity->{$columnName}));
}
}
// exclude cursor entity from the results
$criteria->andWhere(Criteria::expr()->neq("id", $cursorEntity->id));
$criteria->setMaxResults($limit);
return $this->matching($criteria);
}
The idea of cursor based pagination is to use a result row as starting point, instead of an offset, and get the next rows. As stated at alternative for using OFFSET, the idea is to substitute offset, with conditions from the order by clause.

Laravel queryScope

I created a queryScope
public function scopeCtmpActive($query)
{
return $query->where('ctmp_active', 'y');
}
Then I replace following line
$customtemplates_collection = Auth::user()->customtemplates->where('ctmp_active', 'y')->sortByDesc('ctmp_id');
with
$customtemplates_collection = Auth::user()->customtemplates->ctmpActive()->sortByDesc('ctmp_id');
And I am getting following FatalErrorException
Call to undefined method Illuminate\Database\Eloquent\Collection::ctmpActive()
How am I suppose to use a query exception with a relationship?
As the name implies, "query" scopes are for reusable, common "query" constraints.
$customtemplates_collection = Auth::user()->customtemplates;
This returns a collection. You are getting all "customtemplates" that belong to the authenticated user. Then, Laravel is nice in that the Collection class allows for a nice way to filter out results, which is why the next part works:
$customtemplates_collection = Auth::user()->customtemplates->where('ctmp_active', 'y');
You are using PHP. Not MySQL. To emphasize, you are getting every single "customtemplates" that belongs to the user, and then using (PHP) Laravel Collection's where method to go through each one and filter out the results. You are not adding a where clause to the query. That's why the above works.
However, query scopes are for query constraints so they need to happen during the query, not after. What you probably want is something like this:
$customtemplates_collection = Auth::user()->customtemplates()->ctmpActive()->orderBy('ctmp_id', 'desc')->get();
When you add the paranthesis after customtemplates(), you are invoking the customtemplates method. In this case, I'm assuming it's a HasMany relationship so it'll return a HasMany instance. Then basically, it uses PHP's magic method (__call) to build the query builder so each method after that is essentially prepping the database query. Then, when you're finished building the query, you call get to fetch the results.

Resources