diff --git a/en/02_Developer_Guides/00_Model/02_Relations.md b/en/02_Developer_Guides/00_Model/02_Relations.md index 0d72b4119..927367287 100644 --- a/en/02_Developer_Guides/00_Model/02_Relations.md +++ b/en/02_Developer_Guides/00_Model/02_Relations.md @@ -784,6 +784,11 @@ The N + 1 query problem can be alleviated using eager loading which in this exam $teams = Team::get()->eagerLoad('Players'); ``` +> [!CAUTION] +> Manipulating the eager loading query is significantly (up to ~600%) faster than Filtering an `EagerLoadedlist` after the query has been made. +> +> See [manipulating eagerloading queries](#manipulating-eager-loading-queries). + With eager loading now only two queries will be executed: ```sql @@ -891,10 +896,61 @@ Eager loading supports all relationship types. > [!WARNING] > Eager loading is only intended to be used in read-only scenarios such as when outputting data on the front-end of a website. When using default lazy-loading, relationship methods will return a subclass of [`DataList`](api:SilverStripe\ORM\DataList) such as [`HasManyList`](api:SilverStripe\ORM\HasManyList). However when using eager-loading, an [`EagerLoadedList`](api:SilverStripe\ORM\EagerLoadedList) will be returned instead. `EagerLoadedList` has common methods such as `filter()`, `sort()`, `limit()` and `reverse()` available to manipulate its data, as well as some methods you'd expect on the various relation list implementations such as [`getExtraData()`](api:SilverStripe\ORM\EagerLoadedList::getExtraData()). + +### Manipulating eager loading queries + +There are some limitations to manipulating an `EagerLoadedList` (i.e. after the query has been executed). + +The main limitation is that filtering or sorting an `EagerLoadedList` will be done in PHP rather than as part of the database query, since we have already loaded all its relevant data into memory. + +> [!WARNING] +> `EagerLoadedList` can't filter or sort by fields on relations using dot notation (e.g. `sort('MySubRelation.Title')` won't work). > -> Note that filtering or sorting an `EagerLoadedList` will be done in PHP rather than as part of the database query, since we have already loaded all its relevant data into memory. -> -> Note also that `EagerLoadedList` can't filter or sort by fields on relations using dot notation (e.g. `sort('MySubRelation.Title')` won't work). +> Manipulating the eager loading query is significantly (up to ~600%) faster than Filtering an `EagerLoadedlist` after the query has been made. + +You can avoid those problems by applying manipulations such as filtering and sorting to the eager loading query as part of your call to `eagerLoad()`. + +You can pass an associative array into the `eagerLoad()` method, with relation chains as the keys and callbacks as the values. The callback accepts a `DataList` argument, and must return a `DataList`. + +> [!CAUTION] +> You can't manipulate the lists of `has_one` or `belongs_to` relations. This functionality is intended primarily as a way to pre-filter or pre-sort `has_many` and `many_many` relations. + +```php +use SilverStripe\ORM\DataList; + +$teams = Team::get()->eagerLoad([ + 'Players' => fn (DataList $list) => $list->filter(['Age:GreaterThan' => 18]), +]); +``` + +The list passed into your callback represents the query for that relation on *all* of the records you're fetching. For example, the `$list` variable above is a `DataList` that will fetch all `Player` records in the `Players` relation for all `Team` records (so long as they match the filter applied in the callback). + +Note that each relation in the relation chain (e.g. `Players`, `Players.Fans`, `Players.Fans.Events`) can have their own callback: + +```php +use SilverStripe\ORM\DataList; + +$teams = Team::get()->eagerLoad([ + 'Players' => fn (DataList $list) => $list->filter(['Age:GreaterThan' => 18]), + 'Players.Fans' => fn (DataList $list) => $list->filter(['Name:PartialMatch:nocase' => 'Sam']), + 'Players.Fans.Events' => fn (DataList $list) => $list->filter(['DollarCost:LessThan' => 200]), +]); +``` + +If you have complex branching logic, and in some branches you want to avoid pre-filtering or perform different filtering, you can declare a different callback (or `null`) for that relation chain by calling `eagerLoad()` again. Note that subsequent callbacks *replace* previous callbacks. + +```php +use SilverStripe\ORM\DataList; + +$teams = Team::get()->eagerLoad([ + // Remove any callback that was previously defined for this relation chain + 'Players' => fn (DataList $list) => null, + // Replace any callback that was previously defined for this relation chain. + // If you want to apply *additional* filters rather than replacing existing ones, you will + // need to declare the original filter again here. + 'Players.Fans.Events' => fn (DataList $list) => $list->filter(['DollarCost:GreaterThan' => 100]), +]); +``` ## Cascading deletions diff --git a/en/08_Changelogs/5.2.0.md b/en/08_Changelogs/5.2.0.md index a2cfb3f48..3477280d0 100644 --- a/en/08_Changelogs/5.2.0.md +++ b/en/08_Changelogs/5.2.0.md @@ -69,6 +69,35 @@ Check out [the linkfield documentation](/optional_features/linkfield/) which inc This release comes jampacked with new ORM features, granting you access to some new abstractions for more powerful and efficient queries. +#### Manipulating eager loaded relation queries {#eager-loading} + +Filtering or sorting an [`EagerLoadedList`](api:SilverStripe\ORM\EagerLoadedList) (i.e. after the eager loading query has been executed) requires filtering in PHP, which is less powerful and significantly slower than performing those manipulations on `DataList` before executing the query. For example, you can't filter or sort `EagerLoadedList` by fields on relations using dot notation (e.g. `sort('MySubRelation.Title')` won't work). + +To alleviate this problem, we've introduced a new syntax for eager loading relations that lets you manipulate the eager loading queries. + +The old syntax is still supported, because it can be used in templates for simple scenarios. + +In a test setup with looping through 100 records each with 100 related records (for a total of 10,000 records per test run), the following performance improvements were observed for different types of relations (early manipulations in the database vs manipulating results in PHP): + +- HasMany - ~581% faster (0.1080s vs 0.7358s) +- ManyMany - ~612% faster (0.1264s vs 0.9002s) +- ManyManyThrough - ~327% faster (0.2511s vs 1.0719s) + +You can pass an associative array into the [`DataList::eagerLoad()`](api:SilverStripe\ORM\DataList::eagerLoad()) method, with relation chains as the keys and callbacks as the values. The callback accepts a `DataList` argument, and must return a `DataList`. + +```php +use SilverStripe\ORM\DataList; + +$teams = Team::get()->eagerLoad([ + 'Players' => fn (DataList $list) => $list->filter(['Age:GreaterThan' => 18]), +]); +``` + +> [!WARNING] +> It is very important to remember to return the list in your callback function. + +There are a few edge cases to be aware of with this new feature. To learn more, see [eager loading](/developer_guides/model/relations/#eager-loading). + #### Multi-relational `has_one` relations Traditionally, if you wanted to have multiple `has_many` relations for the same class, you would have to include a separate `has_one` relation for *each* `has_many` relation.