Using cache dependencies and chains

Yii supports many cache backends, but what really makes Yii cache flexible is the dependency chaining support. There are situations when you cannot just simply cache data for an hour because the information cached can be changed at any time.

In this recipe, we will see how to cache a whole page and still always get fresh data when it is updated. The page will be dashboard type and will show the five latest articles added and a total calculated for an account. Note that an operation cannot be edited as it was added, but an article can.

Getting ready

  1. Install APC (http://www.php.net/manual/en/apc.installation.php).
  2. Generate a fresh Yii application by using yiic webapp.
  3. Set up a cache in the components section of protected/config/main.php as follows:
    'cache' => array(
      'class' => 'CApcCache',
    ),
  4. Set up and configure a fresh database.
  5. Execute the following SQL:
    CREATE TABLE `account` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`amount` decimal(10,2) NOT NULL,PRIMARY KEY (`id`));
    CREATE TABLE `article` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`title` varchar(255) NOT NULL,`text` text NOT NULL,PRIMARY KEY (`id`));
  6. Generate models for the account and article tables using Gii.
  7. Configure the db and log application components through protected/config/main.php, so we can see actual database queries. In the end, the config for these components should look like the following:
    'db'=>array(
      'connectionString' => 'mysql:host=localhost;dbname=test',
      'username' => 'root',
      'password' => '',
      'charset' => 'utf8',
    
      'schemaCachingDuration' => 180,
    
      'enableProfiling'=>true,
      'enableParamLogging' => true,
    ),
    'log'=>array(
      'class'=>'CLogRouter',
      'routes'=>array(
        array(
          'class'=>'CProfileLogRoute',
        ),
      ),
    ),
  8. Create protected/controllers/DashboardController.php as follows:
    <?php
    class DashboardController extends CController
    {
      public function actionIndex()
      {
        $db = Account::model()->getDbConnection();
        $total = $db->createCommand("SELECT SUM(amount) FROM account")->queryScalar();
    
        $criteria = new CDbCriteria();
        $criteria->order = "id DESC";
        $criteria->limit = 5;
        $articles = Article::model()->findAll($criteria);
    
        $this->render('index', array(
          'total' => $total,
          'articles' => $articles,
        ));
      }
    
      public function actionRandomOperation()
      {
        $rec = new Account();
        $rec->amount = rand(-1000, 1000);
        $rec->save();
    
        echo "OK";
      }
    
      public function actionRandomArticle()
      {
        $n = rand(0, 1000);
    
        $article = new Article();
        $article->title = "Title #".$n;
        $article->text = "Text #".$n;
        $article->save();
    
        echo "OK";
      }
    }
  9. Create protected/views/dashboard/index.php as follows:
    <h2>Total: <?php echo $total?></h2>
    <h2>5 latest articles:</h2>
    <?php foreach($articles as $article):?>
      <h3><?php echo $article->title?></h3>
      <div><?php echo $article->text?></div>
    <?php endforeach ?>
  10. Run dashboard/randomOperation and dashboard/randomArticle several times. Then, run dashboard/index and you should see a screen similar to the one shown in the following screenshot:
    Getting ready

How to do it...

Carry out the following steps:

  1. We need to modify the controller code as follows:
    class DashboardController extends CController
    {
      public function filters()
      {
        return array(
          array(
            'COutputCache +index',
            // will expire in a year
            'duration'=>24*3600*365,
            'dependency'=>array(
              'class'=>'CChainedCacheDependency',
              'dependencies'=>array(
                new CGlobalStateCacheDependency('article'),
                new CDbCacheDependency('SELECT id FROM account ORDER BY id DESC LIMIT 1'),
              ),
            ),
          ),
        );
      }
    
      public function actionIndex()
      {
        $db = Account::model()->getDbConnection();
        $total = $db->createCommand("SELECT SUM(amount) FROM account")->queryScalar();
    
        $criteria = new CDbCriteria();
        $criteria->order = "id DESC";
        $criteria->limit = 5;
        $articles = Article::model()->findAll($criteria);
    
        $this->render('index', array(
          'total' => $total,
          'articles' => $articles,
        ));
      }
      public function actionRandomOperation()
      {
        $rec = new Account();
        $rec->amount = rand(-1000, 1000);
        $rec->save();
        echo "OK";
      }
    
      public function actionRandomArticle()
      {
        $n = rand(0, 1000);
    
        $article = new Article();
        $article->title = "Title #".$n;
        $article->text = "Text #".$n;
        $article->save();
    
        Yii::app()->setGlobalState('article', $article->id);
    
        echo "OK";
      }
    }
  2. That's it! Now, after loading dashboard/index several times, you will get only one simple query, as shown in the following screenshot:
    How to do it...

Also, try to run either dashboard/randomOperation or dashboard/randomArticle and refresh dashboard/index after that. The data should change.

How it works...

In order to achieve maximum performance while doing minimal code modification, we use a full-page cache by using a filter as follows:

public function filters()
{
  return array(
    array(
      'COutputCache +index',
      // will expire in a year
      'duration'=>24*3600*365,
      'dependency'=>array(
        'class'=>'CChainedCacheDependency',
        'dependencies'=>array(
          new CGlobalStateCacheDependency('article'),
          new CDbCacheDependency('SELECT id FROM account ORDER BY id DESC LIMIT 1'),
        ),
      ),
    ),
  );
}

The preceding code means that we apply full-page cache to the index action. The page will be cached for a year and the cache will refresh if any of the dependency data changes. Therefore, in general, the dependency works as follows:

  • First time it gets the fresh condition data as described in the dependency. For example, by querying a database. This data is saved for future reference along with what's cached.
  • On subsequent requests it gets the fresh condition data according to dependency configuration and then compares it with the saved one.
  • If they are equal, uses what is cached.
  • If not, updates the cache with the fresh data, and saves the fresh dependency condition data for future reference.

In our case, two dependency types are used: global state and DB. Global state dependency uses data from Yii::app()->getGlobalState() to decide if we need to invalidate the cache while DB dependency uses the SQL query result for the same purpose.

The question that you have now is probably, "Why have we used DB for one case and global state for another?" That is a good question!

The goal of using the DB dependency is to replace heavy calculations and select a light query that gets as little data as possible. The best thing about this type of dependency is that we don't need to embed any additional logic in the existing code. In our case, we can use this type of dependency for account operations, but cannot use it for articles as the article content can be changed. Therefore, for articles, we set a global state named article to the current time, which basically means that we are scheduling cache invalidation:

Yii::app()->setGlobalState('article', time());

Note

Note that if we edit the article 100 times in a row and view it only after that, the cache will be invalidated and updated only once.

There's more...

In order to learn more about caching and using cache dependencies, refer to the following URLs:

See also

  • The Creating filters recipe in Chapter 8, Extending Yii
  • The Using controller filters recipe in Chapter 10, Security
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.66.156