Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make public \yii\db\ActiveQuery::populateRelations #1154

Closed
mcd-php opened this issue Nov 7, 2013 · 15 comments
Closed

Make public \yii\db\ActiveQuery::populateRelations #1154

mcd-php opened this issue Nov 7, 2013 · 15 comments
Assignees
Milestone

Comments

@mcd-php
Copy link
Contributor

mcd-php commented Nov 7, 2013

I use it many times in controllers where array of models is fetched via object relation, not via query.

Drive-by questions (worth separate issues ?):

  1. Is it possible to load relations in cascaded manner, i.e Department has many JobPositions, JobPosition has one Person - how can i ask framework to load Department with() not only JobPositions but also Persons ? If not yet, what is the best way to implement it ?
  2. In *DataProvider, is it possible to load relations only on models what are to be viewed ?
@cebe
Copy link
Member

cebe commented Nov 7, 2013

waiting for #1097 before decision on this. Can you elaborate on the use case, maybe show some code?

  1. Is it possible to load relations in cascaded manner, i.e Department has many JobPositions, JobPosition has one Person - how can i ask framework to load Department with() not only JobPositions but also Persons ? If not yet, what is the best way to implement it ?

https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md#lazy-and-eager-loading
last example. you can call with() inside that function too.

  1. In *DataProvider, is it possible to load relations only on models what are to be viewed ?

When you have pagination enabled you should only load the models to be viewed so it should also only load their relations.

@mcd-php
Copy link
Contributor Author

mcd-php commented Nov 7, 2013

Can you elaborate on the use case, maybe show some code?

Example is OTOMH, quite contrived since actual buisness logic is under NDA:

class DepartmentController extends Controller {
    public function actionView() {
        $idDprt = Yii::$app->request->get('dprt');
        $obDprt = Department::find($idDprt);
        $arObJobPos = $obDprt->arObJobPos; // has relation getArJobPos()
        $qJobPos = JobPosition::createQuery();
        $qJobPos->populateRelations( // patched to be public by Phing
            $arObJobPos,
            array('obPerson')
        );

        // Now some daydream ..
        $qJobPos->populateRelationsDeep(
            $arObJobPos,
            array(
                'obPerson->arObNotifyChannel',
                'obPerson->arObCertificate',
            )
        );

        // And some more daydream
        $qDprt = new Department::find()
            ->where(array('id'=>$idDprt))
            ->with(array(
                'arObJobPos',
                'arObJobPos->obPerson',
                'arObJobPos->obPerson->arObNotifyChannel',
                'arObJobPos->obPerson->arObCertificate',
            ))
        ;
        $dpJobPos = new DeepLazyRelationActiveDataProvider(array(
            'query' => $qDprt,
        ));

        return $this->render('view',array(
            'obDprt' => $obDprt,
            // ...
        ));
    }
    // ...
}

When you have pagination enabled you should only load the models to be viewed

Apache Cassandra disagrees, with it's broken and near non-existent ORDER BY. So i use ArrayDataProvider a lot, and think of custom data provider first quering only id and sort columns, then sorting in memory and requesting full models on pagination only.

@cebe
Copy link
Member

cebe commented Nov 7, 2013

Also related to #1125. I'd really like to see your use case to make a decision on #1125. maybe that already solves your problem.

@ghost ghost assigned cebe Nov 7, 2013
@cebe
Copy link
Member

cebe commented Nov 7, 2013

What you want to achive should work like this:

class DepartmentController extends Controller {
    public function actionView() {
        $idDprt = Yii::$app->request->get('dprt');

        $qDprt = Department::find()
            ->where(array('id'=>$idDprt))
            ->with(array(
                'arObJobPos' => function($q) {
                    return $q->with(['obPerson' => function($q) {
                         return $q->with(['arObNotifyChannel', 'arObCertificate']);
                    }]);
                }))->one();
        $dpJobPos = new DeepLazyRelationActiveDataProvider(array(
            'query' => $qDprt,
        ));

        return $this->render('view',array(
            'obDprt' => $obDprt,
            // ...
        ));
    }
    // ...
}

@cebe
Copy link
Member

cebe commented Nov 7, 2013

Apache Cassandra disagrees, with it's broken and near non-existent ORDER BY. So i use ArrayDataProvider a lot, and think of custom data provider first quering only id and sort columns, then sorting in memory and requesting full models on pagination only.

How does the dataprovider know which records will be displayed and which not?

@mcd-php
Copy link
Contributor Author

mcd-php commented Nov 7, 2013

@cebe

What you want to achive should work like this:

Thank you very much, i will try immediately, this should go straight to documentation on relations !

How does the dataprovider know which records will be displayed and which not?

ArrayDataProvider - in prepareModels():

if ( $this->with ) {
    $obQueryDontKnowHowToCreate->populateRelations($models,$this->with);
}

