【问题标题】:One ActiveRecord model for multiple tables and databases用于多个表和数据库的一个 ActiveRecord 模型
【发布时间】:2018-03-03 03:27:54
【问题描述】:

目前我正在 Yii2 框架中开发一个新的网站/平台。

项目存在于多个数据库(不同的客户端/mysql用户帐户)中,每个数据库存在于具有相同结构的多个表中。我知道这违反了关系数据库的指导方针,但由于其他不同的技术原因(不仅仅是懒惰......),不可能更改数据库设置。该数据库也集成在其他程序中,因此不可更改... 此时不需要对数据库进行写入。

所以数据库设置如下:

db_100 --o-- tbl_A1  (tables all have the same structure)
         o-- tbl_B2 
         o-- tbl_C3

db_200 --o-- tbl_A1
         o-- tbl_B2
         o-- tbl_C3
         o-- tbl_D4

db_300 --o-- tbl_A1
          ...           

数据库名和表名总是有相同的前缀,没有定义最大的数据库或表。数据库名和表名的后缀是不可预测的。 (目前 40 db,每个大约 50 个表,但仍在增长)。

因为每个表都有相同的结构,我认为使用 Yii2 框架之外的 ActiveRecord 类是一个好主意。 但是,ActiveRecord 类使用静态方法来获取数据库连接和表名。静态方法无法在为每个实例使用不同的表和数据库时创建类的实例。

getDb() (Yii2 框架)

/**
* Returns the database connection used by this AR class.
* By default, the "db" application component is used as the database connection.
* You may override this method if you want to use a different database connection.
* @return Connection the database connection used by this AR class.
*/
public static function getDb()
{
    return Yii::$app->getDb();
}

tableName()(Yii2 框架)

/**
* Declares the name of the database table associated with this AR class.
* By default this method returns the class name as the table name by calling [[Inflector::camel2id()]]
* with prefix [[Connection::tablePrefix]]. For example if [[Connection::tablePrefix]] is `tbl_`,
* `Customer` becomes `tbl_customer`, and `OrderItem` becomes `tbl_order_item`. You may override this method
* if the table is not named after this convention.
* @return string the table name
*/
public static function tableName()
{
    return '{{%' . Inflector::camel2id(StringHelper::basename(get_called_class()), '_') . '}}';
}

此时我通过使用请求中的 get-values 使其工作。 所以我可以在 url 中声明表名和数据库,非常简单 http://...:8080/CustomActiveRecord/index?db=100&customTableName=A1

(简化代码)

public static function tableName() {
    //get base of tablename
        $customTblName = static::customTblName(); //-> Yii::$app->request->get('customTblName') ?: null;
    //throw exception if null
        if (is_null($customTblName )) {
            throw new \yii\web\HttpException(...);
        }
    //return the tablename
        return 'tbl_' . $customTblName;
}

