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

Disambiguing column's table on ActiveQuery select/where/order etc. #33

Open
petrabarus opened this issue Feb 13, 2015 · 50 comments
Open

Comments

@petrabarus
Copy link

I have several tables that have columns with same name. Let's say shelf, book, chapter, have the same field name.

I have a page that will show list of chapter that will also show the name of the book as well as the shelf.

Naturally this is the query I used for the ActiveDataProvider

Chapter::find()
  ->joinWith([
      'book' => function($query) {
          $query->joinWith([
              'shelf'
          ])
      }
  ])

But I only wanted to show the name in the GridView, so this is what I do to avoid the SELECT *.

Chapter::find()
  ->addSelect(['name', 'bookId'])
  ->joinWith([
          'book' => function($query) {
              $query->addSelect(['name', 'shelfId'])
                         ->joinWith([
                                'shelf' => function($query){
                                    $query->addSelect(['name']);
                                }
                         ]);
          }
  ])

On my Yii2 (c21895d4dd4c18d5bab37e0b7b359668f8aa8b67) this will output error Column 'name' in field list is ambiguous. So I have to put something like this

Chapter::find()
  ->addSelect(['Chapters.name', 'bookId'])
  ->joinWith([
          'book' => function($query) {
              $query->addSelect(['Books.name', 'shelfId'])
                         ->joinWith([
                                'shelf' => function($query){
                                    $query->addSelect(['Shelves.name']);
                                }
                         ]);
          }
  ])

Do I really have to do this for every query like this? Or is there any simpler way I don't know?

I'm thinking that if I can disambiguate the table name right from the addSelect method, it would be much easier. I extend the ActiveQuery and do something like this.

    private $_tableName;
    private function getTableName() {
        //since the `from` will be resolved in the `prepare` method.
        if (!isset($this->_tableName)) {
            $class = $this->modelClass;
            $this->_tableName = $class::tableName();
        }
        return $this->_tableName;
    }
    public function addSelect($columns) {
        if (is_array($columns)) {
            $columns = array_map(function($column) {
                return (strpos($column, ".") == false) ? $this->getTableName() . ".{$column}" : $column;
            }, $columns);
        } else {
            $columns = (strpos($column, ".") == false) ? $this->getTableName() . ".{$columns}" : $columns;
        }
        return parent::addSelect($columns);
    }
@mdmunir
Copy link
Contributor

mdmunir commented Feb 13, 2015

mungkin sama dengan ini #2377

@petrabarus
Copy link
Author

Yeah, pretty much the same with #2377, I've read that before this, but I forgot to mention.
The issue stated that it's good to have a way where we can put alias to disambiguate the tables.

But I'm suggesting that, either way we set an alias or not, the select() and addSelect() should also put the table name (or pre-generated alias) to the column name if there is no table/alias set. e.g. ->select(['column']) will automatically result in Table.column (or generatedAlias.column), but ->select(['alias.column']) should stay the same, just in case the programmer want to put a easily-read alias.

@cebe
Copy link
Member

cebe commented Jul 31, 2015

can't you do it like this:

Chapter::find()
  ->select(['Books.name AS bookName', 'Shelfs.name AS shelf_name', 'bookId', /* ... */])
  ->joinWith(['book', 'book.shelf'])
  ->asArray()->all();

as far as I see there is no need for the nesting here.

@petrabarus
Copy link
Author

I'm thinking that it would be good if there is automatic table aliasing to support code reusing. This should not only work on select or addSelect but also on where, order etc.

For example, I have a query for Books and Shelves class.

class Query extends \yii\db\ActiveQuery {
   public function isNotRemoved() {
      return $this->andWhere(['is_removed' => 0]);
   }
}

class ShelfQuery extends Query {
}

class BookQuery extends Query {
}

class Shelf extends \yii\db\ActiveRecord {
    public static find() {
       return \Yii::createObject(Shelf::class, [get_called_class()]);
    }
}

class Book extends \yii\db\ActiveRecord {
    public static find() {
       return \Yii::createObject(Shelf::class, [get_called_class()]);
    }
}

Naturally, we want to select chapters that are not removed in books that are not removed and in shelves that are also not removed.

