Search code examples
phpmysqlpaginationyii

yii pagination issue trying to use 2 criterias


Disclaimer I'm self taught. Got my rudimentary knowledge of php reading forums. I'm an sql newb, and know next to nothing about yii.

I've got a controller that shows the products on our webstore. I would like the out of stock products to show up on the last pages.

I know I could sort by stock quantity but would like the in stock products to change order every time the page is reloaded.

My solution (probably wrong but kinda works) is to run two queries. One for the product that has stock, sorted randomly. One for the out of stock product also ordered randomly. I then merge the two resulting arrays. This much has worked using the code below (although I feel like there must be a more efficient way than running two queries).

The problem is that this messes up the pagination. Every product returned is listed on the same page and changing pages shows the same results. As far as I can tell the pagination only works for 1 CDbCriteria at a time. I've looked at the yii docs for CPagination for a way around this but am not getting anywhere.

        $criteria=new CDbCriteria;
        $criteria->alias = 'Product';
        $criteria->addCondition('(inventory_avail>0 OR inventoried=0)');
        $criteria->addCondition('Product.parent IS NULL');
        $criteria->addCondition('web=1');
        $criteria->addCondition('current=1');
        $criteria->addCondition('sell>sell_web');
        $criteria->order = 'RAND()';

        $criteria2=new CDbCriteria;
        $criteria2->alias = 'Product';
        $criteria2->addCondition('(inventory_avail<1 AND inventoried=1)');
        $criteria2->addCondition('Product.parent IS NULL');
        $criteria2->addCondition('web=1');
        $criteria2->addCondition('current=1');
        $criteria2->addCondition('sell>sell_web');
        $criteria2->order = 'RAND()';   

        $crit1=Product::model()->findAll($criteria);
        $crit2=Product::model()->findAll($criteria2);
        $models=array_merge($crit1,$crit2);                 

        //I know there is something wrong here, no idea how to fix it..
        $count=Product::model()->count($criteria);
        $pages=new CPagination($count);
        //results per page
        $pages->pageSize=30;
        $pages->applyLimit($criteria);

        $this->render('index', array(
        'models' => $models,
            'pages' => $pages
            ));

Clearly I am in over my head. Any help would be much appreciated.

Edit:

I figured that a third CDbCriteria that includes both the in stock and out of stock items could be used for the pagination (as it would include the same number of products as the combined results of the first 2). So I tried adding this (criteria1 and criteria2 remain the same):

        $criteria3=new CDbCriteria;
        $criteria3->alias = 'Product';
        //$criteria3->addCondition('(inventory_avail>0 OR inventoried=0)');
        $criteria3->addCondition('Product.parent IS NULL');
        $criteria3->addCondition('web=1');
        $criteria3->addCondition('current=1');
        $criteria3->addCondition('sell>sell_web');
        //$criteria3->order = 'RAND()';

        $crit1=Product::model()->findAll($criteria);
        $crit2=Product::model()->findAll($criteria2);
        $models=array_merge($crit1,$crit2);                 

        $count=Product::model()->count($criteria3);
        $pages=new CPagination($count);
        //results per page
        $pages->pageSize=30;
        $pages->applyLimit($criteria3);

        $crit1=Product::model()->findAll($criteria);
        $crit2=Product::model()->findAll($criteria2);
        $models=array_merge($crit1,$crit2); 

        $this->render('index', array(
        'models' => $models,
            'pages' => $pages
            ));  

I'm sure I'm missing something super obvious here... Been searching all day getting nowhere.


Solution

  • So you are running into what is IMO one of the potential drawbacks of natural language query builder frameworks. They can get your thinking on how you might approach a SQL problem going down a bad path when trying to work with the "out of the box" methods for building queries. Sometimes you might need to think about using raw SQL query capabilities that most every framework to provide in order to best address your problem.

    So let's start with the basic SQL for how I would suggest you approach your problem. You can either work this into your query builder style (if possible) or make a raw query.

    You could easily form a calculated field representing binary inventory status for sorting. Then also sort by another criteria secondarily.

    SELECT
        field1,
        field2,
        /* other fields */
        IF(inventory_avail > 0, 1, 0) AS in_inventory
    FROM product
    WHERE /* where conditions */
    ORDER BY
        in_inventory DESC, /* sort items in inventory first */
        other_field_to_sort ASC /* other sort criteria */
    LIMIT ?, ? /* pagination row limit and offset */
    

    Note that this approach only returns the rows of data you need to display. You move away from your current approach of doing a lot of work in the application to merge record sets and such.

    I do question use of RAND() for pagination purposes as doing so will yield products potentially appearing on one page after another as the user paginates through the pages, with other products perhaps not showing up at all. Either that or you need to have some additional complexity added to your applicatoin to somehow track the "randomized" version of the entire result set for each specific user. For this reason, it is really unusual to see order randomization for paginated results display.

    I know you mentioned you might like to spike out a randomized view to the user on a "first page". If this is a desire that is OK, but perhaps you decouple or differentiate that specific view from a wider paginated view of the product listing so as to not confuse the end user with a seemingly unpredictable pagination interface.

    In your ORDER BY clause, you should always have enough sorting conditions to where the final (most specific) condition will guarantee you a predictable order result. Oftentimes this means you have to include an autoincrementing primary key field, or similar field that provides uniqueness for the row.

    So let's say for example I had the ability for user to sort items by price, but you still obviously wanted to show all inventoried items first. Now let's say you have 100K products such that you will have many "pages" of products with a common price when ordered by price

    If you used this for ordering:

    ORDER BY in_inventory DESC, price ASC
    

    You could still have the problem of a user seeing the same product repeated when navigating between pages, because a more specific criteria than price was not given and ordering beyond that criteria is not guaranteed.

    You would probably want to do something like:

    ORDER BY in_inventory DESC, price ASC, unique_id ASC
    

    Such that the order is totally predictable (even though the user may not even know there is sorting being applied by unique id).