From bf1da42daf0773415911c6bb50149fa8ee971610 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 22 Oct 2011 18:34:41 +0200 Subject: [PATCH 1/2] DDC-217 - Add Result Cache feature --- lib/Doctrine/DBAL/Cache/ArrayStatement.php | 82 ++++++ lib/Doctrine/DBAL/Cache/RowCacheStatement.php | 256 ++++++++++++++++++ lib/Doctrine/DBAL/Configuration.php | 22 ++ lib/Doctrine/DBAL/Connection.php | 12 +- lib/Doctrine/DBAL/Driver/ResultStatement.php | 105 +++++++ lib/Doctrine/DBAL/Driver/Statement.php | 86 +----- .../Tests/DBAL/Functional/ResultCacheTest.php | 157 +++++++++++ 7 files changed, 637 insertions(+), 83 deletions(-) create mode 100644 lib/Doctrine/DBAL/Cache/ArrayStatement.php create mode 100644 lib/Doctrine/DBAL/Cache/RowCacheStatement.php create mode 100644 lib/Doctrine/DBAL/Driver/ResultStatement.php create mode 100644 tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php diff --git a/lib/Doctrine/DBAL/Cache/ArrayStatement.php b/lib/Doctrine/DBAL/Cache/ArrayStatement.php new file mode 100644 index 00000000000..b33cf7cafd2 --- /dev/null +++ b/lib/Doctrine/DBAL/Cache/ArrayStatement.php @@ -0,0 +1,82 @@ +. + */ + +namespace Doctrine\DBAL\Cache; + +use Doctrine\DBAL\Driver\ResultStatement; +use PDO; + +class ArrayStatement implements ResultStatement +{ + private $data; + private $columnCount = 0; + private $num = 0; + + public function __construct(array $data) + { + $this->data = $data; + if (count($data)) { + $this->columnCount = count($data[0]); + } + } + + public function closeCursor() + { + unset ($this->data); + } + + public function columnCount() + { + return $this->columnCount; + } + + public function fetch($fetchStyle = PDO::FETCH_BOTH) + { + if (isset($this->data[$this->num])) { + $row = $this->data[$this->num++]; + if ($fetchStyle === PDO::FETCH_ASSOC) { + return $row; + } else if ($fetchStyle === PDO::FETCH_NUM) { + return array_values($row); + } else if ($fetchStyle === PDO::FETCH_BOTH) { + return array_merge($row, array_values($row)); + } + } + return false; + } + + public function fetchAll($fetchStyle = PDO::FETCH_BOTH) + { + $rows = array(); + while ($row = $this->fetch($fetchStyle)) { + $rows[] = $row; + } + return $rows; + } + + public function fetchColumn($columnIndex = 0) + { + $row = $this->fetch(PDO::FETCH_NUM); + if (!isset($row[$columnIndex])) { + // TODO: verify this is correct behavior + return false; + } + return $row[$columnIndex]; + } +} \ No newline at end of file diff --git a/lib/Doctrine/DBAL/Cache/RowCacheStatement.php b/lib/Doctrine/DBAL/Cache/RowCacheStatement.php new file mode 100644 index 00000000000..be56d477967 --- /dev/null +++ b/lib/Doctrine/DBAL/Cache/RowCacheStatement.php @@ -0,0 +1,256 @@ +. + */ + +namespace Doctrine\DBAL\Cache; + +use Doctrine\DBAL\Driver\ResultStatement; +use PDO; +use Doctrine\DBAL\Connection; + +/** + * Cache statement for SQL results. + * + * A result is saved in multiple cache keys, there is the originally specified + * cache key which is just pointing to result rows by key. The following things + * have to be ensured: + * + * 1. lifetime of the original key has to be longer than that of all the individual rows keys + * 2. if any one row key is missing the query has to be re-executed. + * + * Also you have to realize that the cache will load the whole result into memory at once to ensure 2. + * This means that the memory usage for cached results might increase by using this feature. + */ +class RowCacheStatement implements ResultStatement +{ + /** + * @var \Doctrine\DBAL\Connection + */ + private $conn; + + /** + * @var \Doctrine\Common\Cache\Cache + */ + private $cache; + + /** + * + * @var string + */ + private $cacheKey; + + /** + * @var int + */ + private $lifetime; + + /** + * @var Doctrine\DBAL\Driver\Statement + */ + private $statement; + + /** + * @var array + */ + private $rowPointers = array(); + + /** + * @var int + */ + private $num = 0; + + /** + * Did we reach the end of the statement? + * + * @var bool + */ + private $emptied = false; + + /** + * @param Connection $conn + * @param string $cacheKey + * @param int|null $lifetime + * @param string $query + * @param array $params + * @param array $types + * @return RowCacheStatement + */ + static public function create(Connection $conn, $cacheKey, $lifetime, $query, $params, $types) + { + $resultCache = $conn->getConfiguration()->getResultCacheImpl(); + if (!$resultCache) { + return $conn->executeQuery($query, $params, $types); + } + + if ($rowPointers = $resultCache->fetch($cacheKey)) { + $data = array(); + foreach ($rowPointers AS $rowPointer) { + if ($row = $resultCache->fetch($rowPointer)) { + $data[] = $row; + } else { + return new self($conn->executeQuery($query, $params, $types), $resultCache, $cacheKey, $lifetime); + } + } + return new ArrayStatement($data); + } + return new self($conn->executeQuery($query, $params, $types), $resultCache, $cacheKey, $lifetime); + } + + public function __construct($stmt, $resultCache, $cacheKey, $lifetime = 0) + { + $this->statement = $stmt; + $this->resultCache = $resultCache; + $this->cacheKey = $cacheKey; + $this->lifetime = $lifetime; + } + + /** + * Closes the cursor, enabling the statement to be executed again. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + public function closeCursor() + { + // the "important" key is written as the last one. This way we ensure it has a longer lifetime than the rest + // avoiding potential cache "misses" during the reconstruction. + if ($this->emptied && $this->rowPointers) { + $this->resultCache->save($this->cacheKey, $this->rowPointers, $this->lifetime); + unset($this->rowPointers); + } + } + + /** + * columnCount + * Returns the number of columns in the result set + * + * @return integer Returns the number of columns in the result set represented + * by the PDOStatement object. If there is no result set, + * this method should return 0. + */ + public function columnCount() + { + return $this->statement->columnCount(); + } + + /** + * fetch + * + * @see Query::HYDRATE_* constants + * @param integer $fetchStyle Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @param integer $cursorOrientation For a PDOStatement object representing a scrollable cursor, + * this value determines which row will be returned to the caller. + * This value must be one of the Query::HYDRATE_ORI_* constants, defaulting to + * Query::HYDRATE_ORI_NEXT. To request a scrollable cursor for your + * PDOStatement object, + * you must set the PDO::ATTR_CURSOR attribute to Doctrine::CURSOR_SCROLL when you + * prepare the SQL statement with Doctrine_Adapter_Interface->prepare(). + * + * @param integer $cursorOffset For a PDOStatement object representing a scrollable cursor for which the + * $cursorOrientation parameter is set to Query::HYDRATE_ORI_ABS, this value specifies + * the absolute number of the row in the result set that shall be fetched. + * + * For a PDOStatement object representing a scrollable cursor for + * which the $cursorOrientation parameter is set to Query::HYDRATE_ORI_REL, this value + * specifies the row to fetch relative to the cursor position before + * PDOStatement->fetch() was called. + * + * @return mixed + */ + public function fetch($fetchStyle = PDO::FETCH_BOTH) + { + $row = $this->statement->fetch(PDO::FETCH_ASSOC); + if ($row) { + $rowCacheKey = $this->cacheKey . "#row". ($this->num++); + $this->rowPointers[] = $rowCacheKey; + $this->resultCache->save($rowCacheKey, $row, $this->lifetime); + if ($fetchStyle == PDO::FETCH_ASSOC) { + return $row; + } else if ($fetchStyle == PDO::FETCH_NUM) { + return array_values($row); + } else if ($fetchStyle == PDO::FETCH_BOTH) { + return array_merge($row, array_values($row)); + } else { + throw new \InvalidArgumentException("Invalid fetch-style given for caching result."); + } + } + $this->emptied = true; + return false; + } + + /** + * Returns an array containing all of the result set rows + * + * @param integer $fetchStyle Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @param integer $columnIndex Returns the indicated 0-indexed column when the value of $fetchStyle is + * Query::HYDRATE_COLUMN. Defaults to 0. + * + * @return array + */ + public function fetchAll($fetchStyle = PDO::FETCH_BOTH) + { + $rows = array(); + while ($row = $this->fetch($fetchStyle)) { + $rows[] = $row; + } + return $rows; + } + + /** + * fetchColumn + * Returns a single column from the next row of a + * result set or FALSE if there are no more rows. + * + * @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row. If no + * value is supplied, PDOStatement->fetchColumn() + * fetches the first column. + * + * @return string returns a single column in the next row of a result set. + */ + public function fetchColumn($columnIndex = 0) + { + $row = $this->fetch(PDO::FETCH_NUM); + if (!isset($row[$columnIndex])) { + // TODO: verify this is correct behavior + return false; + } + return $row[$columnIndex]; + } + + /** + * rowCount + * rowCount() returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement + * executed by the corresponding object. + * + * If the last SQL statement executed by the associated Statement object was a SELECT statement, + * some databases may return the number of rows returned by that statement. However, + * this behaviour is not guaranteed for all databases and should not be + * relied on for portable applications. + * + * @return integer Returns the number of rows. + */ + public function rowCount() + { + return $this->statement->rowCount(); + } +} \ No newline at end of file diff --git a/lib/Doctrine/DBAL/Configuration.php b/lib/Doctrine/DBAL/Configuration.php index 8b5b1586e27..befb02ece19 100644 --- a/lib/Doctrine/DBAL/Configuration.php +++ b/lib/Doctrine/DBAL/Configuration.php @@ -20,6 +20,7 @@ namespace Doctrine\DBAL; use Doctrine\DBAL\Logging\SQLLogger; +use Doctrine\Common\Cache\Cache; /** * Configuration container for the Doctrine DBAL. @@ -61,4 +62,25 @@ public function getSQLLogger() return isset($this->_attributes['sqlLogger']) ? $this->_attributes['sqlLogger'] : null; } + + /** + * Gets the cache driver implementation that is used for query result caching. + * + * @return \Doctrine\Common\Cache\Cache + */ + public function getResultCacheImpl() + { + return isset($this->_attributes['resultCacheImpl']) ? + $this->_attributes['resultCacheImpl'] : null; + } + + /** + * Sets the cache driver implementation that is used for query result caching. + * + * @param \Doctrine\Common\Cache\Cache $cacheImpl + */ + public function setResultCacheImpl(Cache $cacheImpl) + { + $this->_attributes['resultCacheImpl'] = $cacheImpl; + } } \ No newline at end of file diff --git a/lib/Doctrine/DBAL/Connection.php b/lib/Doctrine/DBAL/Connection.php index 29ab91c0018..3277e118cc6 100644 --- a/lib/Doctrine/DBAL/Connection.php +++ b/lib/Doctrine/DBAL/Connection.php @@ -23,7 +23,8 @@ Doctrine\DBAL\Types\Type, Doctrine\DBAL\Driver\Connection as DriverConnection, Doctrine\Common\EventManager, - Doctrine\DBAL\DBALException; + Doctrine\DBAL\DBALException, + Doctrine\DBAL\Cache\RowCacheStatement; /** * A wrapper around a Doctrine\DBAL\Driver\Connection that adds features like @@ -593,11 +594,18 @@ public function prepare($statement) * * @param string $query The SQL query to execute. * @param array $params The parameters to bind to the query, if any. + * @param array $types The types the previous parameters are in. + * @param string|null $cacheResultKey name of the result cache key. + * @param int $cacheLifetime lifetime of the cache result. * @return Doctrine\DBAL\Driver\Statement The executed statement. * @internal PERF: Directly prepares a driver statement, not a wrapper. */ - public function executeQuery($query, array $params = array(), $types = array()) + public function executeQuery($query, array $params = array(), $types = array(), $cacheResultKey = null, $cacheLifetime = 0) { + if ($cacheResultKey !== null) { + return RowCacheStatement::create($this, $cacheResultKey, $cacheLifetime, $query, $params, $types); + } + $this->connect(); $hasLogger = $this->_config->getSQLLogger() !== null; diff --git a/lib/Doctrine/DBAL/Driver/ResultStatement.php b/lib/Doctrine/DBAL/Driver/ResultStatement.php new file mode 100644 index 00000000000..d9426ef1d09 --- /dev/null +++ b/lib/Doctrine/DBAL/Driver/ResultStatement.php @@ -0,0 +1,105 @@ +. + */ + +namespace Doctrine\DBAL\Driver; + +use PDO; + +/** + * Interface for the reading part of a prepare statement only. + * + * @author Benjamin Eberlei + */ +interface ResultStatement +{ + /** + * Closes the cursor, enabling the statement to be executed again. + * + * @return boolean Returns TRUE on success or FALSE on failure. + */ + function closeCursor(); + + + /** + * columnCount + * Returns the number of columns in the result set + * + * @return integer Returns the number of columns in the result set represented + * by the PDOStatement object. If there is no result set, + * this method should return 0. + */ + function columnCount(); + + /** + * fetch + * + * @see Query::HYDRATE_* constants + * @param integer $fetchStyle Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @param integer $cursorOrientation For a PDOStatement object representing a scrollable cursor, + * this value determines which row will be returned to the caller. + * This value must be one of the Query::HYDRATE_ORI_* constants, defaulting to + * Query::HYDRATE_ORI_NEXT. To request a scrollable cursor for your + * PDOStatement object, + * you must set the PDO::ATTR_CURSOR attribute to Doctrine::CURSOR_SCROLL when you + * prepare the SQL statement with Doctrine_Adapter_Interface->prepare(). + * + * @param integer $cursorOffset For a PDOStatement object representing a scrollable cursor for which the + * $cursorOrientation parameter is set to Query::HYDRATE_ORI_ABS, this value specifies + * the absolute number of the row in the result set that shall be fetched. + * + * For a PDOStatement object representing a scrollable cursor for + * which the $cursorOrientation parameter is set to Query::HYDRATE_ORI_REL, this value + * specifies the row to fetch relative to the cursor position before + * PDOStatement->fetch() was called. + * + * @return mixed + */ + function fetch($fetchStyle = PDO::FETCH_BOTH); + + /** + * Returns an array containing all of the result set rows + * + * @param integer $fetchStyle Controls how the next row will be returned to the caller. + * This value must be one of the Query::HYDRATE_* constants, + * defaulting to Query::HYDRATE_BOTH + * + * @param integer $columnIndex Returns the indicated 0-indexed column when the value of $fetchStyle is + * Query::HYDRATE_COLUMN. Defaults to 0. + * + * @return array + */ + function fetchAll($fetchStyle = PDO::FETCH_BOTH); + + /** + * fetchColumn + * Returns a single column from the next row of a + * result set or FALSE if there are no more rows. + * + * @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row. If no + * value is supplied, PDOStatement->fetchColumn() + * fetches the first column. + * + * @return string returns a single column in the next row of a result set. + */ + function fetchColumn($columnIndex = 0); +} + diff --git a/lib/Doctrine/DBAL/Driver/Statement.php b/lib/Doctrine/DBAL/Driver/Statement.php index 6cb8b640228..1f2cd4293be 100644 --- a/lib/Doctrine/DBAL/Driver/Statement.php +++ b/lib/Doctrine/DBAL/Driver/Statement.php @@ -1,7 +1,5 @@ prepare(). - * - * @param integer $cursorOffset For a PDOStatement object representing a scrollable cursor for which the - * $cursorOrientation parameter is set to Query::HYDRATE_ORI_ABS, this value specifies - * the absolute number of the row in the result set that shall be fetched. - * - * For a PDOStatement object representing a scrollable cursor for - * which the $cursorOrientation parameter is set to Query::HYDRATE_ORI_REL, this value - * specifies the row to fetch relative to the cursor position before - * PDOStatement->fetch() was called. - * - * @return mixed - */ - function fetch($fetchStyle = PDO::FETCH_BOTH); - - /** - * Returns an array containing all of the result set rows - * - * @param integer $fetchStyle Controls how the next row will be returned to the caller. - * This value must be one of the Query::HYDRATE_* constants, - * defaulting to Query::HYDRATE_BOTH - * - * @param integer $columnIndex Returns the indicated 0-indexed column when the value of $fetchStyle is - * Query::HYDRATE_COLUMN. Defaults to 0. - * - * @return array - */ - function fetchAll($fetchStyle = PDO::FETCH_BOTH); - - /** - * fetchColumn - * Returns a single column from the next row of a - * result set or FALSE if there are no more rows. - * - * @param integer $columnIndex 0-indexed number of the column you wish to retrieve from the row. If no - * value is supplied, PDOStatement->fetchColumn() - * fetches the first column. - * - * @return string returns a single column in the next row of a result set. - */ - function fetchColumn($columnIndex = 0); - /** * rowCount - * rowCount() returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement + * rowCount() returns the number of rows affected by the last DELETE, INSERT, or UPDATE statement * executed by the corresponding object. * - * If the last SQL statement executed by the associated Statement object was a SELECT statement, - * some databases may return the number of rows returned by that statement. However, - * this behaviour is not guaranteed for all databases and should not be + * If the last SQL statement executed by the associated Statement object was a SELECT statement, + * some databases may return the number of rows returned by that statement. However, + * this behaviour is not guaranteed for all databases and should not be * relied on for portable applications. * * @return integer Returns the number of rows. diff --git a/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php b/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php new file mode 100644 index 00000000000..48d2c7628a7 --- /dev/null +++ b/tests/Doctrine/Tests/DBAL/Functional/ResultCacheTest.php @@ -0,0 +1,157 @@ + 100, 'test_string' => 'foo'), array('test_int' => 200, 'test_string' => 'bar'), array('test_int' => 300, 'test_string' => 'baz')); + private $sqlLogger; + + public function setUp() + { + parent::setUp(); + + try { + /* @var $sm \Doctrine\DBAL\Schema\AbstractSchemaManager */ + $table = new \Doctrine\DBAL\Schema\Table("caching"); + $table->addColumn('test_int', 'integer'); + $table->addColumn('test_string', 'string', array('notnull' => false)); + + $sm = $this->_conn->getSchemaManager(); + $sm->createTable($table); + } catch(\Exception $e) { + + } + $this->_conn->executeUpdate('DELETE FROM caching'); + foreach ($this->expectedResult AS $row) { + $this->_conn->insert('caching', $row); + } + + $config = $this->_conn->getConfiguration(); + $config->setSQLLogger($this->sqlLogger = new \Doctrine\DBAL\Logging\DebugStack); + $config->setResultCacheImpl(new \Doctrine\Common\Cache\ArrayCache); + } + + public function testCacheFetchAssoc() + { + $this->assertCacheNonCacheSelectSameFetchModeAreEqual($this->expectedResult, \PDO::FETCH_ASSOC); + } + + public function testFetchNum() + { + $expectedResult = array(); + foreach ($this->expectedResult AS $v) { + $expectedResult[] = array_values($v); + } + $this->assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, \PDO::FETCH_NUM); + } + + public function testFetchBoth() + { + $expectedResult = array(); + foreach ($this->expectedResult AS $v) { + $expectedResult[] = array_merge($v, array_values($v)); + } + $this->assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, \PDO::FETCH_BOTH); + } + + public function testMixingFetch() + { + $numExpectedResult = array(); + foreach ($this->expectedResult AS $v) { + $numExpectedResult[] = array_values($v); + } + $stmt = $this->_conn->executeQuery("SELECT * FROM caching", array(), array(), "testcachekey", 10); + + $data = array(); + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $data[] = $row; + } + $stmt->closeCursor(); + + $this->assertEquals($this->expectedResult, $data); + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching", array(), array(), "testcachekey", 10); + + $data = array(); + while ($row = $stmt->fetch(\PDO::FETCH_NUM)) { + $data[] = $row; + } + $stmt->closeCursor(); + + $this->assertEquals($numExpectedResult, $data); + } + + public function testDontCloseNoCache() + { + $stmt = $this->_conn->executeQuery("SELECT * FROM caching", array(), array(), "testcachekey", 10); + + $data = array(); + while ($row = $stmt->fetch(\PDO::FETCH_ASSOC)) { + $data[] = $row; + } + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching", array(), array(), "testcachekey", 10); + + $data = array(); + while ($row = $stmt->fetch(\PDO::FETCH_NUM)) { + $data[] = $row; + } + + $this->assertEquals(2, count($this->sqlLogger->queries)); + } + + public function testDontFinishNoCache() + { + $stmt = $this->_conn->executeQuery("SELECT * FROM caching", array(), array(), "testcachekey", 10); + + $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $stmt->closeCursor(); + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching", array(), array(), "testcachekey", 10); + + $data = array(); + while ($row = $stmt->fetch(\PDO::FETCH_NUM)) { + $data[] = $row; + } + $stmt->closeCursor(); + + $this->assertEquals(2, count($this->sqlLogger->queries)); + } + + public function assertCacheNonCacheSelectSameFetchModeAreEqual($expectedResult, $fetchStyle) + { + $stmt = $this->_conn->executeQuery("SELECT * FROM caching", array(), array(), "testcachekey", 10); + + $this->assertEquals(2, $stmt->columnCount()); + + $data = array(); + while ($row = $stmt->fetch($fetchStyle)) { + $data[] = $row; + } + $stmt->closeCursor(); + + $this->assertEquals($expectedResult, $data); + + $stmt = $this->_conn->executeQuery("SELECT * FROM caching", array(), array(), "testcachekey", 10); + + $this->assertEquals(2, $stmt->columnCount()); + + $data = array(); + while ($row = $stmt->fetch($fetchStyle)) { + $data[] = $row; + } + $stmt->closeCursor(); + + $this->assertEquals($expectedResult, $data); + + $this->assertEquals(1, count($this->sqlLogger->queries), "just one dbal hit"); + } +} \ No newline at end of file From ded75ccb278d1b541fbf7d856273b29cd20cb421 Mon Sep 17 00:00:00 2001 From: Benjamin Eberlei Date: Sat, 22 Oct 2011 18:46:03 +0200 Subject: [PATCH 2/2] DDC-217 - Fix some missing docblock, unused variable --- lib/Doctrine/DBAL/Cache/RowCacheStatement.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/Doctrine/DBAL/Cache/RowCacheStatement.php b/lib/Doctrine/DBAL/Cache/RowCacheStatement.php index be56d477967..a7727cd6b4a 100644 --- a/lib/Doctrine/DBAL/Cache/RowCacheStatement.php +++ b/lib/Doctrine/DBAL/Cache/RowCacheStatement.php @@ -38,11 +38,6 @@ */ class RowCacheStatement implements ResultStatement { - /** - * @var \Doctrine\DBAL\Connection - */ - private $conn; - /** * @var \Doctrine\Common\Cache\Cache */ @@ -111,6 +106,13 @@ static public function create(Connection $conn, $cacheKey, $lifetime, $query, $p return new self($conn->executeQuery($query, $params, $types), $resultCache, $cacheKey, $lifetime); } + /** + * + * @param Statement $stmt + * @param Cache $resultCache + * @param string $cacheKey + * @param int $lifetime + */ public function __construct($stmt, $resultCache, $cacheKey, $lifetime = 0) { $this->statement = $stmt;