It would be nice if we can just reuse the isRemoved() method to simplify the query instead of manually writing the table names again and again for the same part of query.

Chapter::find()
  ->joinWith([
      'book' => function(BookQuery $query) {
          $query->joinWith([
              'shelf' => function(ShelfQuery $query) {
                  $query->isNotRemoved();
              }
          ])->isNotRemoved();
      }
  ])->isNotRemoved();

In the current Yii version, the query above will throw error Ambiguous column is_removed. But if we can use aliasing similar with Yii1, the first isNotRemoved will use t.is_removed, the second one will use t2.is_removed (or use the relation name book.is_removed like Yii1), and the third one will use t3.is_removed (or shelf.is_removed).

This way, we can reuse methods in Query classes and get better "Don't Repeat Yourself"s.

@petrabarus petrabarus changed the title Disambiguing column's table on ActiveQuery select/addSelect Disambiguing column's table on ActiveQuery select/where/order etc. Jul 31, 2015
@petrabarus
Copy link
Author

Still reading the whole Query classes to start working on the pull request. But as the framework users so far, the ideas are below.

First, we should add alias attribute to the \yii\db\Query class.

namespace yii\db;

class Query extends Component implements QueryInterface {
   public $alias = 't';
}

and when we use select, where, order, etc, the attribute will automatically appended by the alias, unless it's already appended.

$query = (new Query())->select(['attr1', 'attr2'])->from('table')->where(['attr1' => 5])->order(['attr2' => SORT_ASC]);

for example above the query generated will be

SELECT t.attr1, t.attr2 FROM table t WHERE t.attr1 = 5 ORDER BY t.attr2 ASC

but of course this will break if users use alias in the from method and not using the alias in the select or other methods.

$query = (new Query())->select(['attr1', 'attr2'])->from('table u')->where(['attr1' => 5])->order(['attr2' => SORT_ASC]);

in the current yii version, the query above works if attr1 exists in the table table. But if we use the $alias attribute, this will break.

But the query below is safe.

$query = (new Query())->select(['u.attr1', 'u.attr2'])->from('table u')->where(['u.attr1' => 5])->order(['u.attr2' => SORT_ASC]);

Next in the ActiveQuery, when with method is used, the alias for the query for the relation will be replaced with the name of the relation.

Chapter::find()
  ->joinWith([
      'book' => function(BookQuery $query) {
          $query->joinWith([
              'shelf' => function(ShelfQuery $query) {
                  $query->isNotRemoved();
              }
          ])->isNotRemoved();
      }
  ])->isNotRemoved();

The query above will result on

SELECT t.*, book.*, shelf.* 
     FROM Chapters t 
        JOIN Books book ON t.book_id = book.id
        JOIN Shelves shelf ON book.shelf_id = shelf.id
     WHERE
        t.is_removed = 0 AND
        book.is_removed = 0 AND
        shelf.is_removed = 0;

@cebe cebe self-assigned this Aug 2, 2015
@cebe
Copy link
Member

cebe commented Feb 12, 2016

Read this issue through and found similar to #9326. Automatic aliasing of all columns is hard to do right but we could add methods to retrieve tables aliases currently used in a query.

@mikk150
Copy link
Contributor

mikk150 commented Feb 13, 2016

consider this

class User extends yii\db\ActiveRecord
{
    public function attributes()
    {
        return ['id'];
    }

    public function tableName()
    {
        return 'user';
    }

    public function getUserBlogPosts()
    {
        return $this->hasMany(BlogPosts::className(), ['id' => 'user_id']);
    }
}
class BlogPosts extends yii\db\ActiveRecord
{
    public function attributes()
    {
        return ['id', 'tag_id', 'user_id'];
    }

    public function tableName()
    {
        return 'blog_posts';
    }
}
class TestController extends Controller
{
    public function actionTest()
    {
        User::find()->joinWith('userBlogPosts', false)->where(['userBlogPosts.id' => [46, 88, 13]]);
    }
}

wouldn't it be nice, if you do not have to remember, that userBlogPosts relation table name is blog_posts, instead wrap it with some characters(or it would auto-detect that hey I do not have any alias for userBlogPosts, maybe you meant relation userBlogPosts that is really blog_posts

@cebe
Copy link
Member

cebe commented Feb 13, 2016

