mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-08-28 13:36:12 +02:00
267 lines
8.8 KiB
Markdown
267 lines
8.8 KiB
Markdown
title: Partial Caching
|
|
summary: Cache SilverStripe templates to reduce database queries.
|
|
|
|
# Partial Caching
|
|
|
|
Partial caching is a feature that allows the caching of just a portion of a page.
|
|
|
|
:::ss
|
|
<% cached 'CacheKey' %>
|
|
$DataTable
|
|
...
|
|
<% end_cached %>
|
|
|
|
|
|
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:
|
|
|
|
:::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 %>
|
|
|
|
An additional global key is incorporated in the cache lookup. The default value for this is
|
|
`$CurrentReadingMode, $CurrentUser.ID`. This ensures that the current [api: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;
|
|
|
|
**mysite/_config/app.yml**
|
|
|
|
:::yaml
|
|
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 [api:DataObject]s - the most useful for us being the `max` aggregate.
|
|
|
|
For example, if we have a menu, we 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('SiteTree').max('LastEdited'), $List('SiteTree').count() %>
|
|
|
|
The cache for this will update whenever a page is added, removed or edited.
|
|
|
|
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() %>
|
|
|
|
<div class="notice" markdown="1">
|
|
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.
|
|
</div>
|
|
|
|
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.
|
|
|
|
## 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 = array('Slides' => 'Slide')` changes. In `PageController`:
|
|
|
|
:::php
|
|
|
|
public function SliderCacheKey() {
|
|
$fragments = array(
|
|
'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
|
|
|
|
In some situations it's more important to be fast than to always be showing the latest data. By constructing the cache
|
|
key to invalidate less often than the data updates you can ensure rendering time is constant no matter how often the
|
|
data updates.
|
|
|
|
For instance, if we show some blog statistics, but are happy having them be slightly stale, we could do
|
|
|
|
:::ss
|
|
<% cached 'blogstatistics', $Blog.ID %>
|
|
|
|
|
|
which will invalidate after the cache lifetime expires. If you need more control than that (cache lifetime is
|
|
configurable only on a site-wide basis), you could add a special function to your controller:
|
|
|
|
:::php
|
|
public function BlogStatisticsCounter() {
|
|
return (int)(time() / 60 / 5); // Returns a new number every five minutes
|
|
}
|
|
|
|
|
|
and then use it in the cache key
|
|
|
|
:::ss
|
|
<% 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.
|
|
|
|
Following on from the previous example, you might wish to only cache slightly-stale data if the server is experiencing
|
|
heavy load:
|
|
|
|
:::ss
|
|
<% cached 'blogstatistics', $Blog.ID if $HighLoad %>
|
|
|
|
|
|
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 %>
|
|
|
|
<div class="warning" markdown="1">
|
|
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.
|
|
</div>
|
|
|
|
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 %>
|
|
|
|
## Cache expiry
|
|
|
|
The default expiry for partial caches is 10 minutes. The advantage of a short cache expiry is that if you have a problem
|
|
with your caching logic, the window in which stale content may be shown is short. The disadvantage, particularly for
|
|
low-traffic sites, is that cache blocks may expire before they can be utilised. If you're confident that you're caching
|
|
logic is sound, you could increase the expiry dramatically.
|
|
|
|
**mysite/_config.php**
|
|
|
|
:::php
|
|
// Set partial cache expiry to 7 days
|
|
SS_Cache::set_cache_lifetime('cacheblock', 60 * 60 * 24 * 7);
|