最近因采集项目中各服务日志信息,需要重写 mysql 基础类,故借此机会了解下。项目中常用 sql 语句大概有如下几种:
1: $res = Yii::$app->get('test')->createCommand($sql)->queryOne();
2: $res = Yii::$app->get('test')->createCommand()->update($table, $columns, $condition)->execute();
3: $res = Yii::$app->get('test')->createCommand()->BatchInsert($table, $columns, $rows)->execute();
先看第一种,其中参数 test 是在 配置文件中定义的一个数据库配置:
'test' => array(
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=xx.xxx.xxx.xx;port=xx;dbname=test,
'username' => root,
'password' => '123456',
'charset' => 'utf8mb4',
'tablePrefix' => 'test_',
),
由配置可见,经 yii 框架处理后,Yii::$app->get('test') 会生成一个 yii\db\Connection 类对象,继而调用成员方法 createCommand()。
public function createCommand($sql = null, $params = [])
{
/** @var Command $command */
$command = new $this->commandClass([
'db' => $this,
'sql' => $sql,
]);
return $command->bindValues($params);
}
在该方法中先是 new 一个 $this->commandClass 对象,然后再调用这个新对象的 bindValues() 方法。先看看这新对象是个啥:
public $commandClass = 'yii\db\Command';
class Command extends Component
{
public $db;
private $_sql;
}
class Component extends BaseObject;
class BaseObject implements Configurable
{
public function __construct($config = [])
{
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
}
}
public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}
return $object;
}
该属性在定义时就已经声明成 yii\db\Command 类,所以在 createCommand() 方法生成的也是 Command 类对象。 参数是数组结构,由于自身没有构造函数,实际执行的是类 BaseObject 中的构造函数,最终在该构造函数中通过调用 BaseYii.php 文件中的 static 方法 configure() 给属性 $db 和 $_sql 赋值。回到 Connect 类 $commandClass 属性上来,余以为在定义时声明成外部类并不合理,会导致两个类之间的耦合比较严重,最好是通过构造方法进行外部注入,这样更合适点。然后调用 Command 类中的 bindValues() 方法:
// yii\\db\command
public function bindValues($values)
{
...
$schema = $this->db->getSchema();
...
return $this;
}
// yii\db\Connection
public function getSchema()
{
...
$driver = $this->getDriverName();
if (isset($this->schemaMap[$driver])) {
$config = !is_array($this->schemaMap[$driver]) ? ['class' => $this->schemaMap[$driver]] : $this->schemaMap[$driver];
$config['db'] = $this;
return $this->_schema = Yii::createObject($config);
}
throw new NotSupportedException("Connection does not support reading schema information for '$driver' DBMS.");
}
public function getDriverName()
{
if ($this->_driverName === null) {
if (($pos = strpos($this->dsn, ':')) !== false) {
$this->_driverName = strtolower(substr($this->dsn, 0, $pos));
} else {
$this->_driverName = strtolower($this->getSlavePdo()->getAttribute(PDO::ATTR_DRIVER_NAME));
}
}
return $this->_driverName;
}
该方法主要是通过解析变量 $dsn 来获取该库的 schema,然后返回类对象,回到一开始的应用层代码中,继续调用 queryOne() 方法。
public function queryOne($fetchMode = null)
{
return $this->queryInternal('fetch', $fetchMode);
}
protected function queryInternal($method, $fetchMode = null)
{
...
$this->prepare(true);
...
$result = call_user_func_array([$this->pdoStatement, $method], (array) $fetchMode);
...
return $result;
}
可以看出调用的是 queryInternal() 方法,实际上所有的查询方法最终都是调用的该方法,只是参数不同。在 queryInternal() 方法中,继续调用 prepare() 方法。
public function prepare($forRead = null)
{
...
$sql = $this->getSql();
...
if ($forRead || $forRead === null && $this->db->getSchema()->isReadQuery($sql)) {
$pdo = $this->db->getSlavePdo();
} else {
$pdo = $this->db->getMasterPdo();
}
...
$this->pdoStatement = $pdo->prepare($sql);
$this->bindPendingParams();
}
该方法主要是准备要执行的 sql 语句,并将其赋值给属性 $this->pdoStatement,最终执行回调函数 call_user_func_array() 获取查询结果。
此时第一种情况已捋清楚,再看另外两种情况。后两种情况中, createCommand() 方法并没有 $sql 参数,所以在声明 yii\db\Command 类对象时,只有属性 $db 赋值, $_sql 此时还为空,这时就要看后面的 update() 和 batchInsert() 方法了。先看第二种情况:
public function update($table, $columns, $condition = '', $params = [])
{
$sql = $this->db->getQueryBuilder()->update($table, $columns, $condition, $params);
return $this->setSql($sql)->bindValues($params);
}
方法体中第一句是获取完整的 sql 语句,然后调用 setSql() 方法给属性 $_sql 赋值:
public function setSql($sql)
{
...
$this->_sql = $this->db->quoteSql($sql);
return $this;
}
可以看到是调用的 yii\db\Connection 类中的 quoteSql() 方法获取最终的 sql 语句。此处暂且不管 quoteSql() 方法是如何实现的,后面再说。实际上除了 BatchInsert() 方法,其他所有的修改操作调用的都是 setSql() 方法,那么 BatchInsert() 又是如何获取完整的 sql 语句的呢?
public function batchInsert($table, $columns, $rows)
{
$table = $this->db->quoteSql($table);
$columns = array_map(function ($column) {
return $this->db->quoteSql($column);
}, $columns);
$sql = $this->db->getQueryBuilder()->batchInsert($table, $columns, $rows);
$this->setRawSql($sql);
return $this;
}
方法体前面部分也是刚提到的 quoteSql() 方法,先放一边。在通过调用类 QueryBuilder 的 BatchInsert() 方法生成完整的 sql 语句后,调用了 setRawSql() 方法。
public function setRawSql($sql)
{
...
$this->_sql = $sql;
return $this;
}
这里是直接将参数 $sql 赋值给属性 $this->_sql,没调用 quoteSql() 方法很显然是因为在 BatchInsert() 方法中已经调用了。该看看 quoteSql() 方法体了。
public function quoteSql($sql)
{
return preg_replace_callback(
'/(\\{\\{(%?[\w\-\. ]+%?)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/',
function ($matches) {
if (isset($matches[3])) {
return $this->quoteColumnName($matches[3]);
}
return str_replace('%', $this->tablePrefix, $this->quoteTableName($matches[2]));
},
$sql
);
}
且不管 quoteColumnName() 和 quoteTableName() 俩方法后面还会调用几层方法,就 preg_replace_callback() 方法功能而言,是执行一个正则表达式搜索并且使用一个回调函数进行替换,此处是直接定义的匿名函数。最后是调用 execute() 方法, 和 queryInternal() 方法功能大体差不多。
public function execute()
{
...
$this->prepare(false);
...
$this->pdoStatement->execute();
$n = $this->pdoStatement->rowCount();
return $n;
}
至此,yii 框架对 mysql 的封装已基本理清楚。注意的是,考虑到正则匹配效率相对较低,实际项目中应尽量避免。在有关数据库的任何操作时,最好是利用各参数拼接成完整的 sql 语句,作为方法 createCommand() 的参数赋值给属性 $_sql。