19

I have three database table:

product (id, name)

product_has_adv (product,advantage,sort,important)

advantage (id, text)

In ProductModel I defined this:

public function getAdvantages()
    {
        return $this->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
            ->viaTable('product_has_advantage', ['product' => 'id']);
    }

I get the advantages without any problems.

But now I need to add a where product_has_advantage.important = 1 clausel and also sort the advantages by the sort-columen in the product_has_advantage-table.

How and where I have to realize it?

arogachev
  • 33,150
  • 7
  • 114
  • 117
rakete
  • 2,953
  • 11
  • 54
  • 108

8 Answers8

39

Using via and viaTable methods with relations will cause two separate queries.

You can specify callable in third parameter like this:

public function getAdvantages()
{
    return $this->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
        ->viaTable('product_has_advantage', ['product' => 'id'], function ($query) {
            /* @var $query \yii\db\ActiveQuery */

            $query->andWhere(['important' => 1])
                ->orderBy(['sort' => SORT_DESC]);
        });
}

The filter by important will be applied, but the sort won't since it happens in first query. As a result the order of ids in IN statement will be changed.

Depending on your database logic maybe it's better to move important and sort columns to advantage table.

Then just add condition and sort to the existing method chain:

public function getAdvantages()
{
    return $this->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
        ->viaTable('product_has_advantage', ['product' => 'id'])
        ->andWhere(['important' => 1])
        ->orderBy(['sort' => SORT_DESC]);
}
arogachev
  • 33,150
  • 7
  • 114
  • 117
  • This results in the following error: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'product_has_advantage.important' in 'where clause' The SQL being executed was: SELECT * FROM `advantages` WHERE (`product_has_advantage`.`important`=1) AND (`id` IN ('1', '2', '4', '14')) ORDER BY `product_has_advantage`.`sort` DESC – rakete Dec 30 '14 at 15:18
  • 2
    Why is this the accepted answer? I have this same issue (needing to sort on the table defined in 'viatable') and moving the columns to the other table is not acceptable. – Scott Jul 30 '15 at 20:12
  • ->orderBy(['sort' => SORT_DESC]); Only field to sort will bee "sort" – Ivan Pirus Jan 24 '17 at 10:11
7

Using viaTable methods with relations will cause two separate queries, but if you don't need link() method you can use innerJoin in the following way to sort by product_has_advantage table:

public function getAdvantages()
{
    $query = AdvantageModel::find();
    $query->multiple = true;
    $query->innerJoin('product_has_advantage','product_has_advantage.advantage = advantage.id');
    $query->andWhere(['product_has_advantage.product' => $this->id, 'product_has_advantage.important' => 1]);
    $query->orderBy(['product_has_advantage.sort' => SORT_DESC]);
    return $query;
}

Note than $query->multiple = true allows you to use this method as Yii2 hasMany relation.

