Filter on quantity of related table rows

I have two models Job and JobApplication, in a particular view I would like to present all the jobs with their corresponding number of applications with a CGridView, but I can’t figure out how to make it filter the number of applications.

The problem is how the actual CDbCriteria (more specifically the last call of compare in the search() function) should look like, I’ve added a attribute which is numApplications in order to get the input from the gridview, below I have some code from the Job model.


public $numApplications;


...


public function relations()

{

	return array(

		'jobApplications' => array(self::HAS_MANY, 'JobApplication', 'jobId'),

	);

}[




...


public function search()

{

	$criteria = new CDbCriteria;

	$criteria -> compare('id', $this -> id, true);

	$criteria -> compare('title', $this -> title, true);

	$criteria -> compare('created', $this -> created, true);

	$criteria -> compare('vacancies', $this -> vacancies);

	$criteria -> compare('published', $this -> published);

	$criteria -> compare('', $this -> numApplications);


	return new CActiveDataProvider('Job', array(

		'criteria' => $criteria,

	));

}

you can try adding a statistical relation -


public function relations()

{

	return array(

		'jobApplications' => array(self::HAS_MANY, 'JobApplication', 'jobId'),

                'numApplications'=>array(self::STAT, 'JobApplication', 'jobId'),	);

}

Tried your suggestion but without success, is there any special way to add a STAT relation into a compare function? How would you’ve done?

How did you tried to use stat relation ?

Did you set ‘with’ attribute in CDbcriteria to include your stat relation?

This is how I try to apply it.


public function search(){

	$criteria = new CDbCriteria;

	$criteria -> compare('id', $this -> id, true);

	$criteria -> compare('title', $this -> title, true);

	$criteria -> compare('created', $this -> created, true);

	$criteria -> compare('vacancies', $this -> vacancies);

	$criteria -> compare('numApplications', $this -> auxNumApplications);

	$criteria -> compare('published', $this -> published);

	$criteria -> with = array('numApplications');

	$criteria -> together = true;


	return new CActiveDataProvider('Job', array(

		'criteria' => $criteria,

		'pagination' => array('pageSize' => 5),

	));

}

At the moment, “numApplications” doesn’t return valid number or just filtering doesn’t work? If the latter, then make sure “auxNumApplications” is safe in your search scenario in rules.

Sorry but the numApplications is correctly defined, it’s nothing wrong with the filtering and the aux attribute is safe. After reading this thread I’m doubting that there is a solution that could take direct usage of a statistical query like this.

If you’ve got your suggestion work in any other context, please contribute with the code so I could take a look.

Anyone having some ideas?

If sorting is enough there are some working solutions. I prefer this one: http://www.yiiframework.com/forum/index.php?/topic/9400-sorting-dataprovider-on-statistical-query-relation/page__p__79691#entry79691 (altough I think that points and user_points are mixed up in the text).

The main "trick" is to add a custom CSort instance to your dataProvider, so that yii knows how to sort your column.

Translated to your example this would be:




public function relations() {

    return array(

        'jobApplications' => array(self::HAS_MANY, 'JobApplication', 'jobId'),

        'numApplications'=>array(self::STAT, 'JobApplication', 'jobId'),

    );

}

[...]

public function search(){

    $criteria = new CDbCriteria;

    #the following lines need to be executed before the first compare() to numApplications

    $criteria->with = array('numApplications');

    $criteria->together = true;

    $criteria->group = "t.id";

    $criteria->compare('id', $this -> id, true);

    [...]

    $criteria->compare('numApplications', $this->numApplications, true);




    $sort = array('attributes'=>array(

        'numApplications'=>array(

            'asc'=>'numApplications',

            'desc'=>'numApplications DESC',

            'default'=>'desc',

        ),

        '*',

    ));

    return new CActiveDataProvider(get_class($this), array(

        'criteria'=>$criteria,

        'sort' => $sort

    }



Filtering is a bit trickier. AFAIK you cannot use aggregate functions directly in a WHERE statement, so the following won’t work:




SELECT t.* FROM Job AS t, COUNT(j.id) AS numApplications

    LEFT JOIN JobApplications AS j ON (j.job_id = t.id) WHERE (numApplications > 5)



but this will (if I didn’t make a mistake):




SELECT t.* FROM Job AS t WHERE (SELECT COUNT(j.id) FROM JobApplications AS j WHERE (j.job_id = t.id) > 5



So now you have to force yii into creating a statement of the lower type. This is needed in two places: count() (used by CGridView to determine the number of results - for pagination and similar things) and findAll() (used to retrieve the results). So you need to add three functions to your Model class:





    /**

     * Override CActiveRecord count to allow filtering for a result of 

     *  a SQL aggregate function.

     */

    public function count($condition='',$params=array()){

        $this->fixCondition($condition);

        return parent::count($condition,$params);

    }

    

    /**

     * Override CActiveRecord findAll to allow filtering for a result of 

     *  a SQL aggregate function.

     */

    public function findAll($condition='',$params=array()){

        $this->fixCondition($condition);

        return parent::findAll($condition, $params);

    }

    private function fixCondition($condition){

        $column = 'bewertungAvg';

        $replacement = '(SELECT ROUND(AVG(b.bewertung),0) FROM bewertung AS b WHERE b.idee_fk = t.id)';

        $sqlSeparator = ' AND ';

        if(!is_object($condition) || 

            strpos($condition->condition, $column) === false){

            return;

        }

        $parts = explode($sqlSeparator, $condition->condition);

        $compare = ''; 

        foreach($parts as $id => $part){

            $findpos = strpos($part, $column);

            if($findpos !== false){ #remove condition that uses the non existent column

                $startpos = $findpos + strlen($column);

                $endpos = strpos($part, ')'); #check for closing bracket

                if($endpos === false){

                    $compare = substr($part, $startpos);

                } else { #remove closing bracket

                    $compare = substr($part, $startpos, $endpos-$startpos);

                }

                unset($parts[$id]);

                break;

            }

        }

        $parts[] = $replacement.$compare; #replace the removed condition

        $condition->condition = join($parts, $sqlSeparator);

    }



I might have made some mistakes trying to apply my code to your model, but other than that this should work.

HTH

jak

<edit>small bugfix in fixCondition (filter did not work when statement was not in brackets)</edit>

<edit>Clarification in search()</edit>