CakePHP's cache that wouldn't quit

I had the joy of debugging some unit tests the other day on AMO and ran into caching trouble. Turns out the bug for this was filed over a year ago, but I tested it in the latest build of Cake (1.1.18.5850) and it's still not fixed.

Cake's models have a Boolean variable called $cacheQueries which I misplaced my faith in early on. Turns out this does disable query caching...sometimes.

The test I was writing was pretty straight forward: Look in the database for some information to make sure it wasn't there, add the info, look in the database to make sure it was there. You can see the actual code, but this example is simplified:

1 <?
2    $this->Addon->cacheQueries = false; // Disable the built-in caching.
3    $ret = $this->Addon->query("SELECT addon_id FROM `addons_tags` WHERE addon_id={$this->testdata['addonid']}");
4    $this->assertEmpty($ret, 'Data exists!'); // this should be empty
5    $this->Addon->doSomething($this->testdata['addonid']);
6    $ret = $this->Addon->query("SELECT addon_id FROM `addons_tags` WHERE addon_id={$this->testdata['addonid']}");
7    $this->assertNotEmpty($ret, 'Data exists!'); // The array should have info in it
8 ?>

The problem is, line 6 always returned an empty array. I turned the DEBUG mode to 2 so Cake would print out all the queries it was doing and discovered it never actually made the second SELECT call. I smell query caching!

Let's dig through some CakePHP code to figure out what's up:

$this->Addon->query() is passed through AppModel::query() to Model::query() and eventually works it's way down to DboSource::query() with the same arguments. This is where things go south. Near the top of that function you'll see:

<?
    if (count($args) == 1) {
        return $this->fetchAll($args[0]);
?>

The function signature for DboSource::fetchAll() looks like:

<?
    function fetchAll($sql, $cache = true, $modelName = null)
?>

You can see the second parameter is a Boolean for caching and by default it's on. That's the fly in my clam chowder!

So, two ways around it:

The first way is to scroll up about 20 lines and look at the end of DboSource::query(). There is a handler there for a second parameter to your original query() function. Pass in false and voila, things just work. Of course, I didn't realize this until I was writing this post. I'm pretty sure it's not documented anywhere unless you look at the code.

The second way is to use execute() instead of query(). If you look at DboSource::execute() you'll see it suffers no caching and is as close to a direct SQL call as you can get.

The CakePHP manual has this to say about query() and execute():

Custom SQL calls can be made using the model's query() and execute() methods. The difference between the two is that query() is used to make custom SQL queries (the results of which are returned), and execute() is used to make custom SQL commands (which require no return value).

Apparently there are some other differences as well. (The fact that execute() returns results seems counter to what the manual suggests in the paragraph above but I may just be misinterpreting what they mean.) Regardless, if your tests are failing double check that you're not fighting the cache and save yourself some time.

6 Comments

I had the exact same problem as you did, and I couldn't fix it with the fixes you recommended above (which makes lots of sense, looking at the Cake source code).

In the end I just learned to live with it, since I couldn't fix it. I did stuff like having little arrays to 'cache' things myself etc etc. Ugly stuff. But works.

That's the thing with Cake. All good, until the magic fails :(
-- yihfeng, 27 Feb 2008
I had the same problem and it drove me nuts too.

Here's a working solution WITHOUT touching the cake-source-code...

- In your /config/database.php just change the database-driver-name to (i.e.) "mysql_fixed"
- Create a folder named "dbo" under your /models-directory and create the file "dbo_mysql_fixed.php"
- In this file, place the following code:



Note, that the classname is automatically translated with cake's Inflector-class - and be sure to include the right base-driver-class (in this example, it's mysql).

Now the default value of the parameter $cache is set to false...

Pwned! :-)
-- ChR1ZmO, 23 Oct 2008
Oops!

It seems, that this blog strips all "tags" - so my code-example is gone... Here's the code again, but now WITHOUT the php-brackets (be sure to place them around the code!)

-----

require_once(LIBS.'model'.DS.'dbo'.DS.'dbo_mysql.php');

class DboMysqlFixed extends DboMysql {
function fetchAll($sql, $cache = false, $modelName = null) {
return parent::fetchAll($sql, $cache, $modelName);
}
}

-- ChR1ZmO, 23 Oct 2008
Thanks mate, the $this->ModelName->query($sql,false) thing was what I was looking for ... Cake sucks as always.
-- A dude, 18 Jun 2009
I know this is quite an old post, but using Cake 1.2, this is still applicable.

Thanks for the post. I just ran into the exact same caching issue. I don't want to know how much longer it would have taken me to diagnose this issue if I hadn't run into your post.

Thanks!
-- Geoff, 18 Feb 2010
Still the same in Cake 2.0
$this->ModelName->query($sql,false); solves the problem. Thanks.
-- tsc, 15 Mar 2012

Post a comment

All comments are held for moderation; basic HTML formatting accepted.

Name: