Browse Source

Merge pull request #3536 from markstory/3.0-orm-cache-shell

3.0 orm cache shell
José Lorenzo Rodríguez 12 years ago
parent
commit
a3404fa85b

+ 134 - 0
src/Console/Command/OrmCacheShell.php

@@ -0,0 +1,134 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Console\Command;
+
+use Cake\Cache\Cache;
+use Cake\Console\Shell;
+use Cake\Datasource\ConnectionManager;
+
+/**
+ * ORM Cache Shell.
+ *
+ * Provides a CLI interface to the ORM metadata caching features.
+ * This tool is intended to be used by deployment scripts so that you
+ * can prevent thundering herd effects on the metadata cache when new
+ * versions of your application are deployed, or when migrations
+ * requiring updated metadata are required.
+ */
+class OrmCacheShell extends Shell {
+
+/**
+ * Build metadata.
+ *
+ * @param $name string
+ * @return boolean
+ */
+	public function build($name = null) {
+		$schema = $this->_getSchema();
+		if (!$schema) {
+			return false;
+		}
+		if (!$schema->cacheMetadata()) {
+			$this->_io->verbose('Metadata cache was disabled in config. Enabling to write cache.');
+			$schema->cacheMetadata(true);
+		}
+		$tables = [$name];
+		if (empty($name)) {
+			$tables = $schema->listTables();
+		}
+		foreach ($tables as $table) {
+			$this->_io->verbose('Building metadata cache for ' . $table);
+			$schema->describe($table, ['forceRefresh' => true]);
+		}
+		$this->out('<success>Cache build complete</success>');
+		return true;
+	}
+
+/**
+ * Clear metadata.
+ *
+ * @param $name string
+ * @return boolean
+ */
+	public function clear($name = null) {
+		$schema = $this->_getSchema();
+		if (!$schema) {
+			return false;
+		}
+		$tables = [$name];
+		if (empty($name)) {
+			$tables = $schema->listTables();
+		}
+		if (!$schema->cacheMetadata()) {
+			$this->_io->verbose('Metadata cache was disabled in config. Enabling to clear cache.');
+			$schema->cacheMetadata(true);
+		}
+		$configName = $schema->cacheMetadata();
+
+		foreach ($tables as $table) {
+			$this->_io->verbose(sprintf(
+				'Clearing metadata cache from "%s" for %s',
+				$configName,
+				$table
+			));
+			$key = $schema->cacheKey($table);
+			Cache::delete($key, $configName);
+		}
+		$this->out('<success>Cache clear complete</success>');
+		return true;
+	}
+
+/**
+ * Helper method to get the schema collection.
+ *
+ * @return false|\Cake\Database\Schema\Collection
+ */
+	protected function _getSchema() {
+		$source = ConnectionManager::get($this->params['connection']);
+		if (!method_exists($source, 'schemaCollection')) {
+			$msg = sprintf('The "%s" connection is not compatible with orm caching, ' .
+				'as it does not implement a "schemaCollection()" method.',
+				$this->params['connection']);
+			$this->error($msg);
+			return false;
+		}
+		return $source->schemaCollection();
+	}
+
+/**
+ * Get the option parser for this shell.
+ *
+ * @return \Cake\Console\ConsoleOptionParser
+ */
+	public function getOptionParser() {
+		$parser = parent::getOptionParser();
+		$parser->addSubcommand('clear', [
+			'help' => 'Clear all metadata caches for the connection. If a ' .
+				'table name is provided, only that table will be removed.',
+		])->addSubcommand('build', [
+			'help' => 'Build all metadata caches for the connection. If a ' .
+			'table name is provided, only that table will be cached.',
+		])->addOption('connection', [
+			'help' => 'The connection to build/clear metadata cache data for.',
+			'short' => 'c',
+			'default' => 'default',
+		])->addArgument('name', [
+			'help' => 'A specific table you want to clear/refresh cached data for.',
+			'optional' => true,
+		]);
+
+		return $parser;
+	}
+}

