From e62e84873c22e8800d7e95c90e843804d9a356bf Mon Sep 17 00:00:00 2001 From: Carsten Brandt Date: Sun, 22 Sep 2013 16:29:56 +0200 Subject: [PATCH] more API methods for redis active query: sum, avg, max, min ... --- framework/yii/redis/ActiveQuery.php | 138 ++++++++++++++++++++++-- framework/yii/redis/ActiveRecord.php | 2 - framework/yii/redis/ActiveRelation.php | 1 - framework/yii/redis/Connection.php | 4 + framework/yii/redis/LuaScriptBuilder.php | 75 ++++++------- tests/unit/framework/redis/ActiveRecordTest.php | 10 +- 6 files changed, 173 insertions(+), 57 deletions(-) diff --git a/framework/yii/redis/ActiveQuery.php b/framework/yii/redis/ActiveQuery.php index bd597a1..2a693cf 100644 --- a/framework/yii/redis/ActiveQuery.php +++ b/framework/yii/redis/ActiveQuery.php @@ -56,11 +56,6 @@ class ActiveQuery extends \yii\base\Component */ public $with; /** - * @var string the name of the column by which query results should be indexed by. - * This is only used when the query result is returned as an array when calling [[all()]]. - */ - public $indexBy; - /** * @var boolean whether to return each record as an array. If false (default), an object * of [[modelClass]] will be created to represent each record. */ @@ -80,6 +75,18 @@ class ActiveQuery extends \yii\base\Component * If less than zero it means starting n elements from the end. */ public $offset; + /** + * @var array how to sort the query results. This is used to construct the ORDER BY clause in a SQL statement. + * The array keys are the columns to be sorted by, and the array values are the corresponding sort directions which + * can be either [[Query::SORT_ASC]] or [[Query::SORT_DESC]]. The array may also contain [[Expression]] objects. + * If that is the case, the expressions will be converted into strings without any change. + */ + public $orderBy; + /** + * @var string the name of the column by which query results should be indexed by. + * This is only used when the query result is returned as an array when calling [[all()]]. + */ + public $indexBy; /** * Executes query and returns all results as an array. @@ -154,6 +161,21 @@ class ActiveQuery extends \yii\base\Component } /** + * Executes the query and returns the first column of the result. + * @param string $column name of the column to select + * @return array the first column of the query result. An empty array is returned if the query results in nothing. + */ + public function column($column) + { + // TODO add support for indexBy and orderBy + $modelClass = $this->modelClass; + /** @var Connection $db */ + $db = $modelClass::getDb(); + $script = $db->luaScriptBuilder->buildColumn($this, $column); + return $db->executeCommand('EVAL', array($script, 0)); + } + + /** * Returns the number of records. * @param string $q the COUNT expression. Defaults to '*'. * Make sure you properly quote column names. @@ -187,8 +209,54 @@ class ActiveQuery extends \yii\base\Component } /** + * Returns the average of the specified column values. + * @param string $column the column name or expression. + * Make sure you properly quote column names in the expression. + * @return integer the average of the specified column values. + */ + public function average($column) + { + $modelClass = $this->modelClass; + /** @var Connection $db */ + $db = $modelClass::getDb(); + $script = $db->luaScriptBuilder->buildAverage($this, $column); + return $db->executeCommand('EVAL', array($script, 0)); + } + + /** + * Returns the minimum of the specified column values. + * @param string $column the column name or expression. + * Make sure you properly quote column names in the expression. + * @return integer the minimum of the specified column values. + */ + public function min($column) + { + $modelClass = $this->modelClass; + /** @var Connection $db */ + $db = $modelClass::getDb(); + $script = $db->luaScriptBuilder->buildMin($this, $column); + return $db->executeCommand('EVAL', array($script, 0)); + } + + /** + * Returns the maximum of the specified column values. + * @param string $column the column name or expression. + * Make sure you properly quote column names in the expression. + * @return integer the maximum of the specified column values. + */ + public function max($column) + { + $modelClass = $this->modelClass; + /** @var Connection $db */ + $db = $modelClass::getDb(); + $script = $db->luaScriptBuilder->buildMax($this, $column); + return $db->executeCommand('EVAL', array($script, 0)); + } + + /** * Returns the query result as a scalar value. * The value returned will be the first column in the first row of the query results. + * @param string $column name of the column to select * @return string|boolean the value of the first column in the first row of the query result. * False is returned if the query result is empty. */ @@ -210,7 +278,6 @@ class ActiveQuery extends \yii\base\Component /** * Sets the [[asArray]] property. - * TODO: refactor, it is duplicated from yii/db/ActiveQuery * @param boolean $value whether to return the query results in terms of arrays instead of Active Records. * @return ActiveQuery the query object itself */ @@ -221,8 +288,62 @@ class ActiveQuery extends \yii\base\Component } /** + * Sets the ORDER BY part of the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return Query the query object itself + * @see addOrderBy() + */ + public function orderBy($columns) + { + $this->orderBy = $this->normalizeOrderBy($columns); + return $this; + } + + /** + * Adds additional ORDER BY columns to the query. + * @param string|array $columns the columns (and the directions) to be ordered by. + * Columns can be specified in either a string (e.g. "id ASC, name DESC") or an array + * (e.g. `array('id' => Query::SORT_ASC, 'name' => Query::SORT_DESC)`). + * The method will automatically quote the column names unless a column contains some parenthesis + * (which means the column contains a DB expression). + * @return Query the query object itself + * @see orderBy() + */ + public function addOrderBy($columns) + { + $columns = $this->normalizeOrderBy($columns); + if ($this->orderBy === null) { + $this->orderBy = $columns; + } else { + $this->orderBy = array_merge($this->orderBy, $columns); + } + return $this; + } + + protected function normalizeOrderBy($columns) + { + if (is_array($columns)) { + return $columns; + } else { + $columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY); + $result = array(); + foreach ($columns as $column) { + if (preg_match('/^(.*?)\s+(asc|desc)$/i', $column, $matches)) { + $result[$matches[1]] = strcasecmp($matches[2], 'desc') ? self::SORT_ASC : self::SORT_DESC; + } else { + $result[$column] = self::SORT_ASC; + } + } + return $result; + } + } + + /** * Sets the LIMIT part of the query. - * TODO: refactor, it is duplicated from yii/db/Query * @param integer $limit the limit * @return ActiveQuery the query object itself */ @@ -234,7 +355,6 @@ class ActiveQuery extends \yii\base\Component /** * Sets the OFFSET part of the query. - * TODO: refactor, it is duplicated from yii/db/Query * @param integer $offset the offset * @return ActiveQuery the query object itself */ @@ -264,7 +384,6 @@ class ActiveQuery extends \yii\base\Component * ))->all(); * ~~~ * - * TODO: refactor, it is duplicated from yii/db/ActiveQuery * @return ActiveQuery the query object itself */ public function with() @@ -279,7 +398,6 @@ class ActiveQuery extends \yii\base\Component /** * Sets the [[indexBy]] property. - * TODO: refactor, it is duplicated from yii/db/ActiveQuery * @param string $column the name of the column by which the query results should be indexed by. * @return ActiveQuery the query object itself */ diff --git a/framework/yii/redis/ActiveRecord.php b/framework/yii/redis/ActiveRecord.php index 0a1f7bd..6fdbe58 100644 --- a/framework/yii/redis/ActiveRecord.php +++ b/framework/yii/redis/ActiveRecord.php @@ -20,8 +20,6 @@ use yii\db\TableSchema; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. * - * - * * @author Carsten Brandt * @since 2.0 */ diff --git a/framework/yii/redis/ActiveRelation.php b/framework/yii/redis/ActiveRelation.php index a36f19d..b78c200 100644 --- a/framework/yii/redis/ActiveRelation.php +++ b/framework/yii/redis/ActiveRelation.php @@ -13,7 +13,6 @@ namespace yii\redis; /** * ActiveRecord is the base class for classes representing relational data in terms of objects. * - * * @author Carsten Brandt * @since 2.0 */ diff --git a/framework/yii/redis/Connection.php b/framework/yii/redis/Connection.php index cc6c253..0b45659 100644 --- a/framework/yii/redis/Connection.php +++ b/framework/yii/redis/Connection.php @@ -22,6 +22,7 @@ use yii\helpers\Inflector; * * @property string $driverName Name of the DB driver. This property is read-only. * @property boolean $isActive Whether the DB connection is established. This property is read-only. + * @property LuaScriptBuilder $luaScriptBuilder This property is read-only. * @property Transaction $transaction The currently active transaction. Null if no active transaction. This * property is read-only. * @@ -333,6 +334,9 @@ class Connection extends Component } } + /** + * @return LuaScriptBuilder + */ public function getLuaScriptBuilder() { return new LuaScriptBuilder(); diff --git a/framework/yii/redis/LuaScriptBuilder.php b/framework/yii/redis/LuaScriptBuilder.php index f9b6cf6..f6ebeb5 100644 --- a/framework/yii/redis/LuaScriptBuilder.php +++ b/framework/yii/redis/LuaScriptBuilder.php @@ -19,18 +19,28 @@ class LuaScriptBuilder extends \yii\base\Object { public function buildAll($query) { + // TODO add support for orderBy $modelClass = $query->modelClass; $key = $modelClass::tableName(); - return $this->build($query, "n=n+1 pks[n] = redis.call('HGETALL','$key:a:' .. pk)", 'pks'); // TODO quote + return $this->build($query, "n=n+1 pks[n]=redis.call('HGETALL','$key:a:' .. pk)", 'pks'); // TODO quote } public function buildOne($query) { + // TODO add support for orderBy $modelClass = $query->modelClass; $key = $modelClass::tableName(); return $this->build($query, "do return redis.call('HGETALL','$key:a:' .. pk) end", 'pks'); // TODO quote } + public function buildColumn($query, $field) + { + // TODO add support for orderBy and indexBy + $modelClass = $query->modelClass; + $key = $modelClass::tableName(); + return $this->build($query, "n=n+1 pks[n]=redis.call('HGET','$key:a:' .. pk,'$field')", 'pks'); // TODO quote + } + public function buildCount($query) { return $this->build($query, 'n=n+1', 'n'); @@ -43,6 +53,27 @@ class LuaScriptBuilder extends \yii\base\Object return $this->build($query, "n=n+redis.call('HGET','$key:a:' .. pk,'$field')", 'n'); // TODO quote } + public function buildAverage($query, $field) + { + $modelClass = $query->modelClass; + $key = $modelClass::tableName(); + return $this->build($query, "n=n+1 if v==nil then v=0 end v=v+redis.call('HGET','$key:a:' .. pk,'$field')", 'v/n'); // TODO quote + } + + public function buildMin($query, $field) + { + $modelClass = $query->modelClass; + $key = $modelClass::tableName(); + return $this->build($query, "n=redis.call('HGET','$key:a:' .. pk,'$field') if v==nil or nmodelClass; + $key = $modelClass::tableName(); + return $this->build($query, "n=redis.call('HGET','$key:a:' .. pk,'$field') if v==nil or n>v then v=n end", 'v'); // TODO quote + } + /** * @param ActiveQuery $query */ @@ -69,6 +100,7 @@ class LuaScriptBuilder extends \yii\base\Object local allpks=redis.call('LRANGE','$key',0,-1) local pks={} local n=0 +local v=nil local i=0 for k,pk in ipairs(allpks) do $loadColumnValues @@ -100,47 +132,6 @@ EOF; } /** - * @param array $columns - * @return string the GROUP BY clause - */ - public function buildGroupBy($columns) - { - return empty($columns) ? '' : 'GROUP BY ' . $this->buildColumns($columns); - } - - /** - * @param string|array $condition - * @param array $params the binding parameters to be populated - * @return string the HAVING clause built from [[query]]. - */ - public function buildHaving($condition, &$params) - { - $having = $this->buildCondition($condition, $params); - return $having === '' ? '' : 'HAVING ' . $having; - } - - /** - * @param array $columns - * @return string the ORDER BY clause built from [[query]]. - */ - public function buildOrderBy($columns) - { - if (empty($columns)) { - return ''; - } - $orders = array(); - foreach ($columns as $name => $direction) { - if (is_object($direction)) { - $orders[] = (string)$direction; - } else { - $orders[] = $this->db->quoteColumnName($name) . ($direction === Query::SORT_DESC ? ' DESC' : ''); - } - } - - return 'ORDER BY ' . implode(', ', $orders); - } - - /** * Parses the condition specification and generates the corresponding SQL expression. * @param string|array $condition the condition specification. Please refer to [[Query::where()]] * on how to specify a condition. diff --git a/tests/unit/framework/redis/ActiveRecordTest.php b/tests/unit/framework/redis/ActiveRecordTest.php index a3a5559..e5a5762 100644 --- a/tests/unit/framework/redis/ActiveRecordTest.php +++ b/tests/unit/framework/redis/ActiveRecordTest.php @@ -2,6 +2,7 @@ namespace yiiunit\framework\redis; +use yii\db\Query; use yii\redis\ActiveQuery; use yiiunit\data\ar\redis\ActiveRecord; use yiiunit\data\ar\redis\Customer; @@ -134,11 +135,10 @@ class ActiveRecordTest extends RedisTestCase $this->assertEquals(2, $customer->id); // find count, sum, average, min, max, scalar -/* $this->assertEquals(6, Customer::find()->sum('id')); + $this->assertEquals(6, Customer::find()->sum('id')); $this->assertEquals(2, Customer::find()->average('id')); $this->assertEquals(1, Customer::find()->min('id')); $this->assertEquals(3, Customer::find()->max('id')); - $this->assertEquals(3, Customer::find()->select('COUNT(*)')->scalar());*/ // scope // $this->assertEquals(2, Customer::find()->active()->count()); @@ -227,6 +227,12 @@ class ActiveRecordTest extends RedisTestCase $this->assertEquals(7, OrderItem::find()->sum('quantity')); } + public function testFindColumn() + { + $this->assertEquals(array('user1', 'user2', 'user3'), Customer::find()->column('name')); +// TODO $this->assertEquals(array('user3', 'user2', 'user1'), Customer::find()->orderBy(array('name' => Query::SORT_DESC))->column('name')); + } + public function testExists() { $this->assertTrue(Customer::find()->where(array('id' => 2))->exists());