I'm using Doctrine2 memcache (not memcacheD) in my Symfony 2.7 application. Here is my configuration:
doctrine:
orm:
metadata_cache_driver: memcache
result_cache_driver: memcache
query_cache_driver: memcache
I'm trying to delete the respective cache items on entity create/update/delete. As memcache doesn't support deletion using prefix, I could not find an easy solution to delete certain cache items of an entity on its CUD operations. Let's say I have a Property
entity, I want to delete all caches related to Property
on Property
create/update/delete. (I believe deleteAll()
is not a good idea in this case)
Deleting using namespace could be a workaround, but I think namespace can only be set on the cache driver, not on a particular entity or repository. How could it be possible to distinguish the namespaces for each entity?
So, I'm now invalidating the result cache using onFlush event (This is the only way I found and the article is too old back in 2012. Is there any better recent solution?). One drawback of that solution is to define each cache id for each query and define all of them in the service class CacheInvalidator
such as
namespace MyBundle\Listener\Event;
// ...
class CacheInvalidator
{
//...
protected function getCacheIds()
{
return array(
'MyBundle\Entity\BlogPost' => array(
array(
'id' => 'posts_for_user_%d',
'vars' => array(
array(
'value' => 'getUser',
'type' => 'method',
),
),
'change' => 'any',
),
),
// Add more entities etc here
);
}
}
The article author gave a very simple example using the cache id posts_for_user_%d
. It is fine using such simple query. The problem is that I have many queries using a lot of dynamic query parameters. For example, I have the following method in Property
repository which can be called with various options:
public function findRecommendations($options = null)
{
if (isset($options['limit'])) {
$limit = $options['limit'];
} elseif (is_numeric($options)) {
$limit = $options;
} else {
$limit = null;
}
$qb = $this->createQueryBuilder('p');
$qb->where('p.recommend_flag = :recommend_flag');
$qb->setParameter('recommend_flag', 1);
$qb->andWhere('DATE(p.expired_at) > :now');
$qb->setParameter('now', date('Y-m-d'));
$resultCacheParams = array(
'type' => array('%d', '%s'),
'value' => array(1, date('Ymd'))
);
if ($limit) {
$qb->setMaxResults($limit);
$resultCacheParams['type'][] = 'limit%d';
$resultCacheParams['value'][] = $limit;
}
$resultCacheId = vsprintf(
'Property_findRecommendations_'.implode('_', $resultCacheParams['type']),
$resultCacheParams['value']
);
$query = $qb->getQuery();
$query->useQueryCache(true);
$query->setQueryCacheLifetime(21600);
$query->useResultCache(true, 21600, $resultCacheId);
return $query->getResult();
}
So, I have to define two combinations of cache id in the service class - Property_findRecommendations_%d_%s
and Property_findRecommendations_%d_%s_limit%d
, one using limit and the other without limit.
protected function getCacheIds()
{
return array(
'MyBundle\Entity\Property' => array(
'id' => 'Property_findRecommendations_%d_%s',
'vars' => array(
array('value' => 1), // recommend_flag
array('value' => date('Ymd')),
),
'change' => 'any'
),
array(
'id' => 'Property_findRecommendations_%d_%s_limit%d',
'vars' => array(
array('value' => 1), // recommend_flag
array('value' => date('Ymd')),
array('value' => $this->container->getParameter('max_recommend_properties'))
),
'change' => 'any'
),
);
}
This is just for one parameter as an example. There will be more than one parameters to the method. More parameters, more combinations of cache id definitions in the service class. It is really expensive I think.
The worst case is queries with pagination that accept dynamic page
parameter. Each query result for each page should be cached using different cache id, for example Property_getAllProperties_%d
Property_getAllProperties_1 // for page 1
Property_getAllProperties_2 // for page 2
Property_getAllProperties_3 // for page 3
....
I can't know the current page number in the method getCacheIds()
of the service class. Should I calculate the number of pages of all properties and define array of cache Ids for every page number dynamically in the method getCacheIds()
?
I feel that it is something weird and seems like a workaround. In addition, I have the search queries that depends on various parameters. I think they are difficult to define in getCacheIds()
statically.
Is there a better and more graceful way to invalidate doctrine memcache result cache?
UPDATE
One possible solution I could think of is to create a new database table cache_key
and to save every cache id in that table. I have a function to generate cache id according to the query parameters. It returns a cache id in the format of custom_prefix
+ sha1(serialize($arrayOfParameters))
, e.g., Property_findRecommendations_xxxxxx
, Property_getAllProperties_xxxxxx
.
Whenever I call the two methods findRecommendations()
and getAllProperties()
, those cache ids would be inserted (using REPLACE
) into the table cache_key
as below:
-----------------------------------------------------
| entity | cache_id |
-----------------------------------------------------
| Property | Property_findRecommendations_xxxxxx |
| Property | Property_getAllProperties_xxxxxx |
-----------------------------------------------------
Then, I would be able to retrieve them in the CacheValidator
service class using the entity name Property
. One downside of this is that an extra INSERT/REPLACE
query has to be executed on every query run and an additonal SELECT
executed on every onFlush
event.
Nonetheless, I can't decide whether it is worth to do or not as all caches will be invalidated on every 6 hour according to the lifetime setting.