From a3f5236ea65d72b4d63370f4afb440bd81b8ecf9 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Tue, 3 Dec 2013 14:03:23 +0200 Subject: [PATCH] Mongo Collection "group" and "mapReduce" functions fixed. --- extensions/mongo/Collection.php | 105 +++++++++++++++++++------ tests/unit/extensions/mongo/CollectionTest.php | 58 +++++++++++++- 2 files changed, 136 insertions(+), 27 deletions(-) diff --git a/extensions/mongo/Collection.php b/extensions/mongo/Collection.php index 172e4e6..ba631e9 100644 --- a/extensions/mongo/Collection.php +++ b/extensions/mongo/Collection.php @@ -365,7 +365,7 @@ class Collection extends Object } /** - * Performs aggregation using Mongo Map Reduce mechanism. + * Performs aggregation using Mongo "group" command. * @param mixed $keys fields to group by. If an array or non-code object is passed, * it will be the key used to group results. If instance of [[\MongoCode]] passed, * it will be treated as a function that returns the key to group by. @@ -377,37 +377,92 @@ class Collection extends Object * - condition - criteria for including a document in the aggregation. * - finalize - function called once per unique key that takes the final output of the reduce function. * @return array the result of the aggregation. - * @see http://docs.mongodb.org/manual/core/map-reduce/ + * @see http://docs.mongodb.org/manual/reference/command/group/ + * @throws Exception on failure. */ - public function mapReduce($keys, $initial, $reduce, $options = []) + public function group($keys, $initial, $reduce, $options = []) { - $token = 'Map reduce from ' . $this->getFullName(); + $token = 'Grouping from ' . $this->getFullName(); Yii::info($token, __METHOD__); - Yii::beginProfile($token, __METHOD__); + try { + Yii::beginProfile($token, __METHOD__); - if (!($reduce instanceof \MongoCode)) { - $reduce = new \MongoCode((string)$reduce); - } - if (array_key_exists('condition', $options)) { - $options['condition'] = $this->buildCondition($options['condition']); - } - if (array_key_exists('finalize', $options)) { - if (!($options['finalize'] instanceof \MongoCode)) { - $options['finalize'] = new \MongoCode((string)$options['finalize']); + if (!($reduce instanceof \MongoCode)) { + $reduce = new \MongoCode((string)$reduce); } + if (array_key_exists('condition', $options)) { + $options['condition'] = $this->buildCondition($options['condition']); + } + if (array_key_exists('finalize', $options)) { + if (!($options['finalize'] instanceof \MongoCode)) { + $options['finalize'] = new \MongoCode((string)$options['finalize']); + } + } + // Avoid possible E_DEPRECATED for $options: + if (empty($options)) { + $result = $this->mongoCollection->group($keys, $initial, $reduce); + } else { + $result = $this->mongoCollection->group($keys, $initial, $reduce, $options); + } + $this->tryResultError($result); + + Yii::endProfile($token, __METHOD__); + if (array_key_exists('retval', $result)) { + return $result['retval']; + } else { + return []; + } + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int)$e->getCode(), $e); } - // Avoid possible E_DEPRECATED for $options: - if (empty($options)) { - $result = $this->mongoCollection->group($keys, $initial, $reduce); - } else { - $result = $this->mongoCollection->group($keys, $initial, $reduce, $options); - } + } - Yii::endProfile($token, __METHOD__); - if (array_key_exists('retval', $result)) { - return $result['retval']; - } else { - return []; + /** + * Performs aggregation using Mongo "map reduce" mechanism. + * Note: this function will not return the aggregation result, instead it will + * write it inside the another Mongo collection specified by "out" parameter. + * @param \MongoCode|string $map function, which emits map data from collection. + * Argument will be automatically cast to [[\MongoCode]]. + * @param \MongoCode|string $reduce function that takes two arguments (the map key + * and the map values) and does the aggregation. + * Argument will be automatically cast to [[\MongoCode]]. + * @param string|array $out output collection name. It could be a string for simple output + * ('outputCollection'), or an array for parametrized output (['merge' => 'outputCollection']) + * @param array $condition criteria for including a document in the aggregation. + * @return string the map reduce output collection name. + * @throws Exception on failure. + */ + public function mapReduce($map, $reduce, $out, $condition = []) + { + $token = 'Map reduce from ' . $this->getFullName(); + Yii::info($token, __METHOD__); + + try { + Yii::beginProfile($token, __METHOD__); + if (!($map instanceof \MongoCode)) { + $map = new \MongoCode((string)$map); + } + if (!($reduce instanceof \MongoCode)) { + $reduce = new \MongoCode((string)$reduce); + } + $command = [ + 'mapReduce' => $this->getName(), + 'map' => $map, + 'reduce' => $reduce, + 'out' => $out + ]; + if (!empty($condition)) { + $command['query'] = $this->buildCondition($condition); + } + + $result = $this->mongoCollection->db->command($command); + $this->tryResultError($result); + Yii::endProfile($token, __METHOD__); + return $result['result']; + } catch (\Exception $e) { + Yii::endProfile($token, __METHOD__); + throw new Exception($e->getMessage(), (int)$e->getCode(), $e); } } diff --git a/tests/unit/extensions/mongo/CollectionTest.php b/tests/unit/extensions/mongo/CollectionTest.php index e71df67..4bce0fa 100644 --- a/tests/unit/extensions/mongo/CollectionTest.php +++ b/tests/unit/extensions/mongo/CollectionTest.php @@ -10,6 +10,7 @@ class CollectionTest extends MongoTestCase protected function tearDown() { $this->dropCollection('customer'); + $this->dropCollection('mapReduceOut'); parent::tearDown(); } @@ -157,7 +158,7 @@ class CollectionTest extends MongoTestCase /** * @depends testBatchInsert */ - public function testMapReduce() + public function testGroup() { $collection = $this->getConnection()->getCollection('customer'); $rows = [ @@ -175,12 +176,65 @@ class CollectionTest extends MongoTestCase $keys = ['address' => 1]; $initial = ['items' => []]; $reduce = "function (obj, prev) { prev.items.push(obj.name); }"; - $result = $collection->mapReduce($keys, $initial, $reduce); + $result = $collection->group($keys, $initial, $reduce); $this->assertEquals(2, count($result)); $this->assertNotEmpty($result[0]['address']); $this->assertNotEmpty($result[0]['items']); } + /** + * @depends testBatchInsert + */ + public function testMapReduce() + { + $collection = $this->getConnection()->getCollection('customer'); + $rows = [ + [ + 'name' => 'customer 1', + 'status' => 1, + 'amount' => 100, + ], + [ + 'name' => 'customer 2', + 'status' => 1, + 'amount' => 200, + ], + [ + 'name' => 'customer 2', + 'status' => 2, + 'amount' => 400, + ], + [ + 'name' => 'customer 2', + 'status' => 3, + 'amount' => 500, + ], + ]; + $collection->batchInsert($rows); + + $result = $collection->mapReduce( + 'function () {emit(this.status, this.amount)}', + 'function (key, values) {return Array.sum(values)}', + 'mapReduceOut', + ['status' => ['$lt' => 3]] + ); + $this->assertEquals('mapReduceOut', $result); + + $outputCollection = $this->getConnection()->getCollection($result); + $rows = $outputCollection->findAll(); + $expectedRows = [ + [ + '_id' => 1, + 'value' => 300, + ], + [ + '_id' => 2, + 'value' => 400, + ], + ]; + $this->assertEquals($expectedRows, $rows); + } + public function testCreateIndex() { $collection = $this->getConnection()->getCollection('customer');