Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yii2
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
PSDI Army
yii2
Commits
ea7bfa19
Commit
ea7bfa19
authored
Jul 12, 2014
by
Qiang Xue
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #4274 from yiisoft/feature-multiconnection
Support for separating Read/Write Database
parents
cdb2b44f
919ab232
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
641 additions
and
99 deletions
+641
-99
db-dao.md
docs/guide/db-dao.md
+163
-3
Command.php
extensions/sphinx/Command.php
+1
-1
Connection.php
extensions/sphinx/Connection.php
+0
-1
Schema.php
extensions/sphinx/Schema.php
+14
-4
Command.php
framework/db/Command.php
+65
-26
Connection.php
framework/db/Connection.php
+249
-31
Schema.php
framework/db/Schema.php
+14
-4
Schema.php
framework/db/cubrid/Schema.php
+11
-9
QueryBuilder.php
framework/db/mssql/QueryBuilder.php
+2
-2
QueryBuilder.php
framework/db/oci/QueryBuilder.php
+5
-2
Schema.php
framework/db/oci/Schema.php
+5
-1
QueryBuilder.php
framework/db/pgsql/QueryBuilder.php
+2
-2
QueryBuilder.php
framework/db/sqlite/QueryBuilder.php
+4
-1
DatabaseTestCase.php
tests/unit/framework/db/DatabaseTestCase.php
+21
-12
SqliteConnectionTest.php
tests/unit/framework/db/sqlite/SqliteConnectionTest.php
+85
-0
No files found.
docs/guide/db-dao.md
View file @
ea7bfa19
...
...
@@ -274,8 +274,8 @@ We wrap the execution of all queries in a try-catch-block to be able to handle e
We call [[yii\db\Transaction::commit()|commit()]] on success to commit the transaction and
[[yii\db\Transaction::rollBack()|rollBack()]] in case of an error. This will revert the effect of all queries
that have been executed inside of the transaction.
`throw $e` is used to re-throw the exception in case we can not handle the error oursel
fs and deli
gate it
to some other code or the yii errorhandler.
`throw $e` is used to re-throw the exception in case we can not handle the error oursel
ves and dele
gate it
to some other code or the yii error
handler.
It is also possible to nest multiple transactions, if needed:
...
...
@@ -331,13 +331,173 @@ At the time of this writing affected DBMS are MSSQL and SQLite.
> Note: SQLite only supports two isolation levels, so you can only use `READ UNCOMMITTED` and `SERIALIZABLE`.
Usage of other levels will result in an exception to be thrown.
> Note: PostgreSQL does not allow settin the isolation level before the transaction starts so you can not
> Note: PostgreSQL does not allow settin
g
the isolation level before the transaction starts so you can not
specify the isolation level directly when starting the transaction.
You have to call [[yii\db\Transaction::setIsolationLevel()]] in this case after the transaction has started.
[isolation levels]: http://en.wikipedia.org/wiki/Isolation_%28database_systems%29#Isolation_levels
Replication and Read-Write Splitting
------------------------------------
Many DBMS support [database replication](http://en.wikipedia.org/wiki/Replication_(computing)#Database_replication)
to get better database availability and faster server response time. With database replication, data are replicated
from the so-called *master servers* to *slave servers*. All writes and updates must take place on the master servers,
while reads may take place on the slave servers.
To take advantage of database replication and achieve read-write splitting, you can configure a [[yii\db\Connection]]
component like the following:
```
php
[
'class' => 'yii
\d
b
\C
onnection',
// configuration for the master
'dsn' => 'dsn for master server',
'username' => 'master',
'password' => '',
// common configuration for slaves
'slaveConfig' => [
'username' => 'slave',
'password' => '',
'attributes' => [
// use a smaller connection timeout
PDO::ATTR_TIMEOUT => 10,
],
],
// list of slave configurations
'slaves' => [
['dsn' => 'dsn for slave server 1'],
['dsn' => 'dsn for slave server 2'],
['dsn' => 'dsn for slave server 3'],
['dsn' => 'dsn for slave server 4'],
],
]
```
The above configuration specifies a setup with a single master and multiple slaves. One of the slaves will
be connected and used to perform read queries, while the master will be used to perform write queries.
Such read-write splitting is accomplished automatically with this configuration. For example,
```
php
// create a Connection instance using the above configuration
$db = Yii::createObject($config);
// query against one of the slaves
$rows = $db->createCommand('SELECT
*
FROM user LIMIT 10')->queryAll();
// query against the master
$db->createCommand("UPDATE user SET username='demo' WHERE id=1")->execute();
```
> Info: Queries performed by calling [[yii\db\Command::execute()]] are considered as write queries, while
all other queries done through one of the "query" method of [[yii\db\Command]] are read queries.
You can get the currently active slave connection via `$db->slave`.
The `Connection` component supports load balancing and failover about slaves.
When performing a read query for the first time, the `Connection` component will randomly pick a slave and
try connecting to it. If the slave is found "dead", it will try another one. If none of the slaves is available,
it will connect to the master. By configuring a [[yii\db\Connection::serverStatusCache|server status cache]],
a "dead" server can be remembered so that it will not be tried again during a
[[yii\db\Connection::serverRetryInterval|certain period of time]].
> Info: In the above configuration, a connection timeout of 10 seconds is specified for every slave.
This means if a slave cannot be reached in 10 seconds, it is considered as "dead". You can adjust this parameter
based on your actual environment.
You can also configure multiple masters with multiple slaves. For example,
```
php
[
'class' => 'yii
\d
b
\C
onnection',
// common configuration for masters
'masterConfig' => [
'username' => 'master',
'password' => '',
'attributes' => [
// use a smaller connection timeout
PDO::ATTR_TIMEOUT => 10,
],
],
// list of master configurations
'masters' => [
['dsn' => 'dsn for master server 1'],
['dsn' => 'dsn for master server 2'],
],
// common configuration for slaves
'slaveConfig' => [
'username' => 'slave',
'password' => '',
'attributes' => [
// use a smaller connection timeout
PDO::ATTR_TIMEOUT => 10,
],
],
// list of slave configurations
'slaves' => [
['dsn' => 'dsn for slave server 1'],
['dsn' => 'dsn for slave server 2'],
['dsn' => 'dsn for slave server 3'],
['dsn' => 'dsn for slave server 4'],
],
]
```
The above configuration specifies two masters and four slaves. The `Connection` component also supports
load balancing and failover about masters, like that about slaves. A difference is that in case none of
the masters is available, an exception will be thrown.
> Note: When you use the [[yii\db\Connection::masters|masters]] property to configure one or multiple
masters, all other properties for specifying a database connection (e.g. `dsn`, `username`, `password`)
with the `Connection` object itself will be ignored.
By default, transactions use the master connection. And within a transaction, all DB operations will use
the master connection. For example,
```
php
// the transaction is started on the master connection
$transaction = $db->beginTransaction();
try {
// both queries are performed against the master
$rows = $db->createCommand('SELECT
*
FROM user LIMIT 10')->queryAll();
$db->createCommand("UPDATE user SET username='demo' WHERE id=1")->execute();
$transaction->commit();
} catch(
\E
xception $e) {
$transaction->rollBack();
throw $e;
}
```
If you want to start a transaction with the slave connection, you should explicitly do so, like the following:
```
php
$transaction = $db->slave->beginTransaction();
```
Sometimes, you may want to force using the master connection to perform a read query. This can be achieved
with the `useMaster()` method:
```
php
$rows = $db->useMaster(function ($db) {
return $db->createCommand('SELECT
*
FROM user LIMIT 10')->queryAll();
});
```
You may also directly set `$db->enableSlaves` to be false to direct all queries to the master connection.
Working with database schema
----------------------------
...
...
extensions/sphinx/Command.php
View file @
ea7bfa19
...
...
@@ -191,7 +191,7 @@ class Command extends \yii\db\Command
* @param string $index the name of the index from which to take the text processing settings
* @param string $text the text to break down to keywords.
* @param boolean $fetchStatistic whether to return document and hit occurrence statistics
* @return st
ring the SQL statement for call keywords.
* @return st
atic the command object itself
*/
public
function
callKeywords
(
$index
,
$text
,
$fetchStatistic
=
false
)
{
...
...
extensions/sphinx/Connection.php
View file @
ea7bfa19
...
...
@@ -109,7 +109,6 @@ class Connection extends \yii\db\Connection
*/
public
function
createCommand
(
$sql
=
null
,
$params
=
[])
{
$this
->
open
();
$command
=
new
Command
([
'db'
=>
$this
,
'sql'
=>
$sql
,
...
...
extensions/sphinx/Schema.php
View file @
ea7bfa19
...
...
@@ -323,12 +323,11 @@ class Schema extends Object
*/
public
function
quoteValue
(
$str
)
{
if
(
!
is_string
(
$str
))
{
if
(
is_string
(
$str
))
{
return
$this
->
db
->
getSlavePdo
()
->
quote
(
$str
);
}
else
{
return
$str
;
}
$this
->
db
->
open
();
return
$this
->
db
->
pdo
->
quote
(
$str
);
}
/**
...
...
@@ -519,4 +518,15 @@ class Schema extends Object
throw
new
Exception
(
$message
,
$errorInfo
,
(
int
)
$e
->
getCode
(),
$e
);
}
}
/**
* Returns a value indicating whether a SQL statement is for read purpose.
* @param string $sql the SQL statement
* @return boolean whether a SQL statement is for read purpose.
*/
public
function
isReadQuery
(
$sql
)
{
$pattern
=
'/^\s*(SELECT|SHOW|DESCRIBE)\b/i'
;
return
preg_match
(
$pattern
,
$sql
)
>
0
;
}
}
framework/db/Command.php
View file @
ea7bfa19
...
...
@@ -68,11 +68,15 @@ 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 generate [[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
*/
private
$_sql
;
...
...
@@ -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,34 @@ 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
()
.
"
\n
Failed 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
(
$this
->
db
->
getTransaction
())
{
// master is in a transaction. use the same connection.
$forRead
=
false
;
}
if
(
$forRead
||
$forRead
===
null
&&
$this
->
db
->
getSchema
()
->
isReadQuery
(
$sql
))
{
$pdo
=
$this
->
db
->
getSlavePdo
();
}
else
{
$pdo
=
$this
->
db
->
getMasterPdo
();
}
try
{
$this
->
pdoStatement
=
$pdo
->
prepare
(
$sql
);
}
catch
(
\Exception
$e
)
{
$message
=
$e
->
getMessage
()
.
"
\n
Failed to prepare SQL:
$sql
"
;
$errorInfo
=
$e
instanceof
\PDOException
?
$e
->
errorInfo
:
null
;
throw
new
Exception
(
$message
,
$errorInfo
,
(
int
)
$e
->
getCode
(),
$e
);
}
}
...
...
@@ -184,6 +204,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
);
}
...
...
@@ -200,6 +223,18 @@ class Command extends \yii\base\Component
}
/**
* 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
* using named placeholders, this will be a parameter name of
...
...
@@ -212,11 +247,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,16 +269,17 @@ 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
);
if
(
empty
(
$values
))
{
return
$this
;
}
foreach
(
$values
as
$name
=>
$value
)
{
if
(
is_array
(
$value
))
{
$this
->
_pendingParams
[
$name
]
=
$value
;
$this
->
params
[
$name
]
=
$value
[
0
];
}
else
{
$type
=
$this
->
db
->
getSchema
()
->
getPdoType
(
$value
);
$this
->
_pendingParams
[
$name
]
=
[
$value
,
$type
];
$this
->
params
[
$name
]
=
$value
;
}
}
...
...
@@ -271,11 +306,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 +427,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
===
''
)
{
...
...
framework/db/Connection.php
View file @
ea7bfa19
...
...
@@ -21,6 +21,11 @@ use yii\caching\Cache;
* to provide data access to various DBMS in a common set of APIs. They are a thin wrapper
* of the [[PDO PHP extension]](http://www.php.net/manual/en/ref.pdo.php).
*
* Connection supports database replication and read-write splitting. In particular, a Connection component
* can be configured with multiple [[masters]] and [[slaves]]. It will do load balancing and failover by choosing
* appropriate servers. It will also automatically direct read operations to the slaves and write operations to
* the masters.
*
* To establish a DB connection, set [[dsn]], [[username]] and [[password]], and then
* call [[open()]] to be true.
*
...
...
@@ -71,9 +76,9 @@ use yii\caching\Cache;
* $transaction->rollBack();
* }
* ~~~
*
*
* You also can use shortcut for the above like the following:
*
*
* ~~~
* $connection->transaction(function() {
* $order = new Order($customer);
...
...
@@ -81,15 +86,15 @@ use yii\caching\Cache;
* $order->addItems($items);
* });
* ~~~
*
*
* If needed you can pass transaction isolation level as a second parameter:
*
*
* ~~~
* $connection->transaction(function(Connection $db) {
* //return $db->...
* }, Transaction::READ_UNCOMMITTED);
* ~~~
*
*
* Connection is often used as an application component and configured in the application
* configuration like the following:
*
...
...
@@ -105,6 +110,7 @@ use yii\caching\Cache;
* ],
* ~~~
*
*
* @property string $driverName Name of the DB driver.
* @property boolean $isActive Whether the DB connection is established. This property is read-only.
* @property string $lastInsertID The row ID of the last row inserted, or the last value retrieved from the
...
...
@@ -261,16 +267,16 @@ class Connection extends Component
* [[Schema]] class to support DBMS that is not supported by Yii.
*/
public
$schemaMap
=
[
'pgsql'
=>
'yii\db\pgsql\Schema'
,
// PostgreSQL
'mysqli'
=>
'yii\db\mysql\Schema'
,
// MySQL
'mysql'
=>
'yii\db\mysql\Schema'
,
// MySQL
'sqlite'
=>
'yii\db\sqlite\Schema'
,
// sqlite 3
'pgsql'
=>
'yii\db\pgsql\Schema'
,
// PostgreSQL
'mysqli'
=>
'yii\db\mysql\Schema'
,
// MySQL
'mysql'
=>
'yii\db\mysql\Schema'
,
// MySQL
'sqlite'
=>
'yii\db\sqlite\Schema'
,
// sqlite 3
'sqlite2'
=>
'yii\db\sqlite\Schema'
,
// sqlite 2
'sqlsrv'
=>
'yii\db\mssql\Schema'
,
// newer MSSQL driver on MS Windows hosts
'oci'
=>
'yii\db\oci\Schema'
,
// Oracle driver
'mssql'
=>
'yii\db\mssql\Schema'
,
// older MSSQL driver on MS Windows hosts
'dblib'
=>
'yii\db\mssql\Schema'
,
// dblib drivers on GNU/Linux (and maybe other OSes) hosts
'cubrid'
=>
'yii\db\cubrid\Schema'
,
// CUBRID
'sqlsrv'
=>
'yii\db\mssql\Schema'
,
// newer MSSQL driver on MS Windows hosts
'oci'
=>
'yii\db\oci\Schema'
,
// Oracle driver
'mssql'
=>
'yii\db\mssql\Schema'
,
// older MSSQL driver on MS Windows hosts
'dblib'
=>
'yii\db\mssql\Schema'
,
// dblib drivers on GNU/Linux (and maybe other OSes) hosts
'cubrid'
=>
'yii\db\cubrid\Schema'
,
// CUBRID
];
/**
* @var string Custom PDO wrapper class. If not set, it will use "PDO" or "yii\db\mssql\PDO" when MSSQL is used.
...
...
@@ -282,6 +288,72 @@ class Connection extends Component
*/
public
$enableSavepoint
=
true
;
/**
* @var Cache|string the cache object or the ID of the cache application component that is used to store
* the health status of the DB servers specified in [[masters]] and [[slaves]].
* This is used only when read/write splitting is enabled or [[masters]] is not empty.
*/
public
$serverStatusCache
=
'cache'
;
/**
* @var integer the retry interval in seconds for dead servers listed in [[masters]] and [[slaves]].
* This is used together with [[serverStatusCache]].
*/
public
$serverRetryInterval
=
600
;
/**
* @var boolean whether to enable read/write splitting by using [[slaves]] to read data.
* Note that if [[slaves]] is empty, read/write splitting will NOT be enabled no matter what value this property takes.
*/
public
$enableSlaves
=
true
;
/**
* @var array list of slave connection configurations. Each configuration is used to create a slave DB connection.
* When [[enableSlaves]] is true, one of these configurations will be chosen and used to create a DB connection
* for performing read queries only.
* @see enableSlaves
* @see slaveConfig
*/
public
$slaves
=
[];
/**
* @var array the configuration that should be merged with every slave configuration listed in [[slaves]].
* For example,
*
* ```php
* [
* 'username' => 'slave',
* 'password' => 'slave',
* 'attributes' => [
* // use a smaller connection timeout
* PDO::ATTR_TIMEOUT => 10,
* ],
* ]
* ```
*/
public
$slaveConfig
=
[];
/**
* @var array list of master connection configurations. Each configuration is used to create a master DB connection.
* When [[open()]] is called, one of these configurations will be chosen and used to create a DB connection
* which will be used by this object.
* Note that when this property is not empty, the connection setting (e.g. "dsn", "username") of this object will
* be ignored.
* @see masterConfig
*/
public
$masters
=
[];
/**
* @var array the configuration that should be merged with every master configuration listed in [[masters]].
* For example,
*
* ```php
* [
* 'username' => 'master',
* 'password' => 'master',
* 'attributes' => [
* // use a smaller connection timeout
* PDO::ATTR_TIMEOUT => 10,
* ],
* ]
* ```
*/
public
$masterConfig
=
[];
/**
* @var Transaction the currently active transaction
*/
private
$_transaction
;
...
...
@@ -293,6 +365,11 @@ class Connection extends Component
* @var string driver name
*/
private
$_driverName
;
/**
* @var Connection the currently active slave connection
*/
private
$_slave
=
false
;
/**
* Returns a value indicating whether the DB connection is established.
...
...
@@ -336,22 +413,34 @@ class Connection extends Component
*/
public
function
open
()
{
if
(
$this
->
pdo
===
null
)
{
if
(
empty
(
$this
->
dsn
))
{
throw
new
InvalidConfigException
(
'Connection::dsn cannot be empty.'
);
}
$token
=
'Opening DB connection: '
.
$this
->
dsn
;
try
{
Yii
::
trace
(
$token
,
__METHOD__
);
Yii
::
beginProfile
(
$token
,
__METHOD__
);
$this
->
pdo
=
$this
->
createPdoInstance
();
$this
->
initConnection
();
Yii
::
endProfile
(
$token
,
__METHOD__
);
}
catch
(
\PDOException
$e
)
{
Yii
::
endProfile
(
$token
,
__METHOD__
);
throw
new
Exception
(
$e
->
getMessage
(),
$e
->
errorInfo
,
(
int
)
$e
->
getCode
(),
$e
);
if
(
$this
->
pdo
!==
null
)
{
return
;
}
if
(
!
empty
(
$this
->
masters
))
{
$db
=
$this
->
openFromPool
(
$this
->
masters
,
$this
->
masterConfig
);
if
(
$db
!==
null
)
{
$this
->
pdo
=
$db
->
pdo
;
return
;
}
else
{
throw
new
InvalidConfigException
(
'None of the master DB servers is available.'
);
}
}
if
(
empty
(
$this
->
dsn
))
{
throw
new
InvalidConfigException
(
'Connection::dsn cannot be empty.'
);
}
$token
=
'Opening DB connection: '
.
$this
->
dsn
;
try
{
Yii
::
trace
(
$token
,
__METHOD__
);
Yii
::
beginProfile
(
$token
,
__METHOD__
);
$this
->
pdo
=
$this
->
createPdoInstance
();
$this
->
initConnection
();
Yii
::
endProfile
(
$token
,
__METHOD__
);
}
catch
(
\PDOException
$e
)
{
Yii
::
endProfile
(
$token
,
__METHOD__
);
throw
new
Exception
(
$e
->
getMessage
(),
$e
->
errorInfo
,
(
int
)
$e
->
getCode
(),
$e
);
}
}
/**
...
...
@@ -366,6 +455,11 @@ class Connection extends Component
$this
->
_schema
=
null
;
$this
->
_transaction
=
null
;
}
if
(
$this
->
_slave
)
{
$this
->
_slave
->
close
();
$this
->
_slave
=
null
;
}
}
/**
...
...
@@ -420,7 +514,6 @@ class Connection extends Component
*/
public
function
createCommand
(
$sql
=
null
,
$params
=
[])
{
$this
->
open
();
$command
=
new
Command
([
'db'
=>
$this
,
'sql'
=>
$sql
,
...
...
@@ -608,8 +701,7 @@ class Connection extends Component
if
((
$pos
=
strpos
(
$this
->
dsn
,
':'
))
!==
false
)
{
$this
->
_driverName
=
strtolower
(
substr
(
$this
->
dsn
,
0
,
$pos
));
}
else
{
$this
->
open
();
$this
->
_driverName
=
strtolower
(
$this
->
pdo
->
getAttribute
(
PDO
::
ATTR_DRIVER_NAME
));
$this
->
_driverName
=
strtolower
(
$this
->
getSlavePdo
()
->
getAttribute
(
PDO
::
ATTR_DRIVER_NAME
));
}
}
return
$this
->
_driverName
;
...
...
@@ -623,4 +715,130 @@ class Connection extends Component
{
$this
->
_driverName
=
strtolower
(
$driverName
);
}
/**
* Returns the PDO instance for the currently active slave connection.
* When [[enableSlaves]] is true, one of the slaves will be used for read queries, and its PDO instance
* will be returned by this method.
* @param boolean $fallbackToMaster whether to return a master PDO in case none of the slave connections is available.
* @return PDO the PDO instance for the currently active slave connection. Null is returned if no slave connection
* is available and `$fallbackToMaster` is false.
*/
public
function
getSlavePdo
(
$fallbackToMaster
=
true
)
{
$db
=
$this
->
getSlave
(
false
);
if
(
$db
===
null
)
{
return
$fallbackToMaster
?
$this
->
getMasterPdo
()
:
null
;
}
else
{
return
$db
->
pdo
;
}
}
/**
* Returns the PDO instance for the currently active master connection.
* This method will open the master DB connection and then return [[pdo]].
* @return PDO the PDO instance for the currently active master connection.
*/
public
function
getMasterPdo
()
{
$this
->
open
();
return
$this
->
pdo
;
}
/**
* Returns the currently active slave connection.
* If this method is called the first time, it will try to open a slave connection when [[enableSlaves]] is true.
* @param boolean $fallbackToMaster whether to return a master connection in case there is no slave connection available.
* @return Connection the currently active slave connection. Null is returned if there is slave available and
* `$fallbackToMaster` is false.
*/
public
function
getSlave
(
$fallbackToMaster
=
true
)
{
if
(
!
$this
->
enableSlaves
)
{
return
$fallbackToMaster
?
$this
:
null
;
}
if
(
$this
->
_slave
===
false
)
{
$this
->
_slave
=
$this
->
openFromPool
(
$this
->
slaves
,
$this
->
slaveConfig
);
}
return
$this
->
_slave
===
null
&&
$fallbackToMaster
?
$this
:
$this
->
_slave
;
}
/**
* Executes the provided callback by using the master connection.
*
* This method is provided so that you can temporarily force using the master connection to perform
* DB operations even if they are read queries. For example,
*
* ```php
* $result = $db->useMaster(function ($db) {
* return $db->createCommand('SELECT * FROM user LIMIT 1')->queryOne();
* });
* ```
*
* @param callable $callback a PHP callable to be executed by this method. Its signature is
* `function (Connection $db)`. Its return value will be returned by this method.
* @return mixed the return value of the callback
*/
public
function
useMaster
(
callable
$callback
)
{
$enableSlave
=
$this
->
enableSlaves
;
$this
->
enableSlaves
=
false
;
$result
=
call_user_func
(
$callback
,
$this
);
$this
->
enableSlaves
=
$enableSlave
;
return
$result
;
}
/**
* Opens the connection to a server in the pool.
* This method implements the load balancing among the given list of the servers.
* @param array $pool the list of connection configurations in the server pool
* @param array $sharedConfig the configuration common to those given in `$pool`.
* @return Connection the opened DB connection, or null if no server is available
* @throws InvalidConfigException if a configuration does not specify "dsn"
*/
protected
function
openFromPool
(
array
$pool
,
array
$sharedConfig
)
{
if
(
empty
(
$pool
))
{
return
null
;
}
if
(
!
isset
(
$sharedConfig
[
'class'
]))
{
$sharedConfig
[
'class'
]
=
get_class
(
$this
);
}
$cache
=
is_string
(
$this
->
serverStatusCache
)
?
Yii
::
$app
->
get
(
$this
->
serverStatusCache
,
false
)
:
$this
->
serverStatusCache
;
shuffle
(
$pool
);
foreach
(
$pool
as
$config
)
{
$config
=
array_merge
(
$sharedConfig
,
$config
);
if
(
empty
(
$config
[
'dsn'
]))
{
throw
new
InvalidConfigException
(
'The "dsn" option must be specified.'
);
}
$key
=
[
__METHOD__
,
$config
[
'dsn'
]];
if
(
$cache
instanceof
Cache
&&
$cache
->
get
(
$key
))
{
// should not try this dead server now
continue
;
}
/* @var $db Connection */
$db
=
Yii
::
createObject
(
$config
);
try
{
$db
->
open
();
return
$db
;
}
catch
(
\Exception
$e
)
{
Yii
::
warning
(
"Connection (
{
$config
[
'dsn'
]
}
) failed: "
.
$e
->
getMessage
(),
__METHOD__
);
if
(
$cache
instanceof
Cache
)
{
// mark this server as dead and only retry it after the specified interval
$cache
->
set
(
$key
,
1
,
$this
->
serverRetryInterval
);
}
}
}
return
null
;
}
}
framework/db/Schema.php
View file @
ea7bfa19
...
...
@@ -369,11 +369,10 @@ abstract class Schema extends Object
return
$str
;
}
$this
->
db
->
open
();
if
((
$value
=
$this
->
db
->
pdo
->
quote
(
$str
))
!==
false
)
{
if
((
$value
=
$this
->
db
->
getSlavePdo
()
->
quote
(
$str
))
!==
false
)
{
return
$value
;
}
else
{
// the driver doesn't support quote (e.g. oci)
}
else
{
// the driver doesn't support quote (e.g. oci)
return
"'"
.
addcslashes
(
str_replace
(
"'"
,
"''"
,
$str
),
"
\000\n\r\\\032
"
)
.
"'"
;
}
}
...
...
@@ -520,4 +519,15 @@ abstract class Schema extends Object
throw
new
$exceptionClass
(
$message
,
$errorInfo
,
(
int
)
$e
->
getCode
(),
$e
);
}
}
/**
* Returns a value indicating whether a SQL statement is for read purpose.
* @param string $sql the SQL statement
* @return boolean whether a SQL statement is for read purpose.
*/
public
function
isReadQuery
(
$sql
)
{
$pattern
=
'/^\s*(SELECT|SHOW|DESCRIBE)\b/i'
;
return
preg_match
(
$pattern
,
$sql
)
>
0
;
}
}
framework/db/cubrid/Schema.php
View file @
ea7bfa19
...
...
@@ -116,13 +116,14 @@ class Schema extends \yii\db\Schema
return
$str
;
}
$this
->
db
->
open
();
$pdo
=
$this
->
db
->
getSlavePdo
();
// 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
);
}
}
...
...
@@ -142,8 +143,9 @@ class Schema extends \yii\db\Schema
*/
protected
function
loadTableSchema
(
$name
)
{
$this
->
db
->
open
();
$tableInfo
=
$this
->
db
->
pdo
->
cubrid_schema
(
\PDO
::
CUBRID_SCH_TABLE
,
$name
);
$pdo
=
$this
->
db
->
getSlavePdo
();
$tableInfo
=
$pdo
->
cubrid_schema
(
\PDO
::
CUBRID_SCH_TABLE
,
$name
);
if
(
!
isset
(
$tableInfo
[
0
][
'NAME'
]))
{
return
null
;
...
...
@@ -160,7 +162,7 @@ class Schema extends \yii\db\Schema
$table
->
columns
[
$column
->
name
]
=
$column
;
}
$primaryKeys
=
$
this
->
db
->
pdo
->
cubrid_schema
(
\PDO
::
CUBRID_SCH_PRIMARY_KEY
,
$table
->
name
);
$primaryKeys
=
$pdo
->
cubrid_schema
(
\PDO
::
CUBRID_SCH_PRIMARY_KEY
,
$table
->
name
);
foreach
(
$primaryKeys
as
$key
)
{
$column
=
$table
->
columns
[
$key
[
'ATTR_NAME'
]];
$column
->
isPrimaryKey
=
true
;
...
...
@@ -170,7 +172,7 @@ class Schema extends \yii\db\Schema
}
}
$foreignKeys
=
$
this
->
db
->
pdo
->
cubrid_schema
(
\PDO
::
CUBRID_SCH_IMPORTED_KEYS
,
$table
->
name
);
$foreignKeys
=
$pdo
->
cubrid_schema
(
\PDO
::
CUBRID_SCH_IMPORTED_KEYS
,
$table
->
name
);
foreach
(
$foreignKeys
as
$key
)
{
if
(
isset
(
$table
->
foreignKeys
[
$key
[
'FK_NAME'
]]))
{
$table
->
foreignKeys
[
$key
[
'FK_NAME'
]][
$key
[
'FKCOLUMN_NAME'
]]
=
$key
[
'PKCOLUMN_NAME'
];
...
...
@@ -264,8 +266,8 @@ class Schema extends \yii\db\Schema
*/
protected
function
findTableNames
(
$schema
=
''
)
{
$
this
->
db
->
open
();
$tables
=
$this
->
db
->
pdo
->
cubrid_schema
(
\PDO
::
CUBRID_SCH_TABLE
);
$
pdo
=
$this
->
db
->
getSlavePdo
();
$tables
=
$
pdo
->
cubrid_schema
(
\PDO
::
CUBRID_SCH_TABLE
);
$tableNames
=
[];
foreach
(
$tables
as
$table
)
{
// do not list system tables
...
...
framework/db/mssql/QueryBuilder.php
View file @
ea7bfa19
...
...
@@ -239,8 +239,8 @@ class QueryBuilder extends \yii\db\QueryBuilder
*/
protected
function
isOldMssql
()
{
$
this
->
db
->
open
();
$version
=
preg_split
(
"/\./"
,
$
this
->
db
->
pdo
->
getAttribute
(
\PDO
::
ATTR_SERVER_VERSION
));
$
pdo
=
$this
->
db
->
getSlavePdo
();
$version
=
preg_split
(
"/\./"
,
$pdo
->
getAttribute
(
\PDO
::
ATTR_SERVER_VERSION
));
return
$version
[
0
]
<
11
;
}
}
framework/db/oci/QueryBuilder.php
View file @
ea7bfa19
...
...
@@ -8,6 +8,7 @@
namespace
yii\db\oci
;
use
yii\base\InvalidParamException
;
use
yii\db\Connection
;
/**
* QueryBuilder is the query builder for Oracle databases.
...
...
@@ -132,8 +133,10 @@ EOD;
if
(
$value
!==
null
)
{
$value
=
(
int
)
$value
;
}
else
{
$value
=
(
int
)
$this
->
db
->
createCommand
(
"SELECT MAX(
\"
{
$tableSchema
->
primaryKey
}
\"
) FROM
\"
{
$tableSchema
->
name
}
\"
"
)
->
queryScalar
();
$value
++
;
// use master connection to get the biggest PK value
$value
=
$this
->
db
->
useMaster
(
function
(
Connection
$db
)
use
(
$tableSchema
)
{
return
$db
->
createCommand
(
"SELECT MAX(
\"
{
$tableSchema
->
primaryKey
}
\"
) FROM
\"
{
$tableSchema
->
name
}
\"
"
)
->
queryScalar
();
})
+
1
;
}
return
"DROP SEQUENCE
\"
{
$tableSchema
->
name
}
_SEQ
\"
;"
...
...
framework/db/oci/Schema.php
View file @
ea7bfa19
...
...
@@ -8,6 +8,7 @@
namespace
yii\db\oci
;
use
yii\base\InvalidCallException
;
use
yii\db\Connection
;
use
yii\db\TableSchema
;
use
yii\db\ColumnSchema
;
...
...
@@ -195,7 +196,10 @@ EOD;
public
function
getLastInsertID
(
$sequenceName
=
''
)
{
if
(
$this
->
db
->
isActive
)
{
return
$this
->
db
->
createCommand
(
"SELECT
{
$sequenceName
}
.CURRVAL FROM DUAL"
)
->
queryScalar
();
// get the last insert id from the master connection
return
$this
->
db
->
useMaster
(
function
(
Connection
$db
)
use
(
$sequenceName
)
{
return
$db
->
createCommand
(
"SELECT
{
$sequenceName
}
.CURRVAL FROM DUAL"
)
->
queryScalar
();
});
}
else
{
throw
new
InvalidCallException
(
'DB Connection is not active.'
);
}
...
...
framework/db/pgsql/QueryBuilder.php
View file @
ea7bfa19
...
...
@@ -136,8 +136,8 @@ class QueryBuilder extends \yii\db\QueryBuilder
$command
.=
"ALTER TABLE
$tableName
$enable
TRIGGER ALL; "
;
}
#
enable to have ability to alter several tables
$this
->
db
->
pdo
->
setAttribute
(
\PDO
::
ATTR_EMULATE_PREPARES
,
true
);
//
enable to have ability to alter several tables
$this
->
db
->
getMasterPdo
()
->
setAttribute
(
\PDO
::
ATTR_EMULATE_PREPARES
,
true
);
return
$command
;
}
...
...
framework/db/sqlite/QueryBuilder.php
View file @
ea7bfa19
...
...
@@ -7,6 +7,7 @@
namespace
yii\db\sqlite
;
use
yii\db\Connection
;
use
yii\db\Exception
;
use
yii\base\InvalidParamException
;
use
yii\base\NotSupportedException
;
...
...
@@ -120,7 +121,9 @@ class QueryBuilder extends \yii\db\QueryBuilder
if
(
$value
===
null
)
{
$key
=
reset
(
$table
->
primaryKey
);
$tableName
=
$db
->
quoteTableName
(
$tableName
);
$value
=
$db
->
createCommand
(
"SELECT MAX('
$key
') FROM
$tableName
"
)
->
queryScalar
();
$value
=
$this
->
db
->
useMaster
(
function
(
Connection
$db
)
use
(
$key
,
$tableName
)
{
return
$db
->
createCommand
(
"SELECT MAX('
$key
') FROM
$tableName
"
)
->
queryScalar
();
});
}
else
{
$value
=
(
int
)
$value
-
1
;
}
...
...
tests/unit/framework/db/DatabaseTestCase.php
View file @
ea7bfa19
...
...
@@ -44,26 +44,35 @@ abstract class DatabaseTestCase extends TestCase
if
(
!
$reset
&&
$this
->
db
)
{
return
$this
->
db
;
}
$db
=
new
\yii\db\Connection
;
$db
->
dsn
=
$this
->
database
[
'dsn'
];
if
(
isset
(
$this
->
database
[
'username'
]))
{
$db
->
username
=
$this
->
database
[
'username'
];
$db
->
password
=
$this
->
database
[
'password'
];
$config
=
$this
->
database
;
if
(
isset
(
$config
[
'fixture'
]))
{
$fixture
=
$config
[
'fixture'
];
unset
(
$config
[
'fixture'
]);
}
else
{
$fixture
=
null
;
}
if
(
isset
(
$this
->
database
[
'attributes'
]))
{
$db
->
attributes
=
$this
->
database
[
'attributes'
];
return
$this
->
db
=
$this
->
prepareDatabase
(
$config
,
$fixture
,
$open
);
}
public
function
prepareDatabase
(
$config
,
$fixture
,
$open
=
true
)
{
if
(
!
isset
(
$config
[
'class'
]))
{
$config
[
'class'
]
=
'yii\db\Connection'
;
}
if
(
$open
)
{
$db
->
open
();
$lines
=
explode
(
';'
,
file_get_contents
(
$this
->
database
[
'fixture'
]));
/* @var $db \yii\db\Connection */
$db
=
\Yii
::
createObject
(
$config
);
if
(
!
$open
)
{
return
$db
;
}
$db
->
open
();
if
(
$fixture
!==
null
)
{
$lines
=
explode
(
';'
,
file_get_contents
(
$fixture
));
foreach
(
$lines
as
$line
)
{
if
(
trim
(
$line
)
!==
''
)
{
$db
->
pdo
->
exec
(
$line
);
}
}
}
$this
->
db
=
$db
;
return
$db
;
}
}
tests/unit/framework/db/sqlite/SqliteConnectionTest.php
View file @
ea7bfa19
<?php
namespace
yiiunit\framework\db\sqlite
;
use
yii\db\Connection
;
use
yii\db\Transaction
;
use
yiiunit\framework\db\ConnectionTest
;
use
yiiunit\data\ar\ActiveRecord
;
use
yiiunit\data\ar\Customer
;
/**
* @group db
...
...
@@ -57,4 +60,86 @@ class SqliteConnectionTest extends ConnectionTest
$transaction
=
$connection
->
beginTransaction
(
Transaction
::
SERIALIZABLE
);
$transaction
->
rollBack
();
}
public
function
testMasterSlave
()
{
$counts
=
[[
0
,
2
],
[
1
,
2
],
[
2
,
2
]];
foreach
(
$counts
as
$count
)
{
list
(
$masterCount
,
$slaveCount
)
=
$count
;
$db
=
$this
->
prepareMasterSlave
(
$masterCount
,
$slaveCount
);
$this
->
assertTrue
(
$db
->
getSlave
()
instanceof
Connection
);
$this
->
assertTrue
(
$db
->
getSlave
()
->
isActive
);
$this
->
assertFalse
(
$db
->
isActive
);
// test SELECT uses slave
$this
->
assertEquals
(
2
,
$db
->
createCommand
(
'SELECT COUNT(*) FROM profile'
)
->
queryScalar
());
$this
->
assertFalse
(
$db
->
isActive
);
// test UPDATE uses master
$db
->
createCommand
(
"UPDATE profile SET description='test' WHERE id=1"
)
->
execute
();
$this
->
assertTrue
(
$db
->
isActive
);
$this
->
assertNotEquals
(
'test'
,
$db
->
createCommand
(
"SELECT description FROM profile WHERE id=1"
)
->
queryScalar
());
$result
=
$db
->
useMaster
(
function
(
Connection
$db
)
{
return
$db
->
createCommand
(
"SELECT description FROM profile WHERE id=1"
)
->
queryScalar
();
});
$this
->
assertEquals
(
'test'
,
$result
);
// test ActiveRecord read/write split
ActiveRecord
::
$db
=
$db
=
$this
->
prepareMasterSlave
(
$masterCount
,
$slaveCount
);
$this
->
assertFalse
(
$db
->
isActive
);
$customer
=
Customer
::
findOne
(
1
);
$this
->
assertTrue
(
$customer
instanceof
Customer
);
$this
->
assertEquals
(
'user1'
,
$customer
->
name
);
$this
->
assertFalse
(
$db
->
isActive
);
$customer
->
name
=
'test'
;
$customer
->
save
();
$this
->
assertTrue
(
$db
->
isActive
);
$customer
=
Customer
::
findOne
(
1
);
$this
->
assertTrue
(
$customer
instanceof
Customer
);
$this
->
assertEquals
(
'user1'
,
$customer
->
name
);
$result
=
$db
->
useMaster
(
function
()
{
return
Customer
::
findOne
(
1
)
->
name
;
});
$this
->
assertEquals
(
'test'
,
$result
);
}
}
/**
* @param integer $masterCount
* @param integer $slaveCount
* @return Connection
*/
protected
function
prepareMasterSlave
(
$masterCount
,
$slaveCount
)
{
$databases
=
self
::
getParam
(
'databases'
);
$fixture
=
$databases
[
$this
->
driverName
][
'fixture'
];
$basePath
=
\Yii
::
getAlias
(
'@yiiunit/runtime'
);
$config
=
[
'class'
=>
'yii\db\Connection'
,
'dsn'
=>
'sqlite:memory:'
,
];
$this
->
prepareDatabase
(
$config
,
$fixture
)
->
close
();
for
(
$i
=
0
;
$i
<
$masterCount
;
++
$i
)
{
$master
=
[
'dsn'
=>
"sqlite:
$basePath
/yii2test_master
{
$i
}
.sq3"
];
$db
=
$this
->
prepareDatabase
(
$master
,
$fixture
);
$db
->
close
();
$config
[
'masters'
][]
=
$master
;
}
for
(
$i
=
0
;
$i
<
$slaveCount
;
++
$i
)
{
$slave
=
[
'dsn'
=>
"sqlite:
$basePath
/yii2test_slave
{
$i
}
.sq3"
];
$db
=
$this
->
prepareDatabase
(
$slave
,
$fixture
);
$db
->
close
();
$config
[
'slaves'
][]
=
$slave
;
}
return
\Yii
::
createObject
(
$config
);
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment