Merge pull request #9669 from creative-commoners/pulls/4/docs-template-partial-cache

DOC Update Partial Template Cache documentation
This commit is contained in:
Steve Boyd 2020-09-08 11:14:03 +12:00 committed by GitHub
commit 1a0c80ccdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 443 additions and 217 deletions

View File

@ -1,6 +1,6 @@
---
title: Caching
summary: Reduce rendering time with cached templates and understand the limitations of the ViewableData object caching.
summary: How template variables are cached.
icon: rocket
---
@ -35,13 +35,11 @@ When we render `$Counter` to the template we would expect the value to increase
## Partial caching
Partial caching is a feature that allows the caching of just a portion of a page. Instead of fetching the required data
from the database to display, the contents of the area are fetched from a [cache backend](../performance/caching).
Partial caching is a feature that allows caching of a portion of a page as a single string value. For more details read [its own documentation](partial_template_caching).
Example:
```ss
<% cached 'MyCachedContent', LastEdited %>
$Title
<% cached $CacheKey if $CacheCondition %>
$CacheableContent
<% end_cached %>
```

View File

@ -0,0 +1,335 @@
---
title: Partial Template Caching
summary: Cache a section of a template Reduce rendering time with cached templates and understand the limitations of the ViewableData object caching.
icon: tags
---
## Partial template caching
Partial template caching is a feature that allows caching of rendered portions of templates. Cached content
is fetched from a [cache backend](../performance/caching), instead of being regenerated repeatedly.
### Base syntax
```ss
<% cached $CacheKey if $CacheCondition %>
$CacheableContent
<% end_cached %>
```
This is not a definitive example of the syntax, but it shows the most common use case.
[note]
See also [Complete Syntax definition](#complete-syntax-defintition) section
[/note]
The key parts are `$CacheKey`, `$CacheCondition` and `$CacheableContent`.
The following sections explain every one of them in more detail.
#### $CacheKey
Defines a unique key for the cache storage.
[warning]
Avoid heavy computations in `$CacheKey` as it is evaluated for every template render.
[/warning]
The formal definition is
- Optional list of template expressions delimited by comma
The syntax is
```ss
<% cached [$key1[, $key2[, ...[, $keyN]]]] ... %>
```
The final value is concatenated by the Template Engine into a string. When doing so, Template Engine
adds some extra values to the mix to make it more unique and prevent clashing between cache keys from
different templates.
Here is how it works in detail:
1. `SilverStripe\View\SSViewer::$global_key` hash
With the current template context, value of the `$global_key` variable is rendered into a string and hashed.
`$global_key` content is inserted into the template "as is" at the compilation stage. Changing its value
won't have any effect until template recompilation (e.g. on cache flush).
By default it equals to `'$CurrentReadingMode, $CurrentUser.ID'`.
This ensures the current [Versioned](api:SilverStripe\Versioned\Versioned) state and user ID are used.
At runtime that will become something like `'LIVE, 0'` (for unauthenticated users in live mode).
As usual, you may override its value via YAML configs. For example:
```yml
# app/_config/view.yml
SilverStripe\View\SSViewer:
global_key: '$CurrentReadingMode, $CurrentUser.ID, $Locale'
```
2. Block hash
Everything between the `<% cached %> ... <% end_cached %>` is taken as text (with no rendering) and hashed.
This is done at the template compilation stage, so
the compiled version of the template contains the hash precalculated.
`Block hash` main purpose is to invalidate cache when template itself changes.
3. `$CacheKey` hash
All keys of `$CacheKey` are processed, concatenated and the final value is hashed.
If there are no values defined, this step is skipped.
4. Make the final key vaule
A string produced by concatenation of all the values mentioned above is used as the final value.
Even if `$CacheKey` is omitted, `SilverStripe\View\SSViewer::$global_key` and `Block hash` values are still
getting used to generate cache key for the caching backend storage.
[note]
##### Cache key calculated in controller
If your caching logic is complex or re-usable, you can define a method on your controller to generate a cache key
fragment.
For example, a block that shows a collection of rotating slides needs to update whenever the relationship
`Page::$many_many = ['Slides' => 'Slide']` changes. In `PageController`:
```php
public function SliderCacheKey()
{
$fragments = [
'Page-Slides',
$this->ID,
// identify which objects are in the list and their sort order
implode('-', $this->Slides()->Column('ID')),
// works for both has_many and many_many relationships
$this->Slides()->max('LastEdited')
];
return implode('-_-', $fragments);
}
```
Then reference that function in the cache key:
```ss
<% cached $SliderCacheKey if ... %>
```
[/note]
#### $CacheCondition
Defines if caching is required for the block.
Condition is optional and if omitted, `true` is implied.
If the value is `false`, the block skips `$CacheKey` evaluation completely, does not lookup
the data in the cache storage, neither preserve any data in the storage.
The template within the block keeps working as is, same as it would do without
`<% cached %>` block surrounding it.
Although `$CacheCondition` is optional, it is highly recommended. For example,
if you use `$DataObject->ID` as your `$CacheKey`, you may use
`$DataObject->ID > 0` as the condition.
Without it:
- your cache backend will always be queried for cache (for every template render)
- your cache backend may be cluttered with redundant and useless data
[warning]
The `$CacheCondition` value is evaluated on every template render and should be as lightweight as possible.
[/warning]
#### $CacheableContent
The content block may contain any usual template syntax.
### Cache storage
The cache storage may be re-configured via `Psr\SimpleCache\CacheInterface.cacheblock` key for [Injector](../extending/injector).
By default, it is initialised by `SilverStripe\Core\Cache\DefaultCacheFactory` with the following parameters:
- `namespace: "cacheblock"`
- `defaultLifetime: 600`
[note]
The defaultLifetime 600 means every cache record expires in 10 minutes.
If you have good `$CacheKey` and `$CacheCondition` implementations, you may want to tune these settings to
improve performance.
[/note]
### Nested cached blocks
Every nested cache block is processed independently.
Let's consider the following example:
```ss
<% cached $PageKey %>
<!-- Header -->
<% cached $BodyKey %> <!-- Body --> <% end_cached %>
<!-- Footer -->
<% end_cached %>
```
The template processor will transparently flatten the structure into something similar to the following pseudo-code:
```ss
<% cached $PageKey %><!-- Header --><% end_block %>
<% cached $BodyKey %><!-- Body --><% end_cached %>
<% cached $PageKey %><!-- Footer --><% end_cached %>
```
[note]
`$PageKey` is used twice, but evaluated only once per render because of [template object caching](caching/#object-caching).
[/note]
### Uncached
The tag `<% uncached %> ... <% end_uncached %>` disables caching for its content.
```ss
<% cached $PageKey %>
<!-- Header -->
<% uncached %><!-- Body --><% end_uncached %>
<!-- Footer -->
<% end_cached %>
```
Because of the nested block flattening (see above), it works seamlessly on any level of depth.
[warning]
The `uncached` block only works on the lexical level.
If you have a template that caches content rendering another template with included uncached blocks,
those will not have any effect on the parent template caching blocks.
[/warning]
### Nesting in FOR and IF blocks
Currently, a cache block cannot be included in `if` and `loop` blocks.
The template engine will throw an error letting you know if you've done this.
[note]
You may often get around this using aggregates or by un-nesting the block.
E.g.
```
<% cached $LastEdited %>
<% loop $Children %>
<% cached $LastEdited %>
$Name
<% end_cached %>
<% end_loop %>
<% end_cached %>
```
Might be re-written as something like that:
```
<% cached $LastEdited %>
<% cached $AllChildren.max('LastEdited') %>
<% loop $Children %>
$Name
<% end_loop %>
<% end_cached %>
<% end_cached %>
```
[/note]
### Unless (syntax sugar)
`if` keyword may be swapped with keyword `unless`, which inverts the boolean value evaluation.
The two following forms produce the same result
```ss
<% cached unless $Key %>
"unless $Cond" === "if not $Cond"
<% end_cached %>
```
```ss
<% cached if not $Key %>
"unless $Cond" === "if not $Cond"
<% end_cached %>
```
### Complete Syntax definition
```ss
<% [un]cached [$CacheKey[, ...]] [(if|unless) $CacheCondition] %>
$CacheContent
<% end_[un]cached %>
```
### Examples
```ss
<% cached %>
The key is: hash of the template code within the block with $global_key.
This content is always cached.
<% end_cache %>
```
```ss
<% cached $Key %>
Cached separately for every distinct $Key value
<% end_cached %>
```
```ss
<% cached $KeyA, $KeyB %>
Cached separately for every combination of $KeyA and $KeyB
<% end_cached %>
```
```ss
<% cached $Key if $Cond %>
Cached only if $Cond == true
<% end_cached %>
```
```ss
<% cached $Key unless $Cond %>
Cached only if $Cond == false
<% end_cached %>
```
```ss
<% cached $Key if not $Cond %>
Cached only if $Cond == false
<% end_cached %>
```
```ss
<% cached 'contentblock', $LastEdited, $CurrentMember.ID if $CurrentMember && not $CurrentMember.isAdmin %>
<!--
Hash of this content block is also included
into the final Cache Key value along with
SilverStripe\View\SSViewer::$global_key
-->
<% uncached %>
This text is always dynamic (never cached)
<% end_uncached %>
<!--
This bit is cached again
-->
<% end_cached %>
```

View File

@ -6,124 +6,107 @@ icon: tachometer-alt
# Partial Caching
Partial caching is a feature that allows the caching of just a portion of a page.
[Partial template caching](../templates/partial_template_caching) is a feature that allows caching of rendered portions a template.
## Cache block conditionals
Use conditions whenever possible. The cache tag supports defining conditions via either `if` or `unless` keyword.
Those are optional, however is highly recommended.
[warning]
Avoid performing heavy computations in conditionals, as they are evaluated for every template rendering.
[/warning]
If you cache without conditions:
- your cache backend will always be queried for the cache block (on every template render)
- your cache may be cluttered with heaps of redundant and useless data (especially the default filesystem backend)
As an example, if you use `$DataObject->ID` as a key for the block, consider adding a condition that ID is greater than zero:
```ss
<% cached 'CacheKey' %>
$DataTable
...
<% end_cached %>
<% cached $MenuItem.ID if $MenuItem.ID > 0 %>
```
Each cache block has a cache key. A cache key is an unlimited number of comma separated variables and quoted strings.
Every time the cache key returns a different result, the contents of the block are recalculated. If the cache key is
the same as a previous render, the cached value stored last time is used.
Since the above example contains just one argument as the cache key, a string (which will be the same every render) it
will invalidate the cache after a given amount of time has expired (default 10 minutes).
Here are some more complex examples:
To cache the contents of a page for all anonymous users, but dynamically calculate the contents for logged in members,
use something like:
```ss
<% cached 'database', $LastEdited %>
<!-- that updates every time the record changes. -->
<% end_cached %>
<% cached 'loginblock', $CurrentMember.ID %>
<!-- cached unique to the user. i.e for user 2, they will see a different cache to user 1 -->
<% end_cached %>
<% cached 'loginblock', $LastEdited, $CurrentMember.isAdmin %>
<!-- recached when block object changes, and if the user is admin -->
<% end_cached %>
<% cached unless $CurrentUser %>
```
An additional global key is incorporated in the cache lookup. The default value for this is
`$CurrentReadingMode, $CurrentUser.ID`. This ensures that the current [Versioned](api:SilverStripe\Versioned\Versioned) state and user ID are used.
This may be configured by changing the config value of `SSViewer.global_key`. It is also necessary to flush the
template caching when modifying this config, as this key is cached within the template itself.
For example, to ensure that the cache is configured to respect another variable, and if the current logged in
user does not influence your template content, you can update this key as below;
**app/_config/app.yml**
```yml
SilverStripe\View\SSViewer:
global_key: '$CurrentReadingMode, $Locale'
```
## Aggregates
Often you want to invalidate a cache when any object in a set of objects change, or when the objects in a relationship
change. To do this, SilverStripe introduces the concept of Aggregates. These calculate and return SQL aggregates
on sets of [DataObject](api:SilverStripe\ORM\DataObject)s - the most useful for us being the `max` aggregate.
Sometimes you may want to invalidate cache when any object in a set changes, or when objects in a relationship
change. To do this, you may use [DataList](api:SilverStripe\ORM\DataList) aggregate methods (which we call Aggregates).
These perform SQL aggregate queries on sets of [DataObject](api:SilverStripe\ORM\DataObject)s.
For example, if we have a menu, we want that menu to update whenever _any_ page is edited, but would like to cache it
Here are some useful methods of the [DataList](api:SilverStripe\ORM\DataList) class:
- `int count()` : Return the number of items in this DataList
- `mixed max(string $fieldName)` : Return the maximum value of the given field in this DataList
- `mixed min(string $fieldName)` : Return the minimum value of the given field in this DataList
- `mixed avg(string $fieldName)` : Return the average value of the given field in this DataList
- `mixed sum(string $fieldName)` : Return the sum of the values of the given field in this DataList
To construct a `DataList` over a `DataObject`, we have a global template variable called `$List`.
For example, if we have a menu, we may want that menu to update whenever _any_ page is edited, but would like to cache it
otherwise. By using aggregates, we do that like this:
```ss
<% cached
'navigation',
$List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'),
$List('SilverStripe\CMS\Model\SiteTree').count()
%>
```
The cache for this will update whenever a page is added, removed or edited.
[note]
The use of the fully qualified classname is necessary.
[/note]
[note]
The use of both `.max('LastEdited')` and `.count()` makes sure we check for any object
edited or deleted since the cache was last built.
[/note]
[warning]
Be careful using aggregates. Remember that the database is usually one of the performance bottlenecks.
Keep in mind that every key of every cached block is recalculated for every template render, regardless of caching
result. Aggregating SQL queries are usually produce more load on the database than simple select queries,
especially if you query records by Primary Key or join tables using database indices properly.
Sometimes it may be cheaper to not cache altogether, rather than cache a block using a bunch of heavy aggregating SQL
queries.
Let us consider two versions:
```ss
<% cached 'navigation', $List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'), $List('SilverStripe\CMS\Model\SiteTree').count() %>
# Version 1 (bad)
<% cached
$List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'),
$List('SilverStripe\CMS\Model\SiteTree').count()
%>
Parent title is: $Me.Parent.Title
<% end_cached %>
```
The cache for this will update whenever a page is added, removed or edited. (Note: The use of the fully qualified classname is necessary).
If we have a block that shows a list of categories, we can make sure the cache updates every time a category is added
or edited:
```ss
<% cached 'categorylist', $List('Category').max('LastEdited'), $List('Category').count() %>
# Version 2 (better performance than Version 1)
Parent title is: $Me.Parent.Title
```
[notice]
Note the use of both `.max('LastEdited')` and `.count()` - this takes care of both the case where an object has been
edited since the cache was last built, and also when an object has been deleted since the cache was last built.
[/notice]
`Version 1` always generates two heavy aggregating SQL queries for the database on every
template render.
`Version 2` always generates a single and more performant SQL query fetching the record by its Primary Key.
We can also calculate aggregates on relationships. The logic for that can get a bit complex, so we can extract that on
to the controller so it's not cluttering up our template.
[/warning]
## Cache key calculated in controller
If your caching logic is complex or re-usable, you can define a method on your controller to generate a cache key
fragment.
For example, a block that shows a collection of rotating slides needs to update whenever the relationship
`Page::$many_many = ['Slides' => 'Slide']` changes. In `PageController`:
```php
public function SliderCacheKey()
{
$fragments = [
'Page-Slides',
$this->ID,
// identify which objects are in the list and their sort order
implode('-', $this->Slides()->Column('ID')),
$this->Slides()->max('LastEdited')
];
return implode('-_-', $fragments);
}
```
Then reference that function in the cache key:
```ss
<% cached $SliderCacheKey %>
```
The example above would work for both a has_many and many_many relationship.
## Cache blocks and template changes
In addition to the key elements passed as parameters to the cached control, the system automatically includes the
template name and a sha1 hash of the contents of the cache block in the key. This means that any time the template is
changed the cached contents will automatically refreshed.
## Purposely stale data
@ -149,7 +132,7 @@ public function BlogStatisticsCounter()
}
```
and then use it in the cache key
@ -157,126 +140,36 @@ and then use it in the cache key
<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>
```
## Cache block conditionals
You may wish to conditionally enable or disable caching. To support this, in cached tags you may (after any key
arguments) specify 'if' or 'unless' followed by a standard template variable argument. If 'if' is used, the resultant
value must be true for that block to be cached. Conversely if 'unless' is used, the result must be false.
## Cache backend
Following on from the previous example, you might wish to only cache slightly-stale data if the server is experiencing
heavy load:
The template engine uses [Injector](../extending/injector) service `Psr\SimpleCache\CacheInterface.cacheblock` as
caching backend. The default definition of that service is very conservative and relies on the server filesystem.
This is the most common denominator for most of the applications out there. However,
this is not the most robust neither performant cache implementation. If you have a better solution
available on your platform, you should consider tuning that setting for your application.
All you need to do to swap the cache backend for partial template cache blocks is to redefine this service for the Injector.
Here's an example of how it could be done:
```ss
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
```yml
# app/_config/cache.yml
---
Name: app-cache
After:
- 'corecache'
---
SilverStripe\Core\Injector\Injector:
Psr\SimpleCache\CacheInterface.cacheblock: '%$App\Cache\Service.memcached'
```
By adding a `HighLoad` function to your `PageController`, you could enable or disable caching dynamically.
To cache the contents of a page for all anonymous users, but dynamically calculate the contents for logged in members,
use something like:
```ss
<% cached unless $CurrentUser %>
```
## Uncached
The template tag 'uncached' can be used - it is the exact equivalent of a cached block with an if condition that always
returns false. The key and conditionals in an uncached tag are ignored, so you can easily temporarily disable a
particular cache block by changing just the tag, leaving the key and conditional intact.
```ss
<% uncached %>
```
## Nested cache blocks
You can also nest independent cache blocks Any nested cache blocks are calculated independently from their containing
block, regardless of the cached state of that container.
This allows you to wrap an entire page in a cache block on the page's LastEdited value, but still keep a member-specific
portion dynamic, without having to include any member info in the page's cache key.
An example:
```ss
<% cached $LastEdited %>
Our wonderful site
<% cached $Member.ID %>
Welcome $Member.Name
<% end_cached %>
$ASlowCalculation
<% end_cached %>
```
This will cache the entire outer section until the next time the page is edited, but will display a different welcome
message depending on the logged in member.
Cache conditionals and the uncached tag also work in the same nested manner. Since Member.Name is fast to calculate, you
could also write the last example as:
```ss
<% cached $LastEdited %>
Our wonderful site
<% uncached %>
Welcome $Member.Name
<% end_uncached %>
$ASlowCalculation
<% end_cached %>
```
[note]
For the above example to work it is necessary to have the Injector service `App\Cache\Service.memcached` defined somewhere in the configs.
[/note]
[warning]
Currently a nested cache block can not be contained within an if or loop block. The template engine will throw an error
letting you know if you've done this. You can often get around this using aggregates or by un-nesting the block.
The default filesystem cache backend does not support auto cleanup of the residual files with expired cache records.
If your project relies on Template Caching heavily (e.g. thousands of cache records daily), you may want to keep en eye on the
filesystem storage. Sooner or later its capacity may be exhausted.
[/warning]
Failing example:
```ss
<% cached $LastEdited %>
<% loop $Children %>
<% cached $LastEdited %>
$Name
<% end_cached %>
<% end_loop %>
<% end_cached %>
```
Can be re-written as:
```ss
<% cached $LastEdited %>
<% cached $AllChildren.max('LastEdited') %>
<% loop $Children %>
$Name
<% end_loop %>
<% end_cached %>
<% end_cached %>
```
Or:
```ss
<% cached $LastEdited %>
(other code)
<% end_cached %>
<% loop $Children %>
<% cached $LastEdited %>
$Name
<% end_cached %>
<% end_loop %>
```