diff --git a/framework/db/Command.php b/framework/db/Command.php index eac1092efd..16bb5b4e4d 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -68,10 +68,14 @@ class Command extends \yii\base\Component public $fetchMode = \PDO::FETCH_ASSOC; /** * @var array the parameters (name => value) that are bound to the current PDO statement. - * This property is maintained by methods such as [[bindValue()]]. - * Do not modify it directly. + * This property is maintained by methods such as [[bindValue()]]. It is mainly provided for logging purpose + * and is used to generated [[rawSql]]. Do not modify it directly. */ public $params = []; + /** + * @var array pending parameters to be bound to the current PDO statement. + */ + private $_pendingParams = []; /** * @var string the SQL statement that this command represents */ @@ -97,6 +101,7 @@ class Command extends \yii\base\Component if ($sql !== $this->_sql) { $this->cancel(); $this->_sql = $this->db->quoteSql($sql); + $this->_pendingParams = []; $this->params = []; } @@ -143,19 +148,30 @@ class Command extends \yii\base\Component * this may improve performance. * For SQL statement with binding parameters, this method is invoked * automatically. + * @param boolean $forRead whether this method is called for a read query. If null, it means + * the SQL statement should be used to determine whether it is for read or write. * @throws Exception if there is any DB error */ - public function prepare() + public function prepare($forRead = null) { - if ($this->pdoStatement == null) { - $sql = $this->getSql(); - try { - $this->pdoStatement = $this->db->pdo->prepare($sql); - } catch (\Exception $e) { - $message = $e->getMessage() . "\nFailed to prepare SQL: $sql"; - $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; - throw new Exception($message, $errorInfo, (int) $e->getCode(), $e); - } + if ($this->pdoStatement) { + return; + } + + $sql = $this->getSql(); + + if ($forRead || $forRead === null && $this->db->getSchema()->isReadQuery($sql)) { + $pdo = $this->db->getReadPdo(); + } else { + $pdo = $this->db->getWritePdo(); + } + + try { + $this->pdoStatement = $pdo->prepare($sql); + } catch (\Exception $e) { + $message = $e->getMessage() . "\nFailed to prepare SQL: $sql"; + $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; + throw new Exception($message, $errorInfo, (int) $e->getCode(), $e); } } @@ -184,6 +200,9 @@ class Command extends \yii\base\Component public function bindParam($name, &$value, $dataType = null, $length = null, $driverOptions = null) { $this->prepare(); + + $this->bindPendingParams(); + if ($dataType === null) { $dataType = $this->db->getSchema()->getPdoType($value); } @@ -199,6 +218,18 @@ class Command extends \yii\base\Component return $this; } + /** + * Binds pending parameters that were registered via [[bindValue()]] and [[bindValues()]]. + * Note that this method requires an active [[pdoStatement]]. + */ + protected function bindPendingParams() + { + foreach ($this->_pendingParams as $name => $value) { + $this->pdoStatement->bindValue($name, $value[0], $value[1]); + } + $this->_pendingParams = []; + } + /** * Binds a value to a parameter. * @param string|integer $name Parameter identifier. For a prepared statement @@ -212,11 +243,10 @@ class Command extends \yii\base\Component */ public function bindValue($name, $value, $dataType = null) { - $this->prepare(); if ($dataType === null) { $dataType = $this->db->getSchema()->getPdoType($value); } - $this->pdoStatement->bindValue($name, $value, $dataType); + $this->_pendingParams[$name] = [$value, $dataType]; $this->params[$name] = $value; return $this; @@ -235,18 +265,18 @@ class Command extends \yii\base\Component */ public function bindValues($values) { - if (!empty($values)) { - $this->prepare(); - foreach ($values as $name => $value) { - if (is_array($value)) { - $type = $value[1]; - $value = $value[0]; - } else { - $type = $this->db->getSchema()->getPdoType($value); - } - $this->pdoStatement->bindValue($name, $value, $type); - $this->params[$name] = $value; + if (empty($values)) { + return $this; + } + + foreach ($values as $name => $value) { + if (is_array($value)) { + $this->_pendingParams[$name] = $value; + } else { + $type = $this->db->getSchema()->getPdoType($value); + $this->_pendingParams[$name] = [$value, $type]; } + $this->params[$name] = $value; } return $this; @@ -271,11 +301,13 @@ class Command extends \yii\base\Component return 0; } + $this->prepare(false); + $this->bindPendingParams(); + $token = $rawSql; try { Yii::beginProfile($token, __METHOD__); - $this->prepare(); $this->pdoStatement->execute(); $n = $this->pdoStatement->rowCount(); @@ -390,11 +422,13 @@ class Command extends \yii\base\Component } } + $this->prepare(true); + $this->bindPendingParams(); + $token = $rawSql; try { Yii::beginProfile($token, 'yii\db\Command::query'); - $this->prepare(); $this->pdoStatement->execute(); if ($method === '') { diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 0593aabd9f..89413ca3f3 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -456,7 +456,6 @@ class Connection extends Component */ public function createCommand($sql = null, $params = []) { - $this->open(); $command = new Command([ 'db' => $this, 'sql' => $sql, @@ -660,6 +659,29 @@ class Connection extends Component $this->_driverName = strtolower($driverName); } + /** + * Returns the PDO instance for read queries. + * When [[enableSlave]] is true, one of the slaves will be used for read queries, and its PDO instance + * will be returned by this method. If no slave is available, the [[writePdo]] will be returned. + * @return PDO the PDO instance for read queries. + */ + public function getReadPdo() + { + $db = $this->getSlave(); + return $db ? $db->pdo : $this->getWritePdo(); + } + + /** + * Returns the PDO instance for write queries. + * This method will open the master DB connection and then return [[pdo]]. + * @return PDO the PDO instance for write queries. + */ + public function getWritePdo() + { + $this->open(); + return $this->pdo; + } + /** * Returns the currently active slave. * If this method is called the first time, it will try to open a slave connection when [[enableSlave]] is true. @@ -710,9 +732,11 @@ class Connection extends Component } /* @var $slave Connection */ $slave = Yii::createObject($config); + if (!isset($slave->attributes[PDO::ATTR_TIMEOUT])) { $slave->attributes[PDO::ATTR_TIMEOUT] = $this->slaveTimeout; } + try { $slave->open(); return $slave; diff --git a/framework/db/Schema.php b/framework/db/Schema.php index d03f049467..079605998d 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -369,11 +369,12 @@ abstract class Schema extends Object return $str; } - $this->db->open(); - if (($value = $this->db->pdo->quote($str)) !== false) { - return $value; - } else { // the driver doesn't support quote (e.g. oci) + $pdo = $this->db->getReadPdo(); + if (($value = $pdo->quote($str)) !== false) { + return $value; + } else { + // the driver doesn't support quote (e.g. oci) return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; } } @@ -520,4 +521,10 @@ abstract class Schema extends Object throw new $exceptionClass($message, $errorInfo, (int) $e->getCode(), $e); } } + + public function isReadQuery($sql) + { + $pattern = '/^\s*(SELECT|SHOW|DESCRIBE)\b/i'; + return preg_match($pattern, $sql); + } } diff --git a/framework/db/cubrid/Schema.php b/framework/db/cubrid/Schema.php index 29051d5e95..a44d003711 100644 --- a/framework/db/cubrid/Schema.php +++ b/framework/db/cubrid/Schema.php @@ -116,13 +116,14 @@ class Schema extends \yii\db\Schema return $str; } - $this->db->open(); + $pdo = $this->db->getReadPdo(); + // workaround for broken PDO::quote() implementation in CUBRID 9.1.0 http://jira.cubrid.org/browse/APIS-658 - $version = $this->db->pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION); + $version = $pdo->getAttribute(\PDO::ATTR_CLIENT_VERSION); if (version_compare($version, '8.4.4.0002', '<') || $version[0] == '9' && version_compare($version, '9.2.0.0002', '<=')) { return "'" . addcslashes(str_replace("'", "''", $str), "\000\n\r\\\032") . "'"; } else { - return $this->db->pdo->quote($str); + return $pdo->quote($str); } }