Does ActiveDataProvider preserves order of query ?

Hi,

I’m getting all the posts a user want to “read later” to show them in a list view. I want to show them in the order the user marked them “to read later”. I can show this list, but I can’t order it as I want.

I have a read_later table (relation many-to-many table between user and post).

this many-to-many table has a user_id column, a post_column AND an action_date column

I want my dataprovider to be ordre by this action_date.

Here how I do :

in my PostController :


    

    public function actionWillread($user_id)

    {

        ....


	$dataProvider = Post::getWillreadPostsOfUser($user);

 

        ....



In the post model, I have :




	static function getWillreadPostsOfUser($user)

	{

		$dataProvider = new ActiveDataProvider([

		    'query' => $user->getWillreadPosts()

		]);

		return $dataProvider;

	}



In the user model, I have :


 

    public function getWillreads()

    {

        //

        //  ORDER BY IS DEFINED HERE !!!

        //


        return $this->hasMany(WillReads::className(), ['user_id' => 'id'])->orderBy('action_date');

    }


    public function getWillreadPosts()

    {

    	

		return $this->hasMany(Post::className(), ['id' => 'post_id' ])

			->via('willreads');			

    }



It seems that ActiveDataProvider doesn’t preserve the order of the query.

Also, it seems the query doesn’t return the read_later.action_date column.

what to do ?

You have to sort it manually

here is an example for sorting, it uses gridview but it’s what you have to do just a few extra steps for the gridview that you don’t need.

sorting related data

thank you very much for your help, but I know very well this page, and I know very well how to perform that. It’s a very simple example, and it seems that it’s not my case.

If I do as indicated in the above pages (adding an ‘action_date’ criteria in PostSearch and a getActiondate in the Post Model) I think (but I’m maybe wrong) it means that it will join a second time the will_read table… It seems so dirty…

I’m trying to order my ActiveDataProvider from a joined table (via) coming from the query. Do you know if it’s possible ? Or just defining the query result order as the default ActiveDataProvider order ?

It will much more elegant, and easy, and simple, and intuitive :lol:

I’d rather extend the ActiveDataProvider class than using the search criteria trick…

But step by step, I first make a try with the method indicated in the documentation… and really, if it’s not possible to define the query order as the default data provider order… I’ll extend it. :P

well… I feel with stupid now :rolleyes:

the search function is the default one used by gii to generate the DataProvider for the views… I should have keep it.

Thanks you for the link to the page : reading again and again the docs and the wiki is always useful

Here how I did it. It works, but I’m not sure it respects the Yii “way of coding”. tell me what you think :

PostController :




public function actionWillread($will_user_id)

    {

        ...

		

        $searchModel = new PostSearch();

	$searchModel->scenario="will";

        $dataProvider = $searchModel->search(Yii::$app->request->queryParams);


        return $this->render('index', [

            'dataProvider' => $dataProvider,

        ]);



PostModel :




class Post extends \yii\db\ActiveRecord

{

    public function scenarios()

    {

    	

        return [

            'will' => ['WillUserId'],

        ];

    }	


    ...


    /**

     * @return \yii\db\ActiveQuery

     */

    public function getWills()

    {

        return $this->hasMany(Will::className(), ['post_id' => 'id']);

    }


    public function getWillUserId()

    {

         //

         //  Is that ok ? It seems me weird...

         //

         return Yii::$app->request->queryParams['will_user_id'];

    }


    ...




PostSearchModel :




class PostSearch extends Post

{

    ...

    public function scenarios()

    {

        $scenarios = parent::scenarios();

        return $scenarios;

    }

    ...


    public function search($params)

    {

        $query = Post::find();


        $dataProvider = new ActiveDataProvider([

            'query' => $query,

        ]);

   

       ....

       

       if($this->scenario == "will")

       {

	  $query->joinWith(['wills'])

	  ->where('will.user_id ='. $this->willUserId)

	  ->orderBy('action_date DESC');		

	}				

        return $dataProvider;



So, except the weird Post::getWillUserId() wich return the param, it seems me clean.

Do you think it’s a good way to do it ?

Sorry, I always want to code using the most framework functions as possible, and respecting convention as much as possible. but again, it works : so thanks again :lol:

Hi Louis Gac,

you don’t have to modify base model class Post at all. All changes should be made in PostSearch:




class PostSearch extends Post

{

    public $will_user_id;

    

    ...

    public function rules() {

        return [

   	     ...

   	     ['will_user_id', 'number', 'integerOnly'=>true, 'on'=>'will'], 

        ];

    }

    

    public function scenarios()

    {

        // bypass scenarios() implementation in the parent class

        return Model::scenarios();

    }

    ...


    public function search($params)

    {

        $query = Post::find();


        $dataProvider = new ActiveDataProvider([

   	     'query' => $query,

        ]);

   

   	....

   	

   	if($this->scenario == "will")

   	{

          $query->joinWith(['wills'])

          ->where('will.user_id ='. $this->will_user_id)

          ->orderBy('action_date DESC');       	     

        }                               

        return $dataProvider;



And don’t forget to make corresponding changes to the search form itself, because we just made

$will_user_id a new attribute of it:




...

    <?= Html::activeDropDownList($searchModel, 'will_user_id', $willUsersList); ?>



Hi mad, thanks for your answer.You won new questions :lol:

So, if I define inside the Search Model:




public function rules() {

        return [

             ...

             ['will_user_id', 'number', 'integerOnly'=>true, 'on'=>'will'], 

        ];

    }



I do not need to add scenarios inside the model ? Is that right ?

[b]

I’m not getting those Data Providers from a form, but rather from a link. That’s why I first used a simple hasMany relations (wich generate much less requests than a search).

[/b]

Now that I did it with the searchModel, I realize I should give the possibility to the user to choose himself the order (ASC or DESC), and the order_by (action, post publication, etc.). Also, all my controller code became much more dryer… So : much less code, much more functionalities for user, I understand now why Yii, via Gii code generation, incite all coders to use searchModel rather than model to build item lists.

ok, I finally understood why I had to put weird getters inside the model.

I’m having the datas from a get request and not from a post request. So the data didn’t load

[color=red]So, I just suppose that Yii search models with custom scenarios are just not made to be accessible via get request. Am I Right ? Or not ? [/color]

So, for what I was trying to do (… getting items from a hasMany relation ordered by a column from the relation table … called from a GET request… ), Search Model wasn’t the typical way to do it… neither than a usual model search, because data provider don’t keep $query order.

[b]So : If I’m not wrong, there is a design conception problem inside Yii. Or the search model with custom scenarios should be accessible via GET REQUEST, or an ActiveDataProvider in general should keep by default its query order.

[/b]

But, maybe I’m wrong, and I did something wrong, and Model Search with custom scenarios can be called from a Get request :lol: :lol: :lol:

By the way, I found a "trick" : reading the yii2/base/Model::load ; I understood I could do it parsing the get request inside a table

So, inside the Controller :




    private function _findPosts($scenario)

    {

        $searchModel = new PostSearch();

		$searchModel->scenario=$scenario;

		$getParams = Yii::$app->request->queryParams;

		$params["PostSearch"]    = $getParams; 

        $dataProvider = $searchModel->search($params);

        ...

    }



The tricky point being : $params["PostSearch"] = $getParams;

So, I think this should be documented. Maybe not all my point, but the point : "accessing a ModelSearch with custom scenarios from a GET request"

IMHO, would be nice to change the Model::load method.

Well, the Model::load() method is fine. You just need to look into its implementation and everything should be clear (look at the optional second parameter).

Here is more concise version of your controller action:




$searchModel = new PostSearch(['scenario'=>$scenario]);

$dataProvider = $searchModel->search(Yii::$app->request->queryParams, ''); 



Yii is really a very flexible and elegant framework! But to grasp all of its beauty, one needs to study its source code along with the documentation.

thanks. I’ve seen the second parameter in the load model… and it doesn’t work, neither your snippet. Your snippet is cute, and for sure, load() function was thinked to be used like that, with such a call from controller. But it doesn’t work

Here the signature of /yii/base/Model::load() :


public function load($data, $formName = null)

Here how it is called from \app\models\ModelSearch::search


public function search($params)

{

...

   $this->load($params);

}

Your snippet is giving to the search question a second parameter, that it just doesn’t accept.

[b]

$params : do you see it ?

$params : a single parameter, in search function, in load function.

[/b]

So, you just can’t give a second paramater to this function without changing the logic of \app\models\ModelSearch::search ; and the solution I gave is much easier.

I love yii, yii is beautiful, yii is neat, yii is my favourite framework, thank you guys for the great work.

[b]But, I repeat it : this problem is a conception problem.

The search method should test in a way or another if it has been called from a GET or a POST request : the snippet you provide would be a nice way to do if it worked. [/b]

Also, the Active Data Provider should keep the query order.

I submitted the issue to github, with a fix :

Ah indeed, my mistake there. Here is what I really wanted to write:

in controller action:




$searchModel = new PostSearch(['scenario'=>$scenario]);

$dataProvider = $searchModel->search(Yii::$app->request->queryParams);



in PostSearch:




public function search($params)

{

...

	$this->load($params, '');

}



And again, there is no problem with Yii. Default behavior of Model::load() is just optimized for POST form submissions, but the second parameter allows one to easily override the default behavior. I’d recommend you to close the issue on github, since all this is by design.

yeah : what I said : you change the logic of the search function generated by Gii…

by the way, with your modification, you just can’t any more call the search function from a POST request…

Well, the code generated by Gii is just a convenient starting point to develop concrete solutions. If you want to use the same method search() with GET and POST you can go the following way:




if ($this->scenario === 'will') {

	$this->load($params, '');

} else {

	$this->load($params);

}



The fixed I provide here is more general :




public function search($params)

{

...

if(isset$params['ModelSearch'])

{

$this->load($params);

}

else

{

$this->load($params, '');

}


}

That will work with any scenario.

It should be the code generated by Gii.

If anyone interested by this case, here a resume of how to parse GET parameters to a Search Model.

http://www.yiiframework.com/wiki/811/building-a-search-get-request-with-scenarios-calling-a-searchmodel-from-urls/

For the original question : getting items from a hasMany relation ordered by a column from the relation table (called from a GET request or not), I can provide a quick tuto too now if anyone interested.

To make your method more DRY’ish, do this way:




$classReflection = new \ReflectionClass(static::className());

if (isset($params[$classReflection->getShortName()])) {

	$this->load($params);

} else {

	$this->load($params, '');

}



ps: your wiki article has issues with the code samples, please review it.

it’s a tip, not an how to ; code sample are just here to give a global idea, not to tell what to do exactly step by step. And : wich issues ? I see no issue at all, so, if you see one, just tell it so I change it.

I think I’ll write a complete tutorial when I’ll have some time.

Regarding the wiki issue. Look at this snippet:




    	(isset$params['ModelSearch'])?$this->load($params):$this->load($params, '');