The custom CassandraDataProvider will maybe look like this:

  1. Extract sorting fields from $this->query
  2. Construct another query with fields only enough to sort
  3. Run it getting array only, not hydrating models
  4. Create ArrayDataProvider with this array, sort and paginate with it
  5. With keys from 4, run full query, giving it with from original one
  6. Create another ArrayDataProvider with only full models
  7. Sort with it, not paginate this time
  8. return results in getModels() and getKeys()
  9. Recall the fact i have my custom ArrayDataProvider::sortModels and ArrayHelper::getValue (should i share them ?) to sort by nested/relation values, grab head with both hands, howl at moon.

@cebe
Copy link
Member

cebe commented Nov 7, 2013

Sounds like this is solvable when we merged #1097. You could even try to simulate orderBy in ActiveQuery class then.

@mcd-php
Copy link
Contributor Author

mcd-php commented Nov 7, 2013

@cebe
I just converted one action to nested relations, 3 level deep. Some questions:

  1. I have 140+ queries the old way and 150+ the new way in /app/runtime/debug data files (debug panel does not work for me, for some weird reason. Something missing in layout ?). But i have only 26 entities on my view !

  2. To shut off IDE coloring for 'unknown method', i typehint ActiveRelation $q. Isn't it a subtle error ?

  3. To avoid leaking model knoweledge into controllers, i packed the 3-deep array into MyModel::getWithForSomePurpose, and probably will cascade such hiding: Department::getWithForItemView calls JobPosition::getWithForRichList, calls Person::getWithForRichList.

Won't you come up with more elegant way of collecting this data ? Purpose-crafted relations with pre-made with() ? Inviting extra data loads or even attempt to pull all Db into memory ...

Also, what about partial views ? In this scenario, department/view.php probably has ListView(['view'=>'_itemJobPosition.php']) , in order calling partials for person, certificate and notifyChannel, all belonging to absolutely different controllers. Isn't this too much cohesion ?

Did Yii2 architects ever think of widgetizing models, giving them __toString() and setViewPartial(), so if i once write person/itemRichListWide.php, i can universally assign it in conroler and then call echo $person in view.

So, how would you combine that assigning of view with the process of building deep query ?

@iJackUA
Copy link
Contributor

iJackUA commented Nov 7, 2013

@cebe
don't you know maybe it is possible instead of this syntax

     $qDprt = Department::find()
            ->where(array('id'=>$idDprt))
            ->with(array(
                'arObJobPos' => function($q) {
                    return $q->with(['obPerson' => function($q) {
                         return $q->with(['arObNotifyChannel', 'arObCertificate']);
                    }]);
                }))->one();

to inroduce something like this ?

     $qDprt = Department::find()
            ->where(array('id'=>$idDprt))
            ->with([
                'arObJobPos' => [
                        'obPerson' => [
                                'arObNotifyChannel',
                                'arObCertificate'
                        ]
                    ]
                ])->one();

I understand that closure allows us to set any kind of addWhere etc. parameters also so it is really nice syntax
but in most cases you will just want to have automatic binding of all related entries without parametrisation.
So it culd be nice if such shortcut syntax was available (and if query customisaton will be needed for some relation - it could be specified as closure in Value, but if there is no value for relation key just suppose that only $q->with is required).

@mcd-php
as for 3) it looks like you have reinvented https://github.com/yiisoft/yii2/blob/master/docs/guide/active-record.md#scopes
and about coupling views to models - that's a little bit weird approach in terms of MVC. If you need - just create a custom widgets for each entity of your app and combine them dynamically based on contents of your Container Model (if you need to reuse partial templates - just place them in common subfolder of View, not per controller folder).

@qiangxue
Copy link
Member

qiangxue commented Nov 7, 2013

I added some doc about sub-relation: bc393ff

@iJackUA
Copy link
Contributor

iJackUA commented Nov 7, 2013

Great, thanks @qiangxue :)
now I have found how it works in normalizeRelations

@cebe
Copy link
Member

cebe commented Nov 7, 2013

Is there any point open in this issue? If not, please close.

@mcd-php
Copy link
Contributor Author

mcd-php commented Nov 8, 2013

@cebe So, what about actually make this method public ? If the array of rows is already here (no matter how it's created, maybe not simple one query), this seems to be the only way to populate relations, isn't it ?

@cebe
Copy link
Member

cebe commented Nov 8, 2013

I use it many times in controllers where array of models is fetched via object relation, not via query.

In case you ever encounter a case like this ActiveRelation::findWith() is much better suited for your case imo:

        $qDprt = Department::find()->all()
        $qDprt[0]->getRelation('arObJobPos')->findWith('arObJobPos', $dDprt);

if you have only one record you can use ActiveRecord::populateRelation():

        $qDprt = Department::find(1);
        $models = ...;
        $qDprt->populateRelation('arObJobPos', $models);

@qiangxue
Copy link
Member

Done: a2fe128

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants