一个用于多个表和数据库的ActiveRecord模型

时间:2017-09-21 17:14:40

标签: php activerecord dynamic yii2

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

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

所以数据库设置如下:

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个数据库,每个约有50个表,但仍在增长)。

由于每个表具有相同的结构,我认为从Yii2框架中使用ActiveRecord类是个好主意。 但是,ActiveRecord类使用静态方法来获取数据库连接和tableName。静态方法不能在为每个实例使用不同的表和db时创建类的实例。

getDb()(Yii2 Framework)

/**
* 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 Framework)

/**
* 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值来使其工作。 所以我可以在url中声明tablename和db,非常简单 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做了类似的事情(我用所有数据库凭据填充参数数组,并在getDb中使用... request-> get(...)设置模型中的db )功能。

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

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

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

1 个答案:

答案 0 :(得分:0)

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

我使用了一个不太漂亮的解决方案,但是最终成功了。

首先创建一个自定义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');

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