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

joinWith + criteria over multiple databases not working #5470

Closed
stefandoorn opened this issue Oct 10, 2014 · 22 comments
Closed

joinWith + criteria over multiple databases not working #5470

stefandoorn opened this issue Oct 10, 2014 · 22 comments
Assignees
Labels
feature:db status:to be verified Needs to be reproduced and validated. type:bug Bug

Comments

@stefandoorn
Copy link

Introduction
First of all; I'm not sure if this is not working by design or that it's a bug.

What I tried is making a join over two tables in separate databases with a WHERE statement on it.

The use case is that we have a second database with log tables and in the backend would like to join this with live data. I made a simplified test case.

I tried searching the forum + issues but couldn't find a similar issue.

Environment

  • PHP 5.4.3
  • Yii 2.0.0-dev
  • MySQL 5

Reproduce

Create two database configs in /config/web.php (with according settings):

        'db' => require(__DIR__ . '/db.php'),
        'db2' => require(__DIR__ . '/db2.php')

Create database setup for database #1:

CREATE TABLE IF NOT EXISTS `shop` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `shop` (`id`, `name`) VALUES
    (1, 'Shop #1'),
    (2, 'Shop #2');

Then create setup for database #2:

CREATE TABLE IF NOT EXISTS `employee` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `shop_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  KEY `FK__yii2_test1.shop` (`shop_id`),
  CONSTRAINT `FK__yii2_test1.shop` FOREIGN KEY (`shop_id`) REFERENCES `yii2_test1`.`shop` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `employee` (`id`, `name`, `shop_id`) VALUES
    (1, 'Jon Doe', 1),
    (2, 'Abraham Lincoln', 1),
    (3, 'John Lennon', 2);

Then we create two models, first Shop.php:

/**
 * Shop model
 */
class Shop extends \yii\db\ActiveRecord 
{

    /**
     * Define default database for the sake of clarity
     */
    public static function getDb() {
        return \Yii::$app->db;
    }

    /**
     * Relation to Employee
     *
     * @return Relation
     */
    public function getEmployees() {
        return $this->hasMany(Employee::className(), ['shop_id' => 'id']);
    }

}

And Employee.php:

/**
 * Employee model
 */
class Employee extends \yii\db\ActiveRecord 
{

    /**
     * Use different database 
     */
    public static function getDb() {
        return \Yii::$app->db2;
    }

    /**
     * Relation to Shop
     *
     * @return Relation
     */
    public function getShop() {
        return $this->hasOne(Shop::className(), ['id'=>'shop_id']);
    }
}

Now in a controller I would like to fetch some data with a criteria:

        $employeesShop1 = \app\models\Employee::find()
            ->joinWith(array('shop' => function($query) { return $query->andWhere(array('shop_id' => 1)); }))
            ->all();

This results in an error:

image

The statement would work if the query is created like this:

SELECT `employee`.* FROM `employee` LEFT JOIN `yii2_test1`.`shop` ON `employee`.`shop_id` = `yii2_test1`.`shop`.`id` WHERE `shop_id`=1
@samdark samdark added the status:to be verified Needs to be reproduced and validated. label Oct 10, 2014
@samdark samdark added this to the 2.0.1 milestone Oct 10, 2014
@mdmunir
Copy link
Contributor

mdmunir commented Oct 10, 2014

Of course you cannot. What you expected?
But try this

$employeesShop1 = \app\models\Employee::find()
            ->joinWith(['shop' => function($query) { 
                   return $query->from('yii2_test1.' . Shop::tableName())
                          ->andWhere(['shop_id' => 1]); 
            }])
            ->all();

I am not sure it will work :D

@samdark
Copy link
Member

samdark commented Oct 10, 2014

It is possible when using two MySQL instances as far as I know but the query should look as @mdmunir explained.

@samdark
Copy link
Member

samdark commented Oct 10, 2014

It definitely isn't possible if databases are of different types and I think we should throw an exception or fall back to WHERE IN queries in this case.

@cebe cebe self-assigned this Oct 10, 2014
@stefandoorn
Copy link
Author

In my case the db user/pass are the same, only difference is the database name (this is a requirement for this feature ofcourse).

I expected in this case an option to use the Shop model to find out the database name and form the query based on that. Might be too easy thought, but a feature in this direction could be interesting imo.

@qiangxue
Copy link
Member

I think you should add database prefix when defining tableName() if you are using mysql. I don't think we support joining between tables from different database connections. (We do support relational queries that do not involve JOIN).

@samdark
Copy link
Member

samdark commented Oct 10, 2014

@qiangxue you're right about database prefix and where to define it.

@samdark
Copy link
Member

samdark commented Oct 10, 2014

Also it seems it should be a single connection in order to work properly.

@samdark
Copy link
Member

samdark commented Oct 10, 2014

Overall it's kinda rare situation and framework has enough support for it.

@samdark samdark closed this as completed Oct 10, 2014
@cebe cebe modified the milestone: 2.0.1 Oct 10, 2014
@keeper-evil
Copy link

/**
 * Shop model
 */
class Shop extends \yii\db\ActiveRecord 
{
    // ...
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{' . static::getDb()->getSchema()->getRawTableName(parent::tableName()) . '}}';
    }
    // ...
}

And then

$employeesShop1 = \app\models\Employee::find()
    ->joinWith(array('shop' => function($query) {
        return $query->andWhere(array('shop_id' => 1));
    }))
    ->all();

@dynasource dynasource reopened this Jul 13, 2017
@dynasource
Copy link
Member

based on http://www.yiiframework.com/doc-2.0/guide-db-active-record.html#cross-database-relations, I would expect cross database joining to work without using examples like this #5470 (comment)

@cebe
Copy link
Member

cebe commented Jul 13, 2017

@dynasource cross db JOIN is totally different from cross db relation. with() will work, but joinWith() does not.

If you want to use join on a dbms that supports it, you need to include the database name in the table name of the record:

public static function tableName()
{
      return '{{databasename}}.{{tablename}}';
}

implementing JOIN accross dbs on dbms that do not support it is not feasable in an efficient way.

@dynasource
Copy link
Member

the insertion of that databasename in that tablename() can be quite cumbersome.

For example: I've got a plain Mysql scenario in which

  • 95% of the tables are in the $app->db scope
  • 5% of the tables are in the $app->db_stats scope

When I create i.e. an ActiveQuery in this $app->db_stats scope, it is already using the following code:

    public static function getDb()
    {
        return \Yii::$app->get('db_stats');
    }

Because of this, when I create a ActiveQuery from that $app->db_stats scope, it assumes all the joinWith are in the same DB.

It seems the framework is not (yet) smart enough to include the DB name for all other tables with the default $app->db scope.

@cebe
Copy link
Member

cebe commented Jul 14, 2017

Doing that automatically would introduce a lot of overhead to support a very rarely used feature.

@dynasource
Copy link
Member

IMHO, it is not rare to have multiple DB's of the same DBMS wired to an app. An since we support it anyways, it would be logical to have this working out of the box.

With respect to the overhead. That's another thing to be tackled, of course.

@keeper-evil
Copy link

keeper-evil commented Jul 25, 2017

@dynasource

/**
 * @property StatsItem[] $statsItems
 */
class Item extends \yii\db\ActiveRecord
{
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getStatsItems()
    {
        return $this->hasMany(StatsItem::className(), ['stats_item_id' => 'id']);
    }
}
class StatsItem extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function getDb()
    {
        return \Yii::$app->get('db_stats');
    }

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{stats}}.{{' . self::getDb()->getSchema()->getRawTableName(parent::tableName()) . '}}';
    }
}
$items = \common\models\Item::find()
    ->joinWith('statsItems')
    ->limit(20)
    ->all();

@dynasource
Copy link
Member

The problem is the other way around with StatsItem::find()

@samdark samdark added this to the 2.0.15 milestone Jul 25, 2017
@keeper-evil
Copy link

@dynasource, I do not see a problem

class Item extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{site}}.{{' . self::getDb()->getSchema()->getRawTableName(parent::tableName()) . '}}';
    }
}
/**
 * @property Item $item
 */
class StatsItem extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function getDb()
    {
        return \Yii::$app->get('db_stats');
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getItem()
    {
        return $this->hasOne(Item::className(), ['id' => 'stats_item_id']);
    }
}
$statsItems = \common\models\StatsItem::find()
    ->joinWith('item')
    ->limit(20)
    ->all();

@dynasource
Copy link
Member

dynasource commented Jul 26, 2017

the usecase is that Item returns a tableName like "item" in the default db application component scope

@keeper-evil
Copy link

keeper-evil commented Jul 26, 2017

For example: I've got a plain Mysql scenario in which

  • 95% of the tables are in the $app->db scope
  • 5% of the tables are in the $app->db_stats scope

Yes, this was required by condition. Item returns a tableName like item in the default db application component scope and StatsItem returns a tableName like stats_item in the your db_stats application component scope. I did not understand what's wrong.

@sdlins
Copy link
Contributor

sdlins commented Oct 26, 2018

For MSSQL I had to do this:

public static function tableName()
{
    $defaultSchema = static::getDb()->schema->defaultSchema;
    return 'AGRO_PORTAL' . ($defaultSchema ? '.' . $defaultSchema: '') . '.VW_EMPREGADO';
}

But I will have to adapt my tests because, for simplicity, I have all tables in the same MySQL (not mssql) DB, not 20 dbs as we (unfortunately) have in prod server. A getDbName() (#6533) would be useful here too.

@cebe cebe removed this from the 2.0.16 milestone Nov 27, 2018
@rezikovka
Copy link

rezikovka commented Aug 25, 2019

To automate this process in MySQL we can do smthng like this:

    public static function getDbName(): string
    {
        return self::getDb()->createCommand("SELECT DATABASE()")->queryScalar();
    }

    public static function tableName(): string
    {
        return '{{%' . self::getDbName() . '.table_name}}';
    }

Profit - we don't need to hardcode db-name into the model, just to define it in config. Request in getDbName() can be cached

@bizley bizley closed this as completed Mar 8, 2021
@bizley
Copy link
Member

bizley commented Mar 10, 2021

Closing due to inactivity. If this should be reopen please write here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature:db status:to be verified Needs to be reproduced and validated. type:bug Bug
Projects
None yet
Development

No branches or pull requests

10 participants