The following is possible with 2.0.7 release, which will be out tomorrow:

class TestController extends Controller
{
    public function actionTest()
    {
        User::find()->joinWith('userBlogPosts upb', false)->where(['upb.id' => [46, 88, 13]]);
    }
}

You could even make it a habit to always use the relation name as the alias:

    public function getUserBlogPosts()
    {
        return $this->hasMany(BlogPosts::className(), ['id' => 'user_id'])->alias('userBlogPosts');
    }

so with the above relation definition the action would work as you wrote it.

@petrabarus
Copy link
Author

@cebe

can you give example how should I write the refactored isRemoved method in the above example I gave.

class Query extends \yii\db\ActiveQuery {
   public function isNotRemoved() {
      return $this->andWhere(['is_removed' => 0]);
   }
}

class ShelfQuery extends Query {
}

class BookQuery extends Query {
}

class Shelf extends \yii\db\ActiveRecord {
    public static find() {
       return \Yii::createObject(Shelf::class, [get_called_class()]);
    }
}

class Book extends \yii\db\ActiveRecord {
    public static find() {
       return \Yii::createObject(Shelf::class, [get_called_class()]);
    }
}

@cebe
Copy link
Member

cebe commented Feb 13, 2016

@petrabarus I have no good solution for this case yet, that's why this issue is still open. I'd like to see even more use cases to build a solution for them.

@mikk150
Copy link
Contributor

mikk150 commented Feb 13, 2016

@cebe it would be real nice, because then we can finally do soft-delete easily

@petrabarus
Copy link
Author

@cebe just skimmed the #10813 and #10253, can we do something like this?

class Query extends \yii\db\ActiveQuery {
   public function isNotRemoved() {
      $alias = $this->getAlias();
      return $this->andWhere(["`{$alias}`.is_removed" => 0]);
   }
}

@petrabarus
Copy link
Author

or maybe we can have a shorthand placeholder for current alias.. like {{}}.

class Query extends \yii\db\ActiveQuery {
   public function isNotRemoved() {
      return $this->andWhere(['{{}}.is_removed' => 0]);
      //string {{}} will be automatically replaced with the current alias.
   }
}

@mikk150
Copy link
Contributor

mikk150 commented Feb 14, 2016

@petrabarus that {{}} thing, I don't think it could be a good idea

@sasha-x
Copy link

sasha-x commented Mar 24, 2016

Now I use:

class ActiveRecord {
    /**
     * We can specify "default" table alias here in addition to tableName() for using it in SQLs.
     * By default alias is equal to table name.
     * @return string
     */
    public static function tableAlias()
    {
        return static::tableName();
    }
} 
class ActiveQuery {
    public function getAlias()
    {
        list(, $alias) = $this->getQueryTableName($this);
        return $alias;
    }
}

Abstraction of my issue #10909 may looks like:

$query = ... ; //Some ActiveQuery, may contain join and joinWith sections

$query->forTable(<unique id>, //<unique id> may be table name, or alias, or position pointer, like 'primary'
                function($query){
                    //all column names will be auto-prepended with <unique id> table name/alias
                    $query->select(['id', 'name', 'created'])
                            ->andWhere('id > 10')
                            ->addOrderBy('id');
});

and, may be,

class ActiveQuery {
    /**
     * @param string $tableUniqueId same as in prev example
     */
    public function getAlias($tableUniqueId){}
}

@mikk150
Copy link
Contributor

mikk150 commented Apr 1, 2016

why ActiveQuery::getQueryTableName is private?
why ActiveQuery::getQueryTableName takes in query, when it is itself in query?

@mikk150
Copy link
Contributor

mikk150 commented Apr 1, 2016

Also I cannot find place to attach myself via events(or anything else) right before sql statement is being constructed for this query so I could alter the statement

@AnatolyRugalev
Copy link
Contributor

DOES NOT WORK. SEE NEXT COMMENTS
I have a magic solution for this:

<?php

namespace common\query;

use yii\db\Query;
use \yii\helpers\Json;
use \yii\helpers\ArrayHelper;

class ActiveQuery extends \yii\db\ActiveQuery
{
    public function prepare($builder)
    {
        $query = parent::prepare($builder);
        static::replaceAlias($query);
        return $query;
    }

