Limit the models used in find operations

By Matt Keller (tapter)
Often when issuing a find, findAll or other find*-variety there's way too much data returned which puts unneccessary overhead to your database server or it might pose a security risk when some crucial fields like passwords are available to the view designers just because the users-table was included in your findall...
For example you've got a User model which hasMany Article, hasOne ExtendedProfile.
If you want to get the User with id 1 and his ExtendedProfile in one step, for example using

PHP Snippet:

Download code <?php 
$this
->User->read(null'1');
?>

You'll also get all the associated Articles which you might not be interested at that stage and which also might put unnecessary strain to your mysql server. So you might only want to fetch the data from User and ExtendedProfile.

There's the famous unbindAll() function from CrazyLegs and Oth at http://othy.wordpress.com/tag/uncategorized/ which lets you ignore certain relations for the next query.
But this one is tedious to use because the whole relationship needs to be provided in the argument each time.
So I've modified that function to accept just a list of Model names as parameters which will be included in the find. All not-mentioned models are ignored (at least at recursion <= 1).

Important: You need to call useModel() before every find* function (if needed) because after the find has executed, the relationships are automatically restored to the default.


Example usage from a controller: Get only the data from the User and the ExtendedProfile, but ignore the Articles this user currently has.

PHP Snippet:

Download code <?php 
$this
->User->useModel( array('ExtendedProfile') );
$this->User->read(null'1'); // Find the entry with id=1
?>

Instead of an array of relation names a single relation name can be passed as string to the function.
The name used as argument(s) to useModel() is the name of the relation defined in your hasOne/hasMany/belongsTo/HABTM configuration. This usually corresponds to the model name but it can be different, for example if there is more than one relation between two models.

If you need to do a find with recursion greater than 1 and want to restrict the usage of the relationships in the other models you can use a useModel() call for each Model used.
For example:

PHP Snippet:

Download code <?php 
$this
->User->useModel( array("ExtendedProfile""Article") );
$this->User->Article->useModel(); // To stop recursion there if Article has further relations for recursion > 1
?>

Place this in your /app/app_model.php to make it available everywhere

Model Class:

Download code <?php 
  
function useModel($params = array())
  {
    if( !
is_array($params) )
      
$params = array($params);
    
    
$classname get_class($this); // for debug output
    
    
foreach($this->__associations as $ass)
    {
      if(!empty(
$this->{$ass}))
      {
        
// This model has an association '$ass' defined (like 'hasMany', ...)
        
        
$this->__backAssociation[$ass] = $this->{$ass};

        foreach(
$this->{$ass} as $model => $detail)
        {
          if(!
in_array($model,$params))
          {
            
//debug("Ignoring association $classname <i>$ass</i> $model... ");
            
$this->__backAssociation array_merge($this->__backAssociation$this->{$ass});
            unset(
$this->{$ass}[$model]);
          }

        }
          
      }
    }
    
    return 
true;
  }
?>

 

Comments 78

CakePHP Team Comments Author Comments
 

Comment

1 Even Better

This is a wonderful function, and has already sped up many of our pages. Just two quick comments:

First, I question the necessity of the array_merge line. It doesn't seem useful, and nothing has broken since I commented it out.

Second, I occasionally find myself forgetting to wrap multiple association names in an array() call. Therefore, I changed the first few lines to:

function useModel() {
$params = func_get_args();
if (is_array($params[0])) $params = $params[0];

...
Posted Mar 14, 2007 by Eric Wald
 

Comment

2 I needed a restoreModel function

I have used this method with few others, and I had a problem for one case. I do want to use only one model to make a query but in 1% of the time, depending of the data retreived I want to read the associated data (I wanted to avoid this for the 99% of cases). I had to write a restoreModel function that would restore one or more model previously "unBind" by the useModel call. Note that is relevant if the above case is used during the same "controller" call.
The function I wrote is there, it works for me, hope it can be usefull ..


// usefull for optimizing queries

function restoreModel($params = array())
{
if( !is_array($params) )
$params = array($params);

if(empty($params))
return;

$classname = get_class($this); // for debug output

foreach($this->__backAssociation as $name => $ass)
{
if(!empty($ass) && in_array($name, $this->__associations) && isset($this->{$name}) )
{
foreach($ass as $model => $detail)
{
if(in_array($model,$params) && !in_array($model, $this->{$name}))
{
$this->{$name}[$model] = $detail;
unset($this->__backAssociation[$name][$model]);

if(isset($this->__backAssociation[$model]))
unset($this->__backAssociation[$model]);
}
}
}
}

return true;
}
?>
Posted Mar 24, 2007 by franck