mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
API Use mysql buffered statements
Avoids the usage of any MySQL Native Driver specific API
This commit is contained in:
parent
d3f5d491e6
commit
e8d6f15f28
@ -8,43 +8,28 @@
|
|||||||
*/
|
*/
|
||||||
class MySQLQuery extends SS_Query {
|
class MySQLQuery extends SS_Query {
|
||||||
|
|
||||||
/**
|
|
||||||
* The MySQLiConnector object that created this result set.
|
|
||||||
*
|
|
||||||
* @var MySQLiConnector
|
|
||||||
*/
|
|
||||||
protected $database;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The internal MySQL handle that points to the result set.
|
* The internal MySQL handle that points to the result set.
|
||||||
|
* Select queries will have mysqli_result as a value.
|
||||||
|
* Non-select queries will not
|
||||||
*
|
*
|
||||||
* @var mysqli_result
|
* @var mixed
|
||||||
*/
|
*/
|
||||||
protected $handle;
|
protected $handle;
|
||||||
|
|
||||||
/**
|
|
||||||
* The related mysqli statement object if generated using a prepared query
|
|
||||||
*
|
|
||||||
* @var mysqli_stmt
|
|
||||||
*/
|
|
||||||
protected $statement;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
|
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
|
||||||
* @param MySQLDatabase $database The database object that created this query.
|
*
|
||||||
* @param mysqli_result $handle the internal mysql handle that is points to the resultset.
|
* @param MySQLiConnector $database The database object that created this query.
|
||||||
* @param mysqli_stmt $statement The related statement, if present
|
* @param mixed $handle the internal mysql handle that is points to the resultset.
|
||||||
|
* Non-mysqli_result values could be given for non-select queries (e.g. true)
|
||||||
*/
|
*/
|
||||||
public function __construct(MySQLiConnector $database, $handle = null, $statement = null) {
|
public function __construct($database, $handle) {
|
||||||
$this->database = $database;
|
|
||||||
$this->handle = $handle;
|
$this->handle = $handle;
|
||||||
$this->statement = $statement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function __destruct() {
|
public function __destruct() {
|
||||||
if (is_object($this->handle)) $this->handle->free();
|
if (is_object($this->handle)) $this->handle->free();
|
||||||
// Don't close statement as these may be re-used across the life of this request
|
|
||||||
// if (is_object($this->statement)) $this->statement->close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function seek($row) {
|
public function seek($row) {
|
||||||
|
116
model/connect/MySQLStatement.php
Normal file
116
model/connect/MySQLStatement.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a record-view for mysqli statements
|
||||||
|
*
|
||||||
|
* By default streams unbuffered data, but seek(), rewind(), or numRecords() will force the statement to
|
||||||
|
* buffer itself and sacrifice any potential performance benefit.
|
||||||
|
*/
|
||||||
|
class MySQLStatement extends SS_Query {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The related mysqli statement object if generated using a prepared query
|
||||||
|
*
|
||||||
|
* @var mysqli_stmt
|
||||||
|
*/
|
||||||
|
protected $statement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata result for this statement
|
||||||
|
*
|
||||||
|
* @var mysqli_result
|
||||||
|
*/
|
||||||
|
protected $metadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is the statement bound to the current resultset?
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $bound = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of column names
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $columns = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of bound variables in the current row
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $boundValues = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds this statement to the variables
|
||||||
|
*/
|
||||||
|
protected function bind() {
|
||||||
|
$variables = array();
|
||||||
|
|
||||||
|
// Bind each field
|
||||||
|
while($field = $this->metadata->fetch_field()) {
|
||||||
|
$this->columns[] = $field->name;
|
||||||
|
// Note that while boundValues isn't initialised at this point,
|
||||||
|
// later calls to $this->statement->fetch() Will populate
|
||||||
|
// $this->boundValues later with the next result.
|
||||||
|
$variables[] = &$this->boundValues[$field->name];
|
||||||
|
}
|
||||||
|
|
||||||
|
call_user_func_array(array($this->statement, 'bind_result'), $variables);
|
||||||
|
$this->bound = true;
|
||||||
|
$this->metadata->free();
|
||||||
|
|
||||||
|
// Buffer all results
|
||||||
|
$this->statement->store_result();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook the result-set given into a Query class, suitable for use by SilverStripe.
|
||||||
|
* @param mysqli_stmt $statement The related statement, if present
|
||||||
|
* @param mysqli_result $metadata The metadata for this statement
|
||||||
|
*/
|
||||||
|
public function __construct($statement, $metadata) {
|
||||||
|
$this->statement = $statement;
|
||||||
|
$this->metadata = $metadata;
|
||||||
|
|
||||||
|
// Immediately bind and buffer
|
||||||
|
$this->bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __destruct() {
|
||||||
|
$this->statement->close();
|
||||||
|
$this->closed = true;
|
||||||
|
$this->currentRecord = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function seek($row) {
|
||||||
|
$this->rowNum = $row - 1;
|
||||||
|
$this->statement->data_seek($row);
|
||||||
|
return $this->next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function numRecords() {
|
||||||
|
return $this->statement->num_rows();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nextRecord() {
|
||||||
|
// Skip data if out of data
|
||||||
|
if (!$this->statement->fetch()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dereferenced row
|
||||||
|
$row = array();
|
||||||
|
foreach($this->boundValues as $key => $value) {
|
||||||
|
$row[$key] = $value;
|
||||||
|
}
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rewind() {
|
||||||
|
return $this->seek(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -33,7 +33,7 @@ class MySQLiConnector extends DBConnector {
|
|||||||
*
|
*
|
||||||
* @param mysqli_stmt $statement
|
* @param mysqli_stmt $statement
|
||||||
*/
|
*/
|
||||||
public function setLastStatement($statement) {
|
protected function setLastStatement($statement) {
|
||||||
$this->lastStatement = $statement;
|
$this->lastStatement = $statement;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,8 +45,9 @@ class MySQLiConnector extends DBConnector {
|
|||||||
* @return mysqli_stmt
|
* @return mysqli_stmt
|
||||||
*/
|
*/
|
||||||
public function prepareStatement($sql, &$success) {
|
public function prepareStatement($sql, &$success) {
|
||||||
// Prepare statement with arguments
|
// Record last statement for error reporting
|
||||||
$statement = $this->dbConn->stmt_init();
|
$statement = $this->dbConn->stmt_init();
|
||||||
|
$this->setLastStatement($statement);
|
||||||
$success = $statement->prepare($sql);
|
$success = $statement->prepare($sql);
|
||||||
return $statement;
|
return $statement;
|
||||||
}
|
}
|
||||||
@ -116,7 +117,7 @@ class MySQLiConnector extends DBConnector {
|
|||||||
// Benchmark query
|
// Benchmark query
|
||||||
$conn = $this->dbConn;
|
$conn = $this->dbConn;
|
||||||
$handle = $this->benchmarkQuery($sql, function($sql) use($conn) {
|
$handle = $this->benchmarkQuery($sql, function($sql) use($conn) {
|
||||||
return $conn->query($sql);
|
return $conn->query($sql, MYSQLI_STORE_RESULT);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!$handle || $this->dbConn->error) {
|
if (!$handle || $this->dbConn->error) {
|
||||||
@ -124,10 +125,8 @@ class MySQLiConnector extends DBConnector {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if($handle !== true) {
|
// Some non-select queries return true on success
|
||||||
// Some non-select queries return true on success
|
return new MySQLQuery($this, $handle);
|
||||||
return new MySQLQuery($this, $handle);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -222,9 +221,11 @@ class MySQLiConnector extends DBConnector {
|
|||||||
$lastStatement = $this->benchmarkQuery($sql, function($sql) use($parsedParameters, $blobs, $self) {
|
$lastStatement = $this->benchmarkQuery($sql, function($sql) use($parsedParameters, $blobs, $self) {
|
||||||
|
|
||||||
$statement = $self->prepareStatement($sql, $success);
|
$statement = $self->prepareStatement($sql, $success);
|
||||||
if(!$success) return $statement;
|
if(!$success) return null;
|
||||||
|
|
||||||
$self->bindParameters($statement, $parsedParameters);
|
if($parsedParameters) {
|
||||||
|
$self->bindParameters($statement, $parsedParameters);
|
||||||
|
}
|
||||||
|
|
||||||
// Bind any blobs given
|
// Bind any blobs given
|
||||||
foreach($blobs as $blob) {
|
foreach($blobs as $blob) {
|
||||||
@ -235,18 +236,19 @@ class MySQLiConnector extends DBConnector {
|
|||||||
$statement->execute();
|
$statement->execute();
|
||||||
return $statement;
|
return $statement;
|
||||||
});
|
});
|
||||||
|
|
||||||
// check result
|
|
||||||
$this->setLastStatement($lastStatement);
|
|
||||||
if (!$lastStatement || $lastStatement->error) {
|
if (!$lastStatement || $lastStatement->error) {
|
||||||
$values = $this->parameterValues($parameters);
|
$values = $this->parameterValues($parameters);
|
||||||
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
|
$this->databaseError($this->getLastError(), $errorLevel, $sql, $values);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// May not return result for non-select statements
|
// Non-select queries will have no result data
|
||||||
if($result = $lastStatement->get_result()) {
|
if($lastStatement && ($metaData = $lastStatement->result_metadata())) {
|
||||||
return new MySQLQuery($this, $result, $lastStatement);
|
return new MySQLStatement($lastStatement, $metaData);
|
||||||
|
} else {
|
||||||
|
// Replicate normal behaviour of ->query() on non-select calls
|
||||||
|
return new MySQLQuery($this, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,46 +5,120 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class MySQLDatabaseTest extends SapphireTest {
|
class MySQLDatabaseTest extends SapphireTest {
|
||||||
|
|
||||||
|
protected static $fixture_file = 'MySQLDatabaseTest.yml';
|
||||||
|
|
||||||
protected $extraDataObjects = array(
|
protected $extraDataObjects = array(
|
||||||
'MySQLDatabaseTest_DO',
|
'MySQLDatabaseTest_Data'
|
||||||
);
|
);
|
||||||
|
|
||||||
public function setUp() {
|
public function testPreparedStatements() {
|
||||||
if(DB::get_conn() instanceof MySQLDatabase) {
|
if(!(DB::get_connector() instanceof MySQLiConnector)) {
|
||||||
MySQLDatabaseTest_DO::config()->db = array(
|
$this->markTestSkipped('This test requires the current DB connector is MySQLi');
|
||||||
'MultiEnum1' => 'MultiEnum("A, B, C, D","")',
|
|
||||||
'MultiEnum2' => 'MultiEnum("A, B, C, D","A")',
|
|
||||||
'MultiEnum3' => 'MultiEnum("A, B, C, D","A, B")',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
$this->markTestSkipped('This test requires the Config API to be immutable');
|
|
||||||
parent::setUp();
|
// Test preparation of equivalent statemetns
|
||||||
|
$result1 = DB::get_connector()->preparedQuery(
|
||||||
|
'SELECT "Sort", "Title" FROM "MySQLDatabaseTest_Data" WHERE "Sort" > ? ORDER BY "Sort"',
|
||||||
|
array(0)
|
||||||
|
);
|
||||||
|
|
||||||
|
$result2 = DB::get_connector()->preparedQuery(
|
||||||
|
'SELECT "Sort", "Title" FROM "MySQLDatabaseTest_Data" WHERE "Sort" > ? ORDER BY "Sort"',
|
||||||
|
array(2)
|
||||||
|
);
|
||||||
|
$this->assertInstanceOf('MySQLStatement', $result1);
|
||||||
|
$this->assertInstanceOf('MySQLStatement', $result2);
|
||||||
|
|
||||||
|
// Also select non-prepared statement
|
||||||
|
$result3 = DB::get_connector()->query('SELECT "Sort", "Title" FROM "MySQLDatabaseTest_Data" ORDER BY "Sort"');
|
||||||
|
$this->assertInstanceOf('MySQLQuery', $result3);
|
||||||
|
|
||||||
|
// Iterating one level should not buffer, but return the right result
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 1,
|
||||||
|
'Title' => 'First Item'
|
||||||
|
),
|
||||||
|
$result1->next()
|
||||||
|
);
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 2,
|
||||||
|
'Title' => 'Second Item'
|
||||||
|
),
|
||||||
|
$result1->next()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test first
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 1,
|
||||||
|
'Title' => 'First Item'
|
||||||
|
),
|
||||||
|
$result1->first()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test seek
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 2,
|
||||||
|
'Title' => 'Second Item'
|
||||||
|
),
|
||||||
|
$result1->seek(1)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test count
|
||||||
|
$this->assertEquals(4, $result1->numRecords());
|
||||||
|
|
||||||
|
// Test second statement
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 3,
|
||||||
|
'Title' => 'Third Item'
|
||||||
|
),
|
||||||
|
$result2->next()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test non-prepared query
|
||||||
|
$this->assertEquals(
|
||||||
|
array(
|
||||||
|
'Sort' => 1,
|
||||||
|
'Title' => 'First Item'
|
||||||
|
),
|
||||||
|
$result3->next()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function testAffectedRows() {
|
||||||
* Check that once a schema has been generated, then it doesn't need any more updating
|
if(!(DB::get_connector() instanceof MySQLiConnector)) {
|
||||||
*/
|
$this->markTestSkipped('This test requires the current DB connector is MySQLi');
|
||||||
public function testFieldsDontRerequestChanges() {
|
|
||||||
// These are MySQL specific :-S
|
|
||||||
if(DB::get_conn() instanceof MySQLDatabase) {
|
|
||||||
$schema = DB::get_schema();
|
|
||||||
$test = $this;
|
|
||||||
DB::quiet();
|
|
||||||
|
|
||||||
// Verify that it doesn't need to be recreated
|
|
||||||
$schema->schemaUpdate(function() use ($test, $schema) {
|
|
||||||
$obj = new MySQLDatabaseTest_DO();
|
|
||||||
$obj->requireTable();
|
|
||||||
$needsUpdating = $schema->doesSchemaNeedUpdating();
|
|
||||||
$schema->cancelSchemaUpdate();
|
|
||||||
|
|
||||||
$test->assertFalse($needsUpdating);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$query = new SQLUpdate('MySQLDatabaseTest_Data');
|
||||||
|
$query->setAssignments(array('Title' => 'New Title'));
|
||||||
|
|
||||||
|
// Test update which affects no rows
|
||||||
|
$query->setWhere(array('Title' => 'Bob'));
|
||||||
|
$result = $query->execute();
|
||||||
|
$this->assertInstanceOf('MySQLQuery', $result);
|
||||||
|
$this->assertEquals(0, DB::affected_rows());
|
||||||
|
|
||||||
|
// Test update which affects some rows
|
||||||
|
$query->setWhere(array('Title' => 'First Item'));
|
||||||
|
$result = $query->execute();
|
||||||
|
$this->assertInstanceOf('MySQLQuery', $result);
|
||||||
|
$this->assertEquals(1, DB::affected_rows());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MySQLDatabaseTest_DO extends DataObject implements TestOnly {
|
class MySQLDatabaseTest_Data extends DataObject implements TestOnly {
|
||||||
private static $db = array();
|
private static $db = array(
|
||||||
|
'Title' => 'Varchar',
|
||||||
|
'Description' => 'Text',
|
||||||
|
'Enabled' => 'Boolean',
|
||||||
|
'Sort' => 'Int'
|
||||||
|
);
|
||||||
|
|
||||||
|
private static $default_sort = '"Sort" ASC';
|
||||||
}
|
}
|
||||||
|
17
tests/model/MySQLDatabaseTest.yml
Normal file
17
tests/model/MySQLDatabaseTest.yml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
MySQLDatabaseTest_Data:
|
||||||
|
data1:
|
||||||
|
Title: 'First Item'
|
||||||
|
Description: 'The content'
|
||||||
|
Sort: 1
|
||||||
|
data2:
|
||||||
|
Title: 'Second Item'
|
||||||
|
Description: 'More Content'
|
||||||
|
Sort: 2
|
||||||
|
data3:
|
||||||
|
Title: 'Third Item'
|
||||||
|
Description: ''
|
||||||
|
Sort: 3
|
||||||
|
data4:
|
||||||
|
Title: 'Last Item'
|
||||||
|
Description: 'Testing'
|
||||||
|
Sort: 4
|
Loading…
Reference in New Issue
Block a user