    public static function replaceAlias(Query $query)
    {
        $alias = ArrayHelper::isAssociative($query->from) ? array_keys($query->from)[0] : $query->from[0];

        $replaceAliasRecursively = function ($value) use ($alias, &$replaceAliasRecursively) {
            if ($value instanceof \yii\db\Expression) {
                $value->expression = $replaceAliasRecursively($value->expression);
            } elseif (is_scalar($value)) {
                $value = str_replace('%%alias%%', $alias, $value);
            } elseif (is_array($value)) {
                $newValue = [];
                foreach ($value as $k => $v) {
                    $newKey = $replaceAliasRecursively($k);
                    $newValue[$newKey] = $replaceAliasRecursively($v);
                }
                $value = $newValue;
                unset($newValue);
            }

            return $value;
        };

        $attributes = ['where', 'orderBy'];
        foreach ($attributes as $attribute) {
            if (!empty($query->$attribute)) {
                $query->$attribute = $replaceAliasRecursively($query->$attribute);
            }
        }
    }

    public function aliasMiddleware($callback)
    {
        return function (ActiveQuery $query) use ($callback) {
            $callback($query);
            static::replaceAlias($query);
        };
    }

    public function joinWith($with, $eagerLoading = true, $joinType = 'LEFT JOIN')
    {
        $result = parent::joinWith($with, $eagerLoading, $joinType);

        foreach ($this->joinWith as $i => $config) {
            foreach ($config[0] as $j => $relation) {
                if (is_callable($relation)) {
                    $this->joinWith[$i][0][$j] = $this->aliasMiddleware($relation);
                }
            }
        }

        return $result;
    }
}

Inside models:

    public static function find()
    {
        $query = Yii::createObject(\common\query\ActiveQuery::className(), [get_called_class()]);
        return $query>andWhere(['%%alias%%.isDeleted' => false]);
    }

This hack wraps ActiveQuery relational callbacks with middleware and replaces all %%alias%% strings inside scopes.

Found it together with @sizeg

@mikk150
Copy link
Contributor

mikk150 commented Apr 3, 2016

@AnatolyRugalev it is not magic -> it doesn't work
consider this

Book::find()->joinWith('author')->all()

produces

SELECT `book`.* FROM `book` LEFT JOIN `author` ON `book`.`author_id` = `author`.`id` WHERE (`book`.`deleted_at` IS NULL) AND (`book`.`deleted_at` IS NULL) LIMIT 20

what I actually wanted was

SELECT `book`.* FROM `book` LEFT JOIN `author` ON `book`.`author_id` = `author`.`id` WHERE (`book`.`deleted_at` IS NULL) AND (`author`.`deleted_at` IS NULL) LIMIT 20

@AnatolyRugalev
Copy link
Contributor

@mikk150 sorry about that. Looks like we got regression bug after refactoring, will let you know when we fix it.

@mikk150
Copy link
Contributor

mikk150 commented Apr 4, 2016

@AnatolyRugalev have you changed it? because it seems to work now magically, have no idea why

@sizeg
Copy link

sizeg commented Apr 4, 2016

@mikk150 we test your case, and get the same result. We need a time for refactoring it.

@CedricYii
Copy link
Contributor

Examples provided are putting emphasis on columns' aliases. My main concern is about using the same table in different relations with different aliases (or even on some occasions, reuse the same relation with another alias). Consider this example:

class Order {
...
  //first relation with table COMPANY
  public function getCustomer()
  {
      return $this->hasOne( Company::className(), ['id' => 'customerid'] ) //table COMPANY
                  ->andOnCondition( [ '==relationAlias==.type' => 'customer' ] );
  }

  //second relation with table COMPANY
  public function getSupplier()
  {
      return $this->hasOne( Company::className(), ['id' => 'supplierid'] ) //table COMPANY
                  ->andOnCondition( [ '==relationAlias==.type' => 'supplier' ] );
  }
...
}

Then we could write

$orders = Order::find()
                   ->alias( 'o' )
                   ->innerJoinWith('customer c')
                   ->innerJoinWith('supplier s');

To build query

select o.* from order o
join company c on c.id = o.customerid and c.type = 'customer'
join company s on s.id = o.supplierid and s.type = 'supplier'

