Browse Source

Merge pull request #5623 from cakephp/3.0-real-unbuffered

3.0 real unbuffered
José Lorenzo Rodríguez 11 years ago
parent
commit
e6bb8bbda5

+ 6 - 7
src/Database/Connection.php

@@ -269,8 +269,7 @@ class Connection
      */
     public function run(Query $query)
     {
-        $sql = $query->sql();
-        $statement = $this->prepare($sql);
+        $statement = $this->prepare($query);
         $query->valueBinder()->attachTo($statement);
         $statement->execute();
 
@@ -487,7 +486,7 @@ class Connection
      */
     public function createSavePoint($name)
     {
-        $this->execute($this->_driver->savePointSQL($name));
+        $this->execute($this->_driver->savePointSQL($name))->closeCursor();
     }
 
     /**
@@ -498,7 +497,7 @@ class Connection
      */
     public function releaseSavePoint($name)
     {
-        $this->execute($this->_driver->releaseSavePointSQL($name));
+        $this->execute($this->_driver->releaseSavePointSQL($name))->closeCursor();
     }
 
     /**
@@ -509,7 +508,7 @@ class Connection
      */
     public function rollbackSavepoint($name)
     {
-        $this->execute($this->_driver->rollbackSavePointSQL($name));
+        $this->execute($this->_driver->rollbackSavePointSQL($name))->closeCursor();
     }
 
     /**
@@ -519,7 +518,7 @@ class Connection
      */
     public function disableForeignKeys()
     {
-        $this->execute($this->_driver->disableForeignKeySql());
+        $this->execute($this->_driver->disableForeignKeySql())->closeCursor();
     }
 
     /**
@@ -529,7 +528,7 @@ class Connection
      */
     public function enableForeignKeys()
     {
-        $this->execute($this->_driver->enableForeignKeySql());
+        $this->execute($this->_driver->enableForeignKeySql())->closeCursor();
     }
 
     /**

+ 19 - 0
src/Database/Driver/Mysql.php

@@ -15,6 +15,8 @@
 namespace Cake\Database\Driver;
 
 use Cake\Database\Dialect\MysqlDialectTrait;
+use Cake\Database\Query;
+use Cake\Database\Statement\MysqlStatement;
 use PDO;
 
 class Mysql extends \Cake\Database\Driver
@@ -100,4 +102,21 @@ class Mysql extends \Cake\Database\Driver
     {
         return in_array('mysql', PDO::getAvailableDrivers());
     }
+
+    /**
+     * Prepares a sql statement to be executed
+     *
+     * @param string|\Cake\Database\Query $query The query to prepare.
+     * @return \Cake\Database\StatementInterface
+     */
+    public function prepare($query)
+    {
+        $this->connect();
+        $statement = $this->_connection->prepare((string)$query);
+        $result = new MysqlStatement($statement, $this);
+        if ($query instanceof Query && $query->bufferResults() === false) {
+            $result->bufferResults(false);
+        }
+        return $result;
+    }
 }

+ 6 - 1
src/Database/Driver/Sqlite.php

@@ -15,6 +15,7 @@
 namespace Cake\Database\Driver;
 
 use Cake\Database\Dialect\SqliteDialectTrait;
+use Cake\Database\Query;
 use Cake\Database\Statement\PDOStatement;
 use Cake\Database\Statement\SqliteStatement;
 use PDO;
@@ -87,6 +88,10 @@ class Sqlite extends \Cake\Database\Driver
     {
         $this->connect();
         $statement = $this->_connection->prepare((string)$query);
-        return new SqliteStatement(new PDOStatement($statement, $this), $this);
+        $result = new SqliteStatement(new PDOStatement($statement, $this), $this);
+        if ($query instanceof Query && $query->bufferResults() === false) {
+            $result->bufferResults(false);
+        }
+        return $result;
     }
 }

+ 5 - 1
src/Database/Driver/Sqlserver.php

