diff --git a/includes/cache-install.inc b/includes/cache-install.inc index 8bcf8b7..ae873ba 100644 --- a/includes/cache-install.inc +++ b/includes/cache-install.inc @@ -23,7 +23,7 @@ class DrupalFakeCache extends DrupalDatabaseCache implements DrupalCacheInterfac return array(); } - function set($cid, $data, $expire = CACHE_PERMANENT) { + function set($cid, $data, $expire = CACHE_PERMANENT, $tags = array()) { } function deletePrefix($cid) { diff --git a/includes/cache.inc b/includes/cache.inc index 9b60a7e..632367f 100644 --- a/includes/cache.inc +++ b/includes/cache.inc @@ -276,8 +276,14 @@ interface DrupalCacheInterface { * general cache wipe. * - A Unix timestamp: Indicates that the item should be kept at least until * the given time, after which it behaves like CACHE_TEMPORARY. + * @param $tags + * An array of tags to be stored with the cache item. These should normally + * identify objects used to build the cache item, which should trigger + * cache invalidation when updated. For example if a cached item represents + * a node, both the node ID and the author's user ID might be passed in as + * tags. For example array('node' => array(123), 'user' => array(92)). */ - function set($cid, $data, $expire = CACHE_PERMANENT); + function set($cid, $data, $expire = CACHE_PERMANENT, $tags = array()); /** * Delete an item from the cache. @@ -304,6 +310,15 @@ interface DrupalCacheInterface { function deletePrefix($prefix); /** + * Delete all cached items matching cache tags. + * + * @param $tags + * An array of tags grouped by namespace. The array follows the same format + * as that used to in DrupalCacheInterface::set(). + */ + function deleteTags($tags); + + /** * Flush all cache items in a bin. */ function flush(); @@ -437,7 +452,7 @@ class DrupalDatabaseCache implements DrupalCacheInterface { return $cache; } - function set($cid, $data, $expire = CACHE_PERMANENT) { + function set($cid, $data, $expire = CACHE_PERMANENT, $tags = array()) { $fields = array( 'serialized' => 0, 'created' => REQUEST_TIME, @@ -451,12 +466,27 @@ class DrupalDatabaseCache implements DrupalCacheInterface { $fields['data'] = $data; $fields['serialized'] = 0; } + $insert_values = array(); + foreach ($tags as $namespace => $values) { + foreach ($values as $value) { + $insert_values[] = array( + 'cid' => $cid, + 'namespace' => $namespace, + 'value' => $value, + ); + } + } try { db_merge($this->bin) ->key(array('cid' => $cid)) ->fields($fields) ->execute(); + $query = db_insert($this->bin . '_tags')->fields(array('cid', 'namespace', 'value')); + foreach ($insert_values as $record) { + $query->values($record); + } + $query->execute(); } catch (Exception $e) { // The database may not be available, so we'll ignore cache_set requests. @@ -485,6 +515,39 @@ class DrupalDatabaseCache implements DrupalCacheInterface { ->execute(); } + function deleteTags($tags) { + // MySQL does not use the primary key when performing subqueries with this + // syntax. However it supports deleting via JOINS, which is not ANSI SQL + // but allows the query to complete considerably faster. + // @see http://bugs.mysql.com/bug.php?id=9021 + // @todo: remove when this bug is fixed in a stable release of MySQL. + $connection = Database::getConnection('default'); + if ($connection->supportsJoinDML()) { + foreach ($tags as $namespace => $values) { + db_query('DELETE c FROM {' . db_escape_table($this->bin) . '} c INNER JOIN {' . db_escape_table($this->bin . '_tags') . '} t ON t.cid = c.cid AND t.value IN (:values) AND t.namespace = :namespace', array(':values' => $values, ':namespace' => $namespace)); + } + } + else { + foreach ($tags as $namespace => $values) { + // Delete the cids corresponding to the tags. + $subquery = db_select($this->bin . '_tags', 't'); + $subquery->addField('t', 'cid'); + $subquery->condition('t.value', $values); + $subquery->condition('t.namespace', $namespace); + + db_delete($this->bin) + ->condition('cid', $subquery, 'IN') + ->execute(); + } + // Delete the tags themselves. + db_delete($this->bin . '_tags') + ->condition('value', $values) + ->condition('namespace', $namespace) + ->execute(); + + } + } + function flush() { db_truncate($this->bin)->execute(); } diff --git a/includes/database/database.inc b/includes/database/database.inc index 6108614..5ea33f3 100644 --- a/includes/database/database.inc +++ b/includes/database/database.inc @@ -252,6 +252,15 @@ abstract class DatabaseConnection extends PDO { protected $transactionalDDLSupport = FALSE; /** + * Whether this database connection supports JOIN syntax for DML statements. + * + * Set to FALSE by default because few databases support this feature. + * + * @var bool + */ + protected $joinDMLSupport = FALSE; + + /** * An index used to generate unique temporary table names. * * @var integer @@ -1230,6 +1239,17 @@ abstract class DatabaseConnection extends PDO { } /** + * Determines if this driver supports JOIN syntax for DML statements. + * + * @return + * TRUE if this connection supports JOIN syntax for DML statments, FALSE + * otherwise. + */ + public function supportsJoinDML() { + return $this->joinDMLSupport; + } + + /** * Returns the name of the PDO driver for this connection. */ abstract public function databaseType(); diff --git a/includes/database/mysql/database.inc b/includes/database/mysql/database.inc index 7d5d859..0d77783 100644 --- a/includes/database/mysql/database.inc +++ b/includes/database/mysql/database.inc @@ -26,6 +26,9 @@ class DatabaseConnection_mysql extends DatabaseConnection { // MySQL never supports transactional DDL. $this->transactionalDDLSupport = FALSE; + // MySQL supports JOIN on DML statements. + $this->joinDMLSupport = TRUE; + $this->connectionOptions = $connection_options; // The DSN should use either a socket or a host/port. diff --git a/modules/simpletest/tests/cache.test b/modules/simpletest/tests/cache.test index 1235269..afcec5a 100644 --- a/modules/simpletest/tests/cache.test +++ b/modules/simpletest/tests/cache.test @@ -269,6 +269,37 @@ class CacheClearCase extends CacheTestCase { } /** + * Test clearing using cache tags. + */ + function testClearTags() { + $cache = cache($this->default_bin); + $cache->set('test_cid_clear1', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1))); + $cache->set('test_cid_clear2', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1))); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two cache items were created.')); + $cache->deleteTags(array('test_tag' => array(1))); + $this->assertFalse($this->checkCacheExists('test_cid_clear1', $this->default_value) + || $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Two caches removed after clearing a cache tag.')); + + $cache->set('test_cid_clear1', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(1))); + $cache->set('test_cid_clear2', $this->default_value, CACHE_PERMANENT, array('test_tag' => array(2))); + $cache->set('test_cid_clear3', $this->default_value, CACHE_PERMANENT, array('test_tag_foo' => array(3))); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value) + && $this->checkCacheExists('test_cid_clear3', $this->default_value), + t('Two cached items were created.')); + $cache->deleteTags(array('test_tag_foo' => array(3))); + $this->assertTrue($this->checkCacheExists('test_cid_clear1', $this->default_value) + && $this->checkCacheExists('test_cid_clear2', $this->default_value), + t('Cached items not matching the tag were not cleared.')); + + $this->assertFalse($this->checkCacheExists('test_cid_clear3', $this->default_value), + t('Cached item matching the tag was removed.')); + } + + /** * Test clearing using an array. */ function testClearArray() { diff --git a/modules/system/system.install b/modules/system/system.install index 6d80c43..2100d02 100644 --- a/modules/system/system.install +++ b/modules/system/system.install @@ -679,12 +679,51 @@ function system_schema() { ), 'primary key' => array('cid'), ); + $schema['cache_tags'] = array( + 'description' => 'Cache table for tracking cache tags related to the cache bin.', + 'fields' => array( + 'cid' => array( + 'description' => 'Unique cache ID.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'namespace' => array( + 'description' => "Tag namespace, such as 'node' or 'user.", + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ), + 'value' => array( + 'description' => "The cache tag value. For example a Node or User ID.", + 'type' => 'varchar', + 'length' => 64, + 'not null' => TRUE, + 'default' => '', + ), + ), + 'primary key' => array('cid', 'value', 'namespace'), + 'indexes' => array( + 'value_namespace' => array('value', 'namespace'), + ), + 'foreign_keys' => array( + 'cache_item' => array( + 'table' => 'cache', + 'columns' => array('cid' => 'cid'), + ), + ), + ); + $schema['cache_bootstrap'] = $schema['cache']; $schema['cache_bootstrap']['description'] = 'Cache table for data required to bootstrap Drupal, may be routed to a shared memory cache.'; $schema['cache_form'] = $schema['cache']; $schema['cache_form']['description'] = 'Cache table for the form system to store recently built forms and their storage data, to be used in subsequent page requests.'; $schema['cache_page'] = $schema['cache']; $schema['cache_page']['description'] = 'Cache table used to store compressed pages for anonymous users, if page caching is enabled.'; + $schema['cache_page_tags'] = $schema['cache_tags']; + $schema['cache_page_tags']['foreign_keys']['cache_item']['table'] = 'cache_page'; $schema['cache_menu'] = $schema['cache']; $schema['cache_menu']['description'] = 'Cache table for the menu system to store router information as well as generated link trees for various menu/page/user combinations.'; $schema['cache_path'] = $schema['cache'];