This is supposed to work with pr #11326

CedricYii referenced this issue in CedricYii/yii2 May 27, 2016
The @alias placeholder enables to address dynamicaly the alias of the
table name in an active query or in a relation (without knowing it in
advance).

Usage example:
class Order {
...
  public static function find()
  {
    return parent::find()->orderBy( '@alias.id' );
  }

  //relation with table COMPANY
  public function getCustomer()
  {
    return $this->hasOne( Company::className(), ['id' => 'customerid'] )
                ->andOnCondition( [ '@alias.type' => 'customer' ] );
  }
...
}

$orders = Order::find()->alias('o');
//will build the sql:
// select * from order o orderby o.id

$orders = Order::find()->alias('o')->joinWith('customer c');
//will build the sql:
// select o.* from order o 
// left join company c on c.id = o.customerid and c.type='customer'
// order by o.id

-fixes #7263
-fixes yiisoft#10883
@CedricYii
Copy link
Contributor

CedricYii commented May 27, 2016

Proposed syntax for dynamic alias addressing is simply @alias.
With pr #11646, you can now insert placeholder @alias in an active query to address the current table name without knowing it in advance. If the table as no alias, it will use the table name.

Consider this complete example:
ActiveRecord Order with two relations on the same table company

class Order {
...
  //first relation with table COMPANY
  public function getCustomer()
  {
      return $this->hasOne( Company::className(), ['id' => 'customerid'] ) //table COMPANY
                  ->andOnCondition( [ '@alias.type' => 'customer' ] );
  }

  //second relation with table COMPANY
  public function getSupplier()
  {
      return $this->hasOne( Company::className(), ['id' => 'supplierid'] ) //table COMPANY
                  ->andOnCondition( [ '@alias.type' => 'supplier' ] );
  }

  public static find()
  {
      return parent::find()->orderBy( '@alias.id' );
  }
...
}

ActiveRecord Company related to address (to illustrate reuse of the same relation with another alias).

class Company {
...
  public function getAddress()
  {
      return $this->hasOne( Address::className(), ['companyid' => 'id'] ) //table ADDRESS
                  //condition with one alias notation dynamicaly replaced by two different aliases
                  ->andOnCondition( [ '@alias.type' => 'head' ] ); 
  }
...
}

Then we could write

$orders = Order::find()
                   ->alias( 'o' )
                   ->innerJoinWith(['customer c' => function($q){
                      $q->joinWith(['address adc']);
                   }])
                   ->innerJoinWith(['supplier s' => function($q){
                      $q->joinWith(['address ads']);
                   }]);

To build query

select o.* from order o
join company c on c.id = o.customerid and c.type = 'customer'
join address adc on adc.companyid = c.id and adc.type = 'head'
join company s on s.id = o.supplierid and s.type = 'supplier'
join address ads on ads.companyid = c.id and ads.type = 'head'
order by o.id

It helps building complex queries without worrying about predefined alias. This should solve issues #7263 and #10883.

@petrabarus
Copy link
Author

petrabarus commented May 27, 2016

Haven't read update on ActiveQuery for a while. I assume @alias will be automatically generated right?

so adding @alias in return $this->andWhere(['@alias.is_removed' => 0]); should work right?

class Query extends \yii\db\ActiveQuery {
   public function isNotRemoved() {
      return $this->andWhere(['@alias.is_removed' => 0]);
   }
}

class ShelfQuery extends Query {
}

class BookQuery extends Query {
}

class Shelf extends \yii\db\ActiveRecord {
    public static find() {
       return \Yii::createObject(Shelf::class, [get_called_class()]);
    }
}

class Book extends \yii\db\ActiveRecord {
    public static find() {
       return \Yii::createObject(Book::class, [get_called_class()]);
    }
}

and this case will not result in error for ambiguous column name?

Chapter::find()
  ->joinWith([
      'book' => function(BookQuery $query) {
          $query->joinWith([
              'shelf' => function(ShelfQuery $query) {
                  $query->isNotRemoved();
              }
          ])->isNotRemoved();
      }
  ])->isNotRemoved();

@CedricYii
Copy link
Contributor

CedricYii commented May 27, 2016