@@ -100,7 +100,11 @@ class Sqlserver extends \Cake\Database\Driver
     public function prepare($query)
     {
         $this->connect();
-        $statement = $this->_connection->prepare((string)$query, [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
+        $options = [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL];
+        if ($query instanceof Query && $query->bufferResults() === false) {
+            $options = [];
+        }
+        $statement = $this->_connection->prepare((string)$query, $options);
         return new SqlserverStatement($statement, $this);
     }
 }

+ 35 - 0
src/Database/Query.php

@@ -114,6 +114,14 @@ class Query implements ExpressionInterface, IteratorAggregate
     protected $_functionsBuilder;
 
     /**
+     * Boolean for tracking whether or not buffered results
+     * are enabled.
+     *
+     * @var bool
+     */
+    protected $_useBufferedResults = true;
+
+    /**
      * Constructor.
      *
      * @param \Cake\Database\Connection $connection The connection
@@ -1582,6 +1590,33 @@ class Query implements ExpressionInterface, IteratorAggregate
     }
 
     /**
+     * Enable/Disable buffered results.
+     *
+     * When enabled the results returned by this Query will be
+     * buffered. This enables you to iterate a result set multiple times, or
+     * both cache and iterate it.
+     *
+     * When disabled it will consume less memory as fetched results are not
+     * remembered for future iterations.
+     *
+     * If called with no arguments, it will return whether or not buffering is
+     * enabled.
+     *
+     * @param bool|null $enable whether or not to enable buffering
+     * @return bool|$this
+     */
+    public function bufferResults($enable = null)
+    {
+        if ($enable === null) {
+            return $this->_useBufferedResults;
+        }
+
+        $this->_dirty();
+        $this->_useBufferedResults = (bool)$enable;
+        return $this;
+    }
+
+    /**
      * Auxiliary function used to wrap the original statement from the driver with
      * any registered callbacks.
      *

+ 43 - 0
src/Database/Statement/BufferResultsTrait.php

@@ -0,0 +1,43 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Database\Statement;
+
+/**
+ * Contains a setter for marking a Statement as buffered
+ *
+ * @internal
+ */
+trait BufferResultsTrait
+{
+
+    /**
+     * Whether or not to buffer results in php
+     *
+     * @var bool
+     */
+    protected $_bufferResults = true;
+
+    /**
+     * Whether or not to buffer results in php
+     *
+     * @param bool $buffer Toggle buffering
+     * @return $this
+     */
+    public function bufferResults($buffer)
+    {
+        $this->_bufferResults = (bool)$buffer;
+        return $this;
+    }
+}

+ 40 - 0
src/Database/Statement/MysqlStatement.php

@@ -0,0 +1,40 @@
+<?php
+/**
+ * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
+ * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ *
+ * Licensed under The MIT License
+ * For full copyright and license information, please see the LICENSE.txt
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
+ * @link          http://cakephp.org CakePHP(tm) Project
+ * @since         3.0.0
+ * @license       http://www.opensource.org/licenses/mit-license.php MIT License
+ */
+namespace Cake\Database\Statement;
+
+use PDO;
+
+/**
+ * Statement class meant to be used by a Mysql PDO driver
+ *
+ * @internal
+ */
+class MysqlStatement extends PDOStatement
+{
+
+    use BufferResultsTrait;
+
+    /**
+     * {@inheritDoc}
+     *
+     */
+    public function execute($params = null)
+    {
+        $this->_driver->connection()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, $this->_bufferResults);
+        $result = $this->_statement->execute($params);
+        $this->_driver->connection()->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, true);
+        return $result;
+    }
+}

+ 20 - 1
src/Database/Statement/SqliteStatement.php

@@ -19,9 +19,28 @@ namespace Cake\Database\Statement;
  *
  * @internal
  */
-class SqliteStatement extends BufferedStatement
+class SqliteStatement extends StatementDecorator
 {
 
+    use BufferResultsTrait;
+
+    /**
+     * {@inheritDoc}
+     *
+     */
+    public function execute($params = null)
+    {
+        if ($this->_statement instanceof BufferedStatement) {
+            $this->_statement = $this->_statement->getInnerStatement();
+        }
+
+        if ($this->_bufferResults) {
+            $this->_statement = new BufferedStatement($this->_statement, $this->_driver);
+        }
+
+        return $this->_statement->execute($params);
+    }
+
     /**
      * Returns the number of rows returned of affected by last execution
      *

+ 11 - 0
src/Database/Statement/StatementDecorator.php

@@ -238,6 +238,7 @@ class StatementDecorator implements StatementInterface, \Countable, \IteratorAgg
      */
     public function getIterator()
     {
+        $this->execute();
         return $this->_statement;
     }
 
@@ -297,4 +298,14 @@ class StatementDecorator implements StatementInterface, \Countable, \IteratorAgg
         }
         return $this->_driver->lastInsertId($table, $column);
     }
+
+    /**
+     * Returns the statement object that was decorated by this class.
+     *
+     * @return \Cake\Database\StatementInterface
+     */
+    public function getInnerStatement()
+    {
+        return $this->_statement;
+    }
 }

+ 1 - 2
src/ORM/Behavior/TreeBehavior.php

@@ -665,8 +665,7 @@ class TreeBehavior extends Behavior
             ->select($pk)
             ->where([$parent . ' IS' => $parentId])
             ->order($pk)