Martín M
  • 141
  • 2
  • 2
  • 1
    Thank you for this. Personally I think this is much clearer, even if more verbose, with the added advantage that it avoids the hasMany... ->all() conundrum. – Rich Harding Dec 24 '20 at 20:50
  • Because of `'product_has_advantage.product' => $this->id` I think this solution does not work if you have something like `Product::find()->where(['id' => [1,2,3])->with('advantages')->all(); – robsch Mar 22 '22 at 11:33
4

Just for reference https://github.com/yiisoft/yii2/issues/10174 It's near impossible to ORDER BY viaTable() columns. For Yii 2.0.7 it returns set of ID's from viaTable() query, and final/top query IN() clause ignores the order.

4

For who comes here after a while and don't like above solutions, I got it working by joining back to the via table after the filter via table.

Example for above code:

public function getAdvantages()
{
    return $this->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
        ->viaTable('product_has_advantage', ['product' => 'id'])
        ->innerJoin('product_has_advantage','XXX')
        ->orderBy('product_has_advantage.YYY'=> SORT_ASC);
}

Take care about changing XXX with the right join path and YYY with the right sort column.

MIke
  • 41
  • 1
1

First you need to create a model named ProductHasAdv for junction table (product_has_adv) using CRUD.

Then create relation in product model and sort it:

  public function getAdvRels()
    {
        return $this->hasMany(ProductHasAdv::className(), ['product' => 'id'])->
        orderBy(['sort' => SORT_ASC]);;
    }

Then create second relationship like this:

public function getAdvantages()
{
    $adv_ids = [];
    foreach ($this->advRels as $adv_rel)
        $adv_ids[] = $adv_rel->advantage;
    return $this->hasMany(Advantage::className(), ['id' => 'advantage'])->viaTable('product_has_adv', ['product' => 'id'])->orderBy([new Expression('FIELD (id, ' . implode(',', $adv_ids) . ')')]);
}

This will sort final result using order by FIELD technique.

Don't forget to add:

use yii\db\Expression;

line to head.

Tural Ali
  • 22,202
  • 18
  • 80
  • 129
  • I think this is a real (and probably single) working solution for the [issue](https://github.com/yiisoft/yii2/issues/17166). In order to get $adv_ids you could simplify: `$adv_ids = $this->getAdvRels()->select('advantage')->column();`. Also `->viaTable()` could be simplified by `->via('advRels')` since you have added method `getAdvRels()`. – robsch Mar 22 '22 at 12:42
0

I`ve managed this some how... but it needs additional work after. The point is that you have to query many-to-many relation first from source model and after that inside that closure you should query your target model.

        $query = Product::find();
        $query->joinWith([
                         'product_has_adv' => function ($query)
                         {
                            $query->alias('pha');
                            $query->orderBy('pha.sort ASC');
                            $query->joinWith(['advantage ' => function ($query){
                                $query->select([
                                            'a.id',
                                            'a.text',
                                            ]);
                                 $query->alias('a');
                            }]);
                         },
                         ]);

Then you just have to prettify the sorted result to your needs. The result for each row would look like

        "product_has_adv": [
        {
            "product": "875",
            "advantage": "true",
            "sort": "0",
            "important": "1",
            "advantage ": {
                "id": "875",
                "text": "Some text..",
            }
        },
Stas Panyukov
  • 348
  • 3
  • 8
0

As explained by @arogachev, the viaTable uses two separate queries, which renders any intermediate orderBy obsolete

You could replace the viaTable with an innerJoin as follows, in a similar solution to @MartinM

public function getAdvantages()
{
   return $this->hasMany(AdvantageModel::class, ['pha.product' => 'id'])
       ->innerJoin('product_has_advantage pha', 'pha.advantage = advantage.id')
       ->andWhere(['pha.important' => 1])
       ->orderBy(['pha.sort' => SORT_ASC]);
}

By adjusting the result of hasMany, you are adjusting the query for the target class - AdvantageModel::find(); product_has_advantage can be joined via the advantage identity

The second parameter of hasMany, link, can be viewed as [ query.column => $this->attribute ], which you can now support via the joined product_has_advantage and its product identity


Note, when using viaTable, the link parameter can be viewed as if the intermediate query is complete and we are starting from there; [ query.column => viaTable.column ] hence ['id', 'advantage'] in your question

Arth
  • 12,789
  • 5
  • 37
  • 69
-2
public function getAdvantages()
{
    return $this
        ->hasMany(AdvantageModel::className(), ['id' => 'advantage'])
        ->viaTable('product_has_advantage', ['product' => 'id'])
        ->andWhere(['important' => 1])
        ->orderBy(['sort' => SORT_DESC]);
}
arogachev
  • 33,150
  • 7
  • 114
  • 117
  • 1
    Why should the OP "try this"? A **good answer** will always have an explanation of what was done and why it was done that way, not only for the OP but for future visitors to SO that may find this question and be reading your answer. – Maximilian Ast Jul 11 '16 at 07:39