That's right, it should work properly. Could you give it a try?

However I notice that in your example Shelf and Book are built on Shelf::class, so I assume they are using the same table name 'shelf' (or it probably is just a typo).
Whatever, in this case, you would need to give an alias to shelf and/or to book in order to distinguish them.
For example, it can be inserted:

  • in the Book::find() method
class Book extends \yii\db\ActiveRecord {
    public static find() {
       $query = \Yii::createObject(Shelf::class, [get_called_class()]);
       return $query->alias('book'); //add alias here
    }
}
  • or when you build the query
Chapter::find()
  ->joinWith([
      'book book' => function(BookQuery $query) { //add alias here
          $query->joinWith([
              'shelf' => function(ShelfQuery $query) {
                  $query->isNotRemoved();
              }
          ])->isNotRemoved();
      }
  ])->isNotRemoved();

@petrabarus
Copy link
Author

However I notice that in your example Shelf and Book are built on Shelf::class, so I assume they are using the same table name 'shelf' (or it probably is just a typo).

Yes, it's typo. I just fixed it.
Assuming the alias works, I think my case should be working.

Self joined class should also work with the code you put above.

Thanks!

@arthibald
Copy link

Just trying out your revisions @CedricYii, and they seem to work well for our project.
Any news on when this will become part of the core?

Also, disambiguing should really be disambiguating, or the title works as just disambiguate.

@arthibald
Copy link

@CedricYii, having now used your code more intensively, I had to add both:

$this->select   = self::replaceAliasPlaceholder( $this->select, $alias );

and the commented

     if (!empty($this->join)) {
         foreach ($this->join as &$join) {
             $join = self::replaceAliasPlaceholder( $join, $alias );
         }
     }

to the private function normalizeAliasInQuery() to cover some edge cases.

I haven't needed a UNION yet, but I suspect this will be necessary too at some point.

Thanks again.

@CedricYii
Copy link
Contributor

@arthibald Thank you for your feedback. I have no news on when it will be merged, however it is planned in 2.0.10, so wait and see...
Other commits have been made already, you can find then here https://github.com/yiisoft/yii2/pull/11646/commits (sorry I didn't reference the issue in all of them).
You might want to check the comments also in the pull request #11646
I may need to start a new issue dedicated to this new dynamic alias feature to make it clearer for everybody.

@CedricYii
Copy link
Contributor

As for UNION I haven't found a practical usage (UNION is usually between two unrelated query and there is no main scope for an @alias), so I dropped it for now.
Let me know if you find an example where it would be useful.

@arthibald
Copy link

@CedricYii Thanks, looks like we arrived at the same code in the end!

@mikk150
Copy link
Contributor

mikk150 commented Feb 12, 2017

How is it going?

@mikk150
Copy link
Contributor

mikk150 commented Apr 25, 2017

Could we just expose 2 methods like this:

class Query extends Component implements QueryInterface
{
    ...
    public function getTableAlias()
    {
        list(, $alias) = $this->getTableNameAndAlias();
        return $alias;
    }

    public function getTableName()
    {
        list($name,) = $this->getTableNameAndAlias();
        return $name;
    }
    ...
}

It starts getting really irritating.. and this eases the pain of figuring out the freaking alias

@cebe
Copy link
Member

cebe commented Apr 25, 2017

@mikk150 a query may involve more than one table and only in that case aliases are relevant, so adding these methods does not really help anything, it is not clear which table's alias you are asking for.

@mikk150
Copy link
Contributor

mikk150 commented Apr 25, 2017

Well, I am trying to get alias inside a models Query class, there I know what tables alias I am asking for... these methods could be protected to achieve it(but then when they actually matter again, they do not work again), but right now you just have to remember to use same aliases(which BTW does not work always)

@samdark
Copy link
Member

samdark commented Apr 25, 2017

Is that related to #14049?

@mikk150
Copy link
Contributor

mikk150 commented Apr 25, 2017

Kind of is...

Am I missing something, but why getFromAliases() return an array, if it is inside ActiveQuery(as activequery should only have one "from" declaration)

@cebe
Copy link
Member

cebe commented Apr 25, 2017

@mikk150 FROM can contain multiple tables in SQL. e.g.:

SELECT name FROM user, profile WHERE user.profile_id = profile.id;

@mikk150
Copy link
Contributor

mikk150 commented Apr 25, 2017

I know, but AR doesn't do that

@bscheshirwork
Copy link
Contributor

bscheshirwork commented May 16, 2017

1 The alias must previous to all assoc columns if Alias is set;
2 In the joinWith the alias must be set too (default is a key of array)

example

class Duration extends \yii\db\ActiveRecord {
...
    public function getUnit()
    {
        return $this->hasOne(DurationUnit::className(), ['id' => 'unitId'])->inverseOf('durations');
    }
...
}
        echo Duration::find()
            ->joinWith([
                'unit as unitAlias' => function (DurationUnitQuery $query) {
                    $query->andWhere(['id' => [5, 3, 2, 1]]);
                }
            ])
            ->andWhere(['id' => [4, 5, 9]])
            ->alias('durationAlias')
            ->createCommand()->getRawSql();

must produce

SELECT `durationAlias`.* FROM `duration` `durationAlias` LEFT JOIN `duration_unit` `unitAlias` ON `durationAlias`.`unitId` = `unitAlias`.`id` WHERE (`durationAlias`.`id` IN (4, 5, 9)) AND (`unitAlias`.`id` IN (5, 3, 2, 1))

instead of

SELECT `durationAlias`.* FROM `duration` `durationAlias` LEFT JOIN `duration_unit` `unitAlias` ON `durationAlias`.`unitId` = `unitAlias`.`id` WHERE (`id` IN (4, 5, 9)) AND (`id` IN (5, 3, 2, 1))

Now we process the validator condition throw applyAlias functional.

Where condition must be processed with this function:

https://github.com/bscheshirwork/yii2/blob/184f6d385bd1a7eb3e5216774a3a9783978b3760/framework/db/ActiveQuery.php#L800-L814

pro: 2 years old issue

contr: ActiveQuery with multiply from (and has one $this->modelClass)

@bscheshirwork
Copy link
Contributor

bscheshirwork commented May 17, 2017

For some case can be use #14170 (alias or table_name in joinWith)

For
1 The alias must previous to all assoc columns if Alias is set;

2 In the joinWith the alias must be set too (default is a key of array)

  • For joinWith alias can be set automatic is equal name of the relation.

@mikk150
Copy link
Contributor

mikk150 commented May 18, 2017

What if we change GII, to generate aliases as well? this will ease the pain for sure

@bscheshirwork
Copy link
Contributor

bscheshirwork commented May 18, 2017

Gii generated "starter" functional. I use this method now,
https://github.com/bscheshirwork/yii2-cubs/blob/5e84bd686f183052e62a840751800f9e5caf19a9/src/generators/model/Generator.php#L247
but it's not ideal. It's even harmful. So many case avoid it.

I think the attribute name must still clear in config. After #14163 I will be reverted it to base code

CedricYii referenced this issue in CedricYii/yii2 Feb 6, 2018
The @alias placeholder enables to address dynamicaly the alias of the
table name in an active query or in a relation (without knowing it in
advance).

Usage example:
class Order {
...
  public static function find()
  {
    return parent::find()->orderBy( '@alias.id' );
  }

  //relation with table COMPANY
  public function getCustomer()
  {
    return $this->hasOne( Company::className(), ['id' => 'customerid'] )
                ->andOnCondition( [ '@alias.type' => 'customer' ] );
  }
...
}

$orders = Order::find()->alias('o');
//will build the sql:
// select * from order o orderby o.id

$orders = Order::find()->alias('o')->joinWith('customer c');
//will build the sql:
// select o.* from order o 
// left join company c on c.id = o.customerid and c.type='customer'
// order by o.id

-fixes #7263
-fixes yiisoft#10883
CedricYii referenced this issue in CedricYii/yii2 Feb 7, 2018
The @alias placeholder enables to address dynamicaly the alias of the
table name in an active query or in a relation (without knowing it in
advance).

Usage example:
class Order {
...
  public static function find()
  {
    return parent::find()->orderBy( '@alias.id' );
  }

  //relation with table COMPANY
  public function getCustomer()
  {
    return $this->hasOne( Company::className(), ['id' => 'customerid'] )
                ->andOnCondition( [ '@alias.type' => 'customer' ] );
  }
...
}