-            ->hydrate(false)
-            ->bufferResults(false);
+            ->hydrate(false);
 
         $leftCounter = $counter;
         foreach ($query as $row) {

+ 0 - 35
src/ORM/Query.php

@@ -77,14 +77,6 @@ class Query extends DatabaseQuery implements JsonSerializable
     protected $_autoFields;
 
     /**
-     * Boolean for tracking whether or not buffered results
-     * are enabled.
-     *
-     * @var bool
-     */
-    protected $_useBufferedResults = true;
-
-    /**
      * Whether to hydrate results into entity objects
      *
      * @var bool
@@ -344,33 +336,6 @@ class Query extends DatabaseQuery implements JsonSerializable
     }
 
     /**
-     * Enable/Disable buffered results.
-     *
-     * When enabled the ResultSet returned by this Query will be
-     * buffered. This enables you to iterate a ResultSet multiple times, or
-     * both cache and iterate the ResultSet.
-     *
-     * When disabled it will consume less memory as fetched results are not
-     * remembered in the ResultSet.
-     *
-     * If called with no arguments, it will return whether or not buffering is
-     * enabled.
-     *
-     * @param bool $enable whether or not to enable buffering
-     * @return bool|$this
-     */
-    public function bufferResults($enable = null)
-    {
-        if ($enable === null) {
-            return $this->_useBufferedResults;
-        }
-
-        $this->_dirty();
-        $this->_useBufferedResults = (bool)$enable;
-        return $this;
-    }
-
-    /**
      * Returns a key => value array representing a single aliased field
      * that can be passed directly to the select() method.
      * The key will contain the alias and the value the actual field name.

+ 13 - 9
src/ORM/ResultSet.php

@@ -143,10 +143,10 @@ class ResultSet implements ResultSetInterface
         $this->_hydrate = $this->_query->hydrate();
         $this->_entityClass = $repository->entityClass();
         $this->_useBuffering = $query->bufferResults();
-        $this->count();
 
         if ($this->_useBuffering) {
-            $this->_results = new SplFixedArray($this->_count);
+            $count = $this->count();
+            $this->_results = new SplFixedArray($count);
         }
     }
 
@@ -217,22 +217,26 @@ class ResultSet implements ResultSetInterface
      */
     public function valid()
     {
-        if ($this->_index >= $this->_count) {
+        $valid = true;
+        if ($this->_useBuffering) {
+            $valid = $this->_index < $this->_count;
+            if ($valid && $this->_results[$this->_index] !== null) {
+                $this->_current = $this->_results[$this->_index];
+                return true;
+            }
+        }
+
+        if (!$valid) {
             if ($this->_statement !== null) {
                 $this->_statement->closeCursor();
             }
             return false;
         }
 
-        if ($this->_useBuffering && $this->_results[$this->_index] !== null) {
-            $this->_current = $this->_results[$this->_index];
-            return true;
-        }
-
         $this->_current = $this->_fetchResult();
         $valid = $this->_current !== false;
 
-        if ($this->_useBuffering) {
+        if ($valid && $this->_useBuffering) {
             $this->_results[$this->_index] = $this->_current;
         }
 

+ 10 - 0
tests/TestCase/Database/ConnectionTest.php

@@ -274,6 +274,11 @@ class ConnectionTest extends TestCase
         $this->assertEquals(2, $result['total']);
         $total->closeCursor();
 
+        $total->execute();
+        $result = $total->fetch('assoc');
+        $this->assertEquals(2, $result['total']);
+        $total->closeCursor();
+
         $result = $this->connection->execute('SELECT title, body  FROM things');
         $row = $result->fetch('assoc');
         $this->assertEquals('a title', $row['title']);
@@ -283,6 +288,11 @@ class ConnectionTest extends TestCase
         $result->closeCursor();
         $this->assertEquals('another title', $row['title']);
         $this->assertEquals('another body', $row['body']);
+
+        $result->execute();
+        $row = $result->fetch('assoc');
+        $result->closeCursor();
+        $this->assertEquals('a title', $row['title']);
     }
 
     /**

+ 34 - 0
tests/TestCase/Database/QueryTest.php

@@ -3132,6 +3132,40 @@ class QueryTest extends TestCase
     }
 
     /**
+     * Shows that bufferResults(false) will prevent client-side results buffering
+     *
+     * @return void
+     */
+    public function testUnbufferedQuery()
+    {
+        $query = new Query($this->connection);
+        $result = $query->select(['body', 'author_id'])
+            ->from('articles')
+            ->bufferResults(false)
+            ->execute();
+
+        $this->skipIf(
+            !method_exists($result, 'bufferResults'),
+            'This driver does not support unbuffered queries'
+        );
+
+        $this->assertCount(0, $result);
+        $list = $result->fetchAll('assoc');
+        $this->assertCount(3, $list);
+        $result->closeCursor();
+
+        $query = new Query($this->connection);
+        $result = $query->select(['body', 'author_id'])
+            ->from('articles')
+            ->execute();
+
+        $this->assertCount(3, $result);
+        $list = $result->fetchAll('assoc');
+        $this->assertCount(3, $list);
+        $result->closeCursor();
+    }
+
+    /**
      * Assertion for comparing a table's contents with what is in it.
      *
      * @param string $table