2019-11-18 05:58:33 +01:00
|
|
|
---
|
2014-10-17 10:16:50 +02:00
|
|
|
title: Partial Caching
|
|
|
|
summary: Cache SilverStripe templates to reduce database queries.
|
2019-11-18 05:58:33 +01:00
|
|
|
icon: tachometer-alt
|
|
|
|
---
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2014-10-17 10:16:50 +02:00
|
|
|
# Partial Caching
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
[Partial template caching](../templates/partial_template_caching) is a feature that allows caching of rendered portions a template.
|
2011-02-07 07:48:44 +01:00
|
|
|
|
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
## Cache block conditionals
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
Use conditions whenever possible. The cache tag supports defining conditions via either `if` or `unless` keyword.
|
|
|
|
Those are optional, however is highly recommended.
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
[warning]
|
|
|
|
Avoid performing heavy computations in conditionals, as they are evaluated for every template rendering.
|
|
|
|
[/warning]
|
2017-08-03 02:51:32 +02:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
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)
|
2017-08-03 02:51:32 +02:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
As an example, if you use `$DataObject->ID` as a key for the block, consider adding a condition that ID is greater than zero:
|
2017-08-07 05:11:17 +02:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
```ss
|
|
|
|
<% cached $MenuItem.ID if $MenuItem.ID > 0 %>
|
2017-08-03 02:51:32 +02:00
|
|
|
```
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
To cache the contents of a page for all anonymous users, but dynamically calculate the contents for logged in members,
|
|
|
|
use something like:
|
2014-03-26 06:27:07 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
```ss
|
|
|
|
<% cached unless $CurrentUser %>
|
|
|
|
```
|
2014-03-26 06:27:07 +01:00
|
|
|
|
2014-10-17 10:16:50 +02:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
## Aggregates
|
2017-08-03 02:51:32 +02:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
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.
|
2014-03-26 06:27:07 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
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
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
To construct a `DataList` over a `DataObject`, we have a global template variable called `$List`.
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
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
|
2014-10-17 10:16:50 +02:00
|
|
|
otherwise. By using aggregates, we do that like this:
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2017-08-03 02:51:32 +02:00
|
|
|
```ss
|
2020-05-21 04:36:27 +02:00
|
|
|
<% cached
|
|
|
|
'navigation',
|
|
|
|
$List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'),
|
|
|
|
$List('SilverStripe\CMS\Model\SiteTree').count()
|
|
|
|
%>
|
2017-08-03 02:51:32 +02:00
|
|
|
```
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
The cache for this will update whenever a page is added, removed or edited.
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
[note]
|
|
|
|
The use of the fully qualified classname is necessary.
|
|
|
|
[/note]
|
2017-08-03 02:51:32 +02:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
[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]
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
[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.
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
Sometimes it may be cheaper to not cache altogether, rather than cache a block using a bunch of heavy aggregating SQL
|
|
|
|
queries.
|
2016-10-13 18:38:49 +02:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
Let us consider two versions:
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
```ss
|
|
|
|
# Version 1 (bad)
|
2017-08-03 02:51:32 +02:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
<% cached
|
|
|
|
$List('SilverStripe\CMS\Model\SiteTree').max('LastEdited'),
|
|
|
|
$List('SilverStripe\CMS\Model\SiteTree').count()
|
|
|
|
%>
|
|
|
|
Parent title is: $Me.Parent.Title
|
|
|
|
<% end_cached %>
|
2017-08-03 02:51:32 +02:00
|
|
|
```
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2017-08-03 02:51:32 +02:00
|
|
|
```ss
|
2020-05-21 04:36:27 +02:00
|
|
|
# Version 2 (better performance than Version 1)
|
|
|
|
|
|
|
|
Parent title is: $Me.Parent.Title
|
2017-08-03 02:51:32 +02:00
|
|
|
```
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
`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.
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
[/warning]
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-10-05 00:29:19 +02:00
|
|
|
[warning]
|
|
|
|
If you use the same aggregate in a template more than once, it will be recalculated every time
|
|
|
|
unless you move it out into a separate
|
|
|
|
[controller method](../templates/partial_template_caching/#cache-key-calculated-in-controller).
|
|
|
|
[Object Caching](../templates/caching/#object-caching) only works for single variables and not for chained expressions.
|
|
|
|
[/warning]
|
|
|
|
|
2011-02-07 07:48:44 +01:00
|
|
|
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
2017-08-03 02:51:32 +02:00
|
|
|
```ss
|
2017-10-27 04:38:27 +02:00
|
|
|
<% cached 'blogstatistics', $Blog.ID %>
|
2017-08-03 02:51:32 +02:00
|
|
|
```
|
2011-02-07 07:48:44 +01:00
|
|
|
|
|
|
|
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:
|
|
|
|
|
2017-08-03 02:51:32 +02:00
|
|
|
|
|
|
|
```php
|
2017-10-27 04:38:27 +02:00
|
|
|
public function BlogStatisticsCounter()
|
|
|
|
{
|
|
|
|
return (int)(time() / 60 / 5); // Returns a new number every five minutes
|
|
|
|
}
|
2017-08-03 02:51:32 +02:00
|
|
|
```
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
|
2011-02-07 07:48:44 +01:00
|
|
|
and then use it in the cache key
|
|
|
|
|
|
|
|
|
2017-08-03 02:51:32 +02:00
|
|
|
```ss
|
2017-10-27 04:38:27 +02:00
|
|
|
<% cached 'blogstatistics', $Blog.ID, $BlogStatisticsCounter %>
|
2017-08-03 02:51:32 +02:00
|
|
|
```
|
2011-02-07 07:48:44 +01:00
|
|
|
|
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
## Cache backend
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
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.
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
Here's an example of how it could be done:
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
```yml
|
|
|
|
# app/_config/cache.yml
|
2017-08-03 02:51:32 +02:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
---
|
|
|
|
Name: app-cache
|
|
|
|
After:
|
|
|
|
- 'corecache'
|
|
|
|
---
|
|
|
|
SilverStripe\Core\Injector\Injector:
|
|
|
|
Psr\SimpleCache\CacheInterface.cacheblock: '%$App\Cache\Service.memcached'
|
2017-08-03 02:51:32 +02:00
|
|
|
```
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2020-05-21 04:36:27 +02:00
|
|
|
[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]
|
2011-02-07 07:48:44 +01:00
|
|
|
|
2019-11-18 05:58:33 +01:00
|
|
|
[warning]
|
2020-05-21 04:36:27 +02:00
|
|
|
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.
|
2019-11-18 05:58:33 +01:00
|
|
|
[/warning]
|