+ 13 - 0
src/Database/Connection.php

@@ -551,6 +551,19 @@ class Connection {
 	}
 
 /**
+ * Enables or disables metadata caching for this connectino
+ *
+ * Changing this setting will not modify existing schema collections objects.
+ *
+ * @param bool|string $cache Either boolean false to disable meta dataing caching, or
+ *   true to use `_cake_model_` or the name of the cache config to use.
+ * @return void
+ */
+	public function cacheMetadata($cache) {
+		$this->_config['cacheMetadata'] = $cache;
+	}
+
+/**
  * Sets the logger object instance. When called with no arguments
  * it returns the currently setup logger instance.
  *

+ 22 - 4
src/Database/Schema/Collection.php

@@ -60,7 +60,7 @@ class Collection {
 		$config = $this->_connection->config();
 
 		if (!empty($config['cacheMetadata'])) {
-			$this->cacheMetadata(true);
+			$this->cacheMetadata($config['cacheMetadata']);
 		}
 	}
 
@@ -86,14 +86,22 @@ class Collection {
  * Caching will be applied if `cacheMetadata` key is present in the Connection
  * configuration options. Defaults to _cake_model_ when true.
  *
+ * ### Options
+ *
+ * - `forceRefresh` - Set to true to force rebuilding the cached metadata.
+ *   Defaults to false.
+ *
  * @param string $name The name of the table to describe.
+ * @param array $options The options to use, see above.
  * @return \Cake\Database\Schema\Table Object with column metadata.
  * @throws \Cake\Database\Exception when table cannot be described.
  */