我为 db-connections 做了一些类似的事情(我用所有数据库凭据填充参数数组,并在模型中设置 db 也使用 getDb() 中的 ...request->get(...)功能。

这一切现在都可以与 gridview、listviews、kartik-Chartjs 结合使用……但前提是在 URL 中定义了 tableName 和 db。 这不能同时使用多个模型,这是我需要的。 (比较、统计……)

有人知道如何将一个 ActiveRecord 用于多个表/数据库吗? 理想情况下使用构造函数,以便我可以为每个表创建一个实例?

$model = New CustomActiveRecord(['db' => '100', 'tbl' => 'A1']);

【问题讨论】:

  • 看看ActiveQuery,这是find()静态方法在调用时创建的。 ActiveQuery 允许您选择运行操作的数据库。我快速查看并没有看到传递多个数据库的方法,因此您可能需要在每个数据库上运行查询并将结果合并到一个数组中。
  • 我在 ActiveRecord-Model 中尝试了以下操作:public static function find(){ return new CustomQuery(get_called_class()); } 和 CustomQuery(从 \yii\db\ActiveQuery 扩展)我按照方法更新了函数 all() 和 one() (作为测试): public function all($db = null) { $this->from('A1');返回父级::all($db);这已经离我的问题更近了一步,很多查询都知道使用 wright 表,但是像 gridview 这样的小部件仍然使用 ActiveRecord-model 中的静态方法中的 tableName。

标签: php activerecord dynamic yii2


【解决方案1】:

最近我遇到了同样的问题。

我使用了一个不漂亮的解决方案,但它最终起作用了。

首先创建一个自定义的ActiveRecord类:

use Yii;
use yii\base\InvalidArgumentException;
use yii\base\InvalidCallException;
use yii\db\ActiveRecord;
use yii\db\Connection;

class ActiveRecordCustom extends ActiveRecord
{
    /**
     * @var Connection[]
     */
    protected static $_connections = [];

    /**
     * @var static[]
     */
    private static $_classes = [];

    private static function ensureConnection(string $db): void
    {
        if (!preg_match('/^[a-z0-9_]++$/i', $db)) throw new InvalidArgumentException('Argument $db is not a valid database name');

        if (array_key_exists($db, self::$_connections)) return;

        /* @var Connection $connection */
        $connection = clone Yii::$app->get('db');
        $connection->dsn = SomeHelperClass::GenerateDsn($db);

        self::$_connections[$db] = $connection;
    }

    /**
     * Creates a dynamic class (and caches it). The resulting class uses a specific DB on its connection string.
     * @param string $db
     * @return static
     */
    public static function classForDb(string $db)
    {
        $calledClass = static::class;
        if (!in_array(self::class, class_parents($calledClass))) throw new InvalidCallException('This function must be called from child classes only');

        self::ensureConnection($db);

        $classKey = "{$calledClass}_{$db}";
        if (!array_key_exists($classKey, self::$_classes)) {
            if (!UString::startsWith('\\', $calledClass)) $calledClass = "\\{$calledClass}";
            $generatedClassName = 'dynamic_' . UString::secureRandomHexString();
            $generatedClassCode = <<<HEREDOC
class {$generatedClassName} extends {$calledClass} {
    public static function tableName() {
        return {$calledClass}::tableName();
    }

    public static function getDb(): \yii\db\Connection {
        return self::\$_connections['{$db}'];
    }

    public static function getDbName(): string {
        return '{$db}';
    }
}
HEREDOC;
            eval($generatedClassCode);
            self::$_classes[$classKey] = $generatedClassName;
        }

        return self::$_classes[$classKey];
    }

    /**
     * Creates an instance of a dynamic class (and caches it). The resulting instance uses a specific DB on its connection string.
     * @param string $db
     * @return static
     */
    public static function instanceForDb(string $db)
    {
        $class = self::classForDb($db);
        return new $class;
    }
}

UString 类:

class UString {
    /**
     * Checks if the string $haystack starts with $needle
     * @param string $haystack The string to check if starts with $needle
     * @param string $needle The string used to check if $haystack starts with it
     * @return bool True if $haystack starts with $needle, otherwise, false
     */
    public static function startsWith($haystack, $needle) {
        $length = mb_strlen($needle);
        return (mb_substr($haystack, 0, $length) === $needle);
    }

    /**
     * Generates a random HEX string with a fixed length of 128 chars. Its guaranteed to be cryptographically secure.
     * @return string|bool The generated random HEX string, or false in case of failure
     */
    public static function secureRandomHexString() {
        $data = openssl_random_pseudo_bytes(64, $secure);
        return $secure ? bin2hex($data) : false;
    }
}

最后是一个示例 AR:

class MyARClass extends ActiveRecordCustom {...}

现在剩下的就是使用 AR 类,如下所示:

  • 对于静态方法调用:MyARClass::classForDb('some_database')::find()...

  • 用于创建绑定到特定数据库的实例:$instanceTiedToDb = MyARClass::instanceForDb('some_database');

即使这个特定的代码很难用于动态数据库连接,它也可以轻松扩展以支持表。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-03-29
    • 2013-09-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多