$orders = Order::find()->alias('o');
//will build the sql:
// select * from order o orderby o.id

$orders = Order::find()->alias('o2')->joinWith('customer c');
//will build the sql:
// select o2.* from order o2 
// left join company c on c.id = o2.customerid and c.type='customer'
// order by o2.id

The dynamic alias feature can be enabled in the application
configuration by the property 'enableAliasDynamic'.
It is false by default so the feature is skipped to avoid possible speed
concerns when not needed.

To enable, you can add the following line in config/main.php:
return [
    'enableAliasDynamic' => true,
];

-fixes #7263
-fixes yiisoft#10883
CedricYii referenced this issue in CedricYii/yii2 Feb 8, 2018
The @alias placeholder enables to address dynamicaly the alias of the
table name in an active query or in a relation (without knowing it in
advance).

Usage example:
class Order {
...
  public static function find()
  {
    return parent::find()->orderBy( '@alias.id' );
  }

  //relation with table COMPANY
  public function getCustomer()
  {
    return $this->hasOne( Company::className(), ['id' => 'customerid'] )
                ->andOnCondition( [ '@alias.type' => 'customer' ] );
  }
...
}

$orders = Order::find()->alias('o');
//will build the sql:
// select * from order o orderby o.id

$orders = Order::find()->alias('o2')->joinWith('customer c');
//will build the sql:
// select o2.* from order o2 
// left join company c on c.id = o2.customerid and c.type='customer'
// order by o2.id

The dynamic alias feature can be enabled in the application
configuration by the property 'enableAliasDynamic'.
It is false by default so the feature is skipped to avoid possible speed
concerns when not needed.

To enable, you can add the following line in config/main.php:
return [
    'enableAliasDynamic' => true,
];

-fixes #7263
-fixes yiisoft#10883
CedricYii referenced this issue in CedricYii/yii2 Feb 9, 2018
The @alias placeholder enables to address dynamicaly the alias of the
table name in an active query or in a relation (without knowing it in
advance).

Usage example:
class Order {
...
  public static function find()
  {
    return parent::find()->orderBy( '@alias.id' );
  }

  //relation with table COMPANY
  public function getCustomer()
  {
    return $this->hasOne( Company::className(), ['id' => 'customerid'] )
                ->andOnCondition( [ '@alias.type' => 'customer' ] );
  }
...
}

$orders = Order::find()->alias('o');
//will build the sql:
// select * from order o orderby o.id

$orders = Order::find()->alias('o2')->joinWith('customer c');
//will build the sql:
// select o2.* from order o2 
// left join company c on c.id = o2.customerid and c.type='customer'
// order by o2.id

The dynamic alias feature can be enabled in the application
configuration by the property 'enableAliasDynamic'.
It is false by default so the feature is skipped to avoid possible speed
concerns when not needed.

To enable, you can add the following line in config/main.php:
return [
    'enableAliasDynamic' => true,
];

-fixes #7263
-fixes yiisoft#10883
@arthibald
Copy link

Is there any movement on this functionality being added to the core framework?
Currently we are having to update our stand in ActiveQuery class every time the framework is updated.

@samdark
Copy link
Member

samdark commented Mar 5, 2018

Yes and no. There's a pull request by @CedricYii but we haven't reviewed it in depth. It won't get into 2.0 for sure. 2.1 - likely.

@arthibald
Copy link

Ah, ok.. it's just it's been 3 years since this issue was raised, and as far as I can tell this is an integral to the flexibility of the framework; Yii 1.1 has this functionality.

@samdark
Copy link
Member

samdark commented Mar 6, 2018

Yes. And it was removed on purpose. That's why getting it back isn't a simple question.

@mikk150
Copy link
Contributor

mikk150 commented Mar 6, 2018

@arthibald Yii1.0 did not have this feature, so we are back where we started at least :)

@samdark samdark transferred this issue from yiisoft/yii2 Jan 7, 2019
@idMolotov
Copy link

idMolotov commented Jun 7, 2019

We definitely need this feature

CedricYii/yii2@1e25d3e

in the Yii2 and next generation versions too!

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

No branches or pull requests