-	public function describe($name) {
+	public function describe($name, array $options = []) {
+		$options += ['forceRefresh' => false];
 		$cacheConfig = $this->cacheMetadata();
-		if ($cacheConfig) {
-			$cacheKey = $this->_connection->configName() . '_' . $name;
+		$cacheKey = $this->cacheKey($name);
+
+		if (!empty($cacheConfig) && !$options['forceRefresh']) {
 			$cached = Cache::read($cacheKey, $cacheConfig);
 			if ($cached !== false) {
 				return $cached;
@@ -132,6 +140,16 @@ class Collection {
 	}
 
 /**
+ * Get the cache key for a given name.
+ *
+ * @param string $name The name to get a cache key for.
+ * @return string The cache key.
+ */
+	public function cacheKey($name) {
+		return $this->_connection->configName() . '_' . $name;
+	}
+
+/**
  * Sets the cache config name to use for caching table metadata, or
  * disabels it if false is passed.
  * If called with no arguments it returns the current configuration name.

+ 1 - 1
tests/TestCase/Console/Command/CommandListShellTest.php

@@ -94,7 +94,7 @@ class CommandListShellTest extends TestCase {
 		$expected = "/\[.*TestPluginTwo.*\] example, welcome/";
 		$this->assertRegExp($expected, $output);
 
-		$expected = "/\[.*CORE.*\] bake, i18n, server, test/";
+		$expected = "/\[.*CORE.*\] bake, i18n, orm_cache, server, test/";
 		$this->assertRegExp($expected, $output);
 
 		$expected = "/\[.*app.*\] sample/";

+ 1 - 1
tests/TestCase/Console/Command/CompletionShellTest.php

@@ -112,7 +112,7 @@ class CompletionShellTest extends TestCase {
 		$this->Shell->runCommand(['commands']);
 		$output = $this->out->output;
 
-		$expected = "TestPlugin.example TestPluginTwo.example TestPluginTwo.welcome bake i18n server test sample\n";
+		$expected = "TestPlugin.example TestPluginTwo.example TestPluginTwo.welcome bake i18n orm_cache server test sample\n";
 		$this->assertEquals($expected, $output);
 	}
 

+ 167 - 0
tests/TestCase/Console/Command/OrmCacheShellTest.php

@@ -0,0 +1,167 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Test\TestCase\Console\Command;
+
+use Cake\Cache\Cache;
+use Cake\Console\Command\OrmCacheShell;
+use Cake\Datasource\ConnectionManager;
+use Cake\TestSuite\TestCase;
+
+/**
+ * OrmCacheShell test.
+ */
+class OrmCacheShellTest extends TestCase {
+
+/**
+ * Fixtures.
+ *
+ * @var array
+ */
+	public $fixtures = ['core.article', 'core.tag'];
+
+/**
+ * setup method
+ *
+ * @return void
+ */
+	public function setUp() {
+		parent::setUp();
+		$this->io = $this->getMock('Cake\Console\ConsoleIo');
+		$this->shell = new OrmCacheShell($this->io);
+
+		$this->cache = $this->getMock('Cake\Cache\CacheEngine');
+		$this->cache->expects($this->any())
+			->method('init')
+			->will($this->returnValue(true));
+		Cache::config('orm_cache', $this->cache);
+
+		$ds = ConnectionManager::get('test');
+		$ds->cacheMetadata('orm_cache');
+	}
+
+/**
+ * Teardown
+ *
+ * @return void
+ */
+	public function tearDown() {
+		parent::tearDown();
+		Cache::drop('orm_cache');
+
+		$ds = ConnectionManager::get('test');
+		$ds->cacheMetadata(false);
+	}
+
+/**
+ * Test build() with no args.
+ *
+ * @return void
+ */
+	public function testBuildNoArgs() {
+		$this->cache->expects($this->at(2))
+			->method('write')
+			->with('test_articles');
+
+		$this->shell->params['connection'] = 'test';
+		$this->shell->build();
+	}
+
+/**
+ * Test build() with one arg.
+ *
+ * @return void
+ */
+	public function testBuildNamedModel() {
+		$this->cache->expects($this->once())
+			->method('write')
+			->with('test_articles');
+		$this->cache->expects($this->never())
+			->method('delete');
+
+		$this->shell->params['connection'] = 'test';
+		$this->shell->build('articles');
+	}
+
+/**
+ * Test build() overwrites cached data.
+ *
+ * @return void
+ */
+	public function testBuildOverwritesExistingData() {
+		$this->cache->expects($this->once())
+			->method('write')
+			->with('test_articles');
+		$this->cache->expects($this->never())
+			->method('read');
+		$this->cache->expects($this->never())
+			->method('delete');
+
+		$this->shell->params['connection'] = 'test';
+		$this->shell->build('articles');
+	}
+
+/**
+ * Test build() with a non-existing connection name.
+ *
+ * @expectedException Cake\Datasource\Error\MissingDatasourceConfigException
+ * @return void
+ */
+	public function testBuildInvalidConnection() {
+		$this->shell->params['connection'] = 'derpy-derp';
+		$this->shell->build('articles');
+	}
+
+/**
+ * Test clear() with an invalid connection name.
+ *
+ * @expectedException Cake\Datasource\Error\MissingDatasourceConfigException
+ * @return void
+ */
+	public function testClearInvalidConnection() {
+		$this->shell->params['connection'] = 'derpy-derp';
+		$this->shell->clear('articles');
+	}
+
+/**
+ * Test clear() with no args.
+ *
+ * @return void
+ */
+	public function testClearNoArgs() {
+		$this->cache->expects($this->at(2))
+			->method('delete')
+			->with('test_articles');
+
+		$this->shell->params['connection'] = 'test';
+		$this->shell->clear();
+	}
+
+/**
+ * Test clear() with a model name.
+ *
+ * @return void
+ */
+	public function testClearNamedModel() {
+		$this->cache->expects($this->never())
+			->method('write');
+		$this->cache->expects($this->once())
+			->method('delete')
+			->with('test_articles');
+
+		$this->shell->params['connection'] = 'test';
+		$this->shell->clear('articles');
+	}
+
+}