hasOne subrelation not working in API

Hello,

The subRelations are not working on my app. They mix up the datas, however, it seems than my relations are good because when they are used as simple ones they have the proper datas.

I opened an issue about it because I’m pretty it’s a bug but it was closed, so for the examples and setting i’ll link you there : https://github.com/yiisoft/yii2/issues/18556

Thank you

Hi @omsi668,

While getContracts() is a standard hasMany definition of a relation, getLastContract() looks a bit strange to me because:

  1. It returns an ActiveRecord object instead of a ActiveQuery object. So it’s not a definition of a relation.
  2. It uses getContracts() method that defines a hasMany relation, but you seems to want this method to define a hasOne relation.

The following will work with your definition of getLastContract().

$candidates = Candidate::find()->all();
foreach ($candidates as $candidate) {
    echo $candidate->lastContract->name;
}

But I’m afraid the following may have some trouble.

$candidates = Candidate::find()->with(['lastContract'])->all();
...

It’s because getLastContract() doesn’t return an ActiveQeury object.

I’m not very sure, but I would try the following:

    public function getLastContract()
    {
        return $this->hasOne(
            Contract::class,
            ['candidate_id' => 'id']
        )->orderBy('contract.to DESC');
    }

Hello

Thank you for helping me.

I tried your solution but it doesn’t fix the bug, however it is way worse performance wise (I think we had that before and we changed it for optimization purpose).

I tried to remove the jobFunction relation to check if the ID is correct without it but it isn’t.

IMO jobFunction relation has nothing to do with the problem.

The key point of the problem is getLastContract() method and the way you are trying to use it. I mean, it doesn’t define a relation but you are using it as such.

Yes ok why not but even with “the right way” of doing it I still get the same bug so it doesn’t seem to be related with that as well

Sorry, but I also don’t know why. What I can do for now is just saying that it seems to be a bug on your side, since you haven’t given us much information.

Why don’t you give us the minimum code that reproduces your issue if you are sure it’s a bug on the framework side?

Well the problem is that I can’t show everything because it’s licensed code.

I thought I gave quite everything that was needed, but maybe the controller part will help.
It’s actually really simple it’s just a classic ActiveController.

We have a route like this :

public function actionCompleteIndex()
{
    $customerContact = $this->getCurrentCustomerContact();

    $searchModel = new CandidateCompleteFilter();
    $searchModel->customerContact = $customerContact;

    return $searchModel->search(\Yii::$app->request->queryParams, $this->getCurrentCustomerContact());
}

Here is the Filter :

public function search($params, CustomerContact $customerContact): ActiveDataProvider
{
    $this->load($params, '');

    $query = Candidate::find()
        ->where(['=', 'candidate.unavailable', 0])
        ->groupBy('candidate.id');

    $whereTo = 'WHERE customer_id = ' . $customerContact->customer_id . ' ';
    if (isset($this->contract_since)) {
        $to = (new \DateTime())->sub(new \DateInterval('P' . $this->contract_since . 'M'))->format('Y-m-d');
        $whereTo .= " AND `to` > '" . $to . "' ";
    }

    $query
            ->leftJoin("(SELECT *, row_number() OVER (PARTITION BY candidate_id ORDER BY `to` DESC) AS rn
                FROM contract
                $whereTo) lastContract", 'rn = 1 AND lastContract.candidate_id = candidate.id')
    ;

    $dataProvider = new ActiveDataProvider([
        'query' => $query,
        'pagination' => ['pageSize' => 50],
        'sort' => [
            'params' => array_merge(\Yii::$app->request->queryParams, ['#' => 'candidate']),
            'defaultOrder' => [
                'candidateFullName' => SORT_ASC,
            ],
            'attributes' => [
                'candidateFullName' => [
                    'asc' => ['candidateFullName' => new Expression('CONCAT_WS(" ", candidate.last_name, candidate.first_name) ASC')],
                    'desc' => ['candidateFullName' => new Expression('CONCAT_WS(" ", candidate.last_name, candidate.first_name) DESC')],
                ],
            ],
        ],
    ]);
    
    $query->andWhere('lastContract.id IS NOT NULL');
    $countQuery = clone $query;
    $countQuery->select('COUNT(*) as count');

    $dataProvider->setTotalCount($countQuery->createCommand()->query()->read()['count']);

    return $dataProvider;
}

(I know the query is a bit dirty but you do what you have to do for performances)
And then the Candidate object is just a Model extending the common one, removing some fields and adding lastContract as an extraField :

public function extraFields()
{
    return ['lastContract'];
} 

(and you already have the relations)

I don’t know what else i can give to you. The route where the bug appears is candidate/complete-index, so everything else is plain framework code for me. Thank you for helping tho

Hi @omsi668,

I have to say sorry in advance for my unkind words, but I don’t want to read your unprocessed current code fragments. It’s beyond my capacity to understand this kind of complicated code especially when they are scattered across different places.

What I want is a one-stop, simplified, minimum code that reproduces the issue. Could you take some time to make it?

One thing comes to my mind.

You are joining a sub query in the above and it has an alias of lastContract. Are you expecting this to populate the lastContract relation? If so, you are wrong.

The query result of the search method will not be used to populate the relation. lastContract relation will be populated later with another query using the definition of lastContract relation.

It could be the cause of the problem, I guess.

Well sorry you asked me for more code. And I did clean it up before giving it. So if you need anything else I don’t know

I’m not expecting the lastContract relation to populate my fields with the right datas when it’s returned, I need it to have some filters with it, then i’d expect the relation to be made with the getLastContract() relation.

Isn’t it supposed to work like that?

Ok nvm I found the bug. “lastContract” relation is wrong and actually returns the first one…

Thank you. Sorry for this time lost

1 Like

Oh, my … :sweat_smile:

Anyway, I believe that you should reconsider what I said in the first reply.