Forráskód Böngészése

Merge branch 'implement-batch-operations'

Closes #25, Closes #11.

* implement-batch-operations:
  Implement batch operations
Andreas Braun 10 éve
szülő
commit
a83c37ca1f

+ 2 - 16
lib/Alcaeus/MongoDbAdapter/Helper/WriteConcern.php

@@ -20,6 +20,8 @@ namespace Alcaeus\MongoDbAdapter\Helper;
  */
 trait WriteConcern
 {
+    use WriteConcernConverter;
+
     /**
      * @var \MongoDB\Driver\WriteConcern
      */
@@ -51,22 +53,6 @@ trait WriteConcern
     /**
      * @param string|int $wstring
      * @param int $wtimeout
-     * @return \MongoDB\Driver\WriteConcern
-     */
-    protected function createWriteConcernFromParameters($wstring, $wtimeout)
-    {
-        if (! is_string($wstring) && ! is_int($wstring)) {
-            trigger_error("w for WriteConcern must be a string or integer", E_WARNING);
-            return false;
-        }
-
-        // Ensure wtimeout is not < 0
-        return new \MongoDB\Driver\WriteConcern($wstring, max($wtimeout, 0));
-    }
-
-    /**
-     * @param string|int $wstring
-     * @param int $wtimeout
      * @return bool
      */
     protected function setWriteConcernFromParameters($wstring, $wtimeout = 0)

+ 47 - 0
lib/Alcaeus/MongoDbAdapter/Helper/WriteConcernConverter.php

@@ -0,0 +1,47 @@
+<?php
+/*
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+namespace Alcaeus\MongoDbAdapter\Helper;
+
+trait WriteConcernConverter
+{
+    /**
+     * @param string|int $wstring
+     * @param int $wtimeout
+     * @return \MongoDB\Driver\WriteConcern
+     */
+    protected function createWriteConcernFromParameters($wstring, $wtimeout)
+    {
+        if (! is_string($wstring) && ! is_int($wstring)) {
+            trigger_error("w for WriteConcern must be a string or integer", E_WARNING);
+            return false;
+        }
+
+        // Ensure wtimeout is not < 0
+        return new \MongoDB\Driver\WriteConcern($wstring, max($wtimeout, 0));
+    }
+
+    /**
+     * @param array $writeConcernArray
+     * @return \MongoDB\Driver\WriteConcern
+     */
+    protected function createWriteConcernFromArray($writeConcernArray)
+    {
+        $wstring = isset($writeConcernArray['w']) ? $writeConcernArray['w'] : 1;
+        $wtimeout = isset($writeConcernArray['wtimeout']) ? $writeConcernArray['wtimeout'] : 0;
+
+        return $this->createWriteConcernFromParameters($wstring, $wtimeout);
+    }
+}

+ 35 - 0
lib/Mongo/MongoDeleteBatch.php

@@ -0,0 +1,35 @@
+<?php
+/*
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * Constructs a batch of DELETE operations
+ *
+ * @see http://php.net/manual/en/class.mongodeletebatch.php
+ * @see http://php.net/manual/en/class.mongowritebatch.php
+ */
+class MongoDeleteBatch extends MongoWriteBatch
+{
+    /**
+     * Creates a new batch of delete operations
+     *
+     * @see http://php.net/manual/en/mongodeletebatch.construct.php
+     * @param MongoCollection $collection
+     * @param array $writeOptions
+     */
+    public function __construct(MongoCollection $collection, array $writeOptions = [])
+    {
+        parent::__construct($collection, self::COMMAND_DELETE, $writeOptions);
+    }
+}

+ 35 - 0
lib/Mongo/MongoInsertBatch.php

@@ -0,0 +1,35 @@
+<?php
+/*
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * Constructs a batch of INSERT operations
+ *
+ * @see http://php.net/manual/en/class.mongoinsertbatch.php
+ * @see http://php.net/manual/en/class.mongowritebatch.php
+ */
+class MongoInsertBatch extends MongoWriteBatch
+{
+    /**
+     * Creates a new batch of insert operations
+     *
+     * @see http://php.net/manual/en/mongoinsertbatch.construct.php
+     * @param MongoCollection $collection
+     * @param array $writeOptions
+     */
+    public function __construct(MongoCollection $collection, array $writeOptions = [])
+    {
+        parent::__construct($collection, self::COMMAND_INSERT, $writeOptions);
+    }
+}

+ 35 - 0
lib/Mongo/MongoUpdateBatch.php

@@ -0,0 +1,35 @@
+<?php
+/*
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/**
+ * Constructs a batch of UPDATE operations
+ *
+ * @see http://php.net/manual/en/class.mongoupdatebatch.php
+ * @see http://php.net/manual/en/class.mongowritebatch.php
+ */
+class MongoUpdateBatch extends MongoWriteBatch
+{
+    /**
+     * Creates a new batch of update operations
+     *
+     * @see http://php.net/manual/en/mongoupdatebatch.construct.php
+     * @param MongoCollection $collection
+     * @param array $writeOptions
+     */
+    public function __construct(MongoCollection $collection, array $writeOptions = [])
+    {
+        parent::__construct($collection, self::COMMAND_UPDATE, $writeOptions);
+    }
+}

+ 180 - 0
lib/Mongo/MongoWriteBatch.php

@@ -0,0 +1,180 @@
+<?php
+/*
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+use Alcaeus\MongoDbAdapter\TypeConverter;
+use Alcaeus\MongoDbAdapter\Helper\WriteConcernConverter;
+
+/**
+ * MongoWriteBatch allows you to "batch up" multiple operations (of same type)
+ * and shipping them all to MongoDB at the same time. This can be especially
+ * useful when operating on many documents at the same time to reduce roundtrips.
+ *
+ * @see http://php.net/manual/en/class.mongowritebatch.php
+ */
+class MongoWriteBatch
+{
+    use WriteConcernConverter;
+
+    const COMMAND_INSERT = 1;
+    const COMMAND_UPDATE = 2;
+    const COMMAND_DELETE = 3;
+
+    /**
+     * @var MongoCollection
+     */
+    private $collection;
+
+    /**
+     * @var int
+     */
+    private $batchType;
+
+    /**
+     * @var array
+     */
+    private $writeOptions;
+
+    /**
+     * @var array
+     */
+    private $items = [];
+
+    /**
+     * Creates a new batch of write operations
+     *
+     * @see http://php.net/manual/en/mongowritebatch.construct.php
+     * @param MongoCollection $collection
+     * @param int $batchType
+     * @param array $writeOptions
+     */
+    protected function __construct(MongoCollection $collection, $batchType, $writeOptions)
+    {
+        $this->collection = $collection;
+        $this->batchType = $batchType;
+        $this->writeOptions = $writeOptions;
+    }
+
+    /**
+     * Adds a write operation to a batch
+     *
+     * @see http://php.net/manual/en/mongowritebatch.add.php
+     * @param array|object $item
+     * @return boolean
+     */
+    public function add($item)
+    {
+        if (is_object($item)) {
+            $item = (array)$item;
+        }
+
+        $this->validate($item);
+        $this->addItem($item);
+
+        return true;
+    }
+
+    /**
+     * Executes a batch of write operations
+     *
+     * @see http://php.net/manual/en/mongowritebatch.execute.php
+     * @param array $writeOptions
+     * @return array
+     */
+    final public function execute(array $writeOptions = [])
+    {
+        $writeOptions += $this->writeOptions;
+        if (! count($this->items)) {
+            return ['ok' => true];
+        }
+
+        if (isset($writeOptions['j'])) {
+            trigger_error('j parameter is not supported', E_WARNING);
+        }
+        if (isset($writeOptions['fsync'])) {
+            trigger_error('fsync parameter is not supported', E_WARNING);
+        }
+
+        $options['writeConcern'] = $this->createWriteConcernFromArray($writeOptions);
+        if (isset($writeOptions['ordered'])) {
+            $options['ordered'] = $writeOptions['ordered'];
+        }
+
+        $collection = $this->collection->getCollection();
+
+        try {
+            $result = $collection->BulkWrite($this->items, $options);
+            $ok = 1.0;
+        } catch (\MongoDB\Driver\Exception\BulkWriteException $e) {
+            $result = $e->getWriteResult();
+            $ok = 0.0;
+        }
+
+        if ($ok === 1.0) {
+            $this->items = [];
+        }
+
+        return [
+            'ok' => $ok,
+            'nInserted' => $result->getInsertedCount(),
+            'nMatched' => $result->getMatchedCount(),
+            'nModified' => $result->getModifiedCount(),
+            'nUpserted' => $result->getUpsertedCount(),
+            'nRemoved' => $result->getDeletedCount(),
+        ];
+    }
+
+    private function validate(array $item)
+    {
+        switch ($this->batchType) {
+            case self::COMMAND_UPDATE:
+                if (! isset($item['q']) || ! isset($item['u'])) {
+                    throw new Exception('invalid item');
+                }
+                break;
+
+            case self::COMMAND_DELETE:
+                if (! isset($item['q']) || ! isset($item['limit'])) {
+                    throw new Exception('invalid item');
+                }
+                break;
+        }
+    }
+
+    private function addItem(array $item)
+    {
+        switch ($this->batchType) {
+            case self::COMMAND_UPDATE:
+                $method = isset($item['multi']) ? 'updateMany' : 'updateOne';
+
+                $options = [];
+                if (isset($item['upsert']) && $item['upsert']) {
+                    $options['upsert'] = true;
+                }
+
+                $this->items[] = [$method => [TypeConverter::fromLegacy($item['q']), TypeConverter::fromLegacy($item['u']), $options]];
+                break;
+
+            case self::COMMAND_INSERT:
+                $this->items[] = ['insertOne' => [TypeConverter::fromLegacy($item)]];
+                break;
+
+            case self::COMMAND_DELETE:
+                $method = $item['limit'] === 0 ? 'deleteMany' : 'deleteOne';
+
+                $this->items[] = [$method => [TypeConverter::fromLegacy($item['q'])]];
+                break;
+        }
+    }
+}

+ 67 - 0
tests/Alcaeus/MongoDbAdapter/MongoDeleteBatchTest.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Alcaeus\MongoDbAdapter\Tests;
+
+class MongoDeleteBatchTest extends TestCase
+{
+    public function testDeleteOne()
+    {
+        $collection = $this->getCollection();
+        $batch = new \MongoDeleteBatch($collection);
+
+        $collection->insert(['foo' => 'bar']);
+        $collection->insert(['foo' => 'bar']);
+
+        $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'limit' => 1]));
+
+        $expected = [
+            'ok' => 1.0,
+            'nInserted' => 0,
+            'nMatched' => 0,
+            'nModified' => 0,
+            'nUpserted' => 0,
+            'nRemoved' => 1,
+        ];
+
+        $this->assertSame($expected, $batch->execute());
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(1, $newCollection->count());
+    }
+
+    public function testDeleteMany()
+    {
+        $collection = $this->getCollection();
+        $batch = new \MongoDeleteBatch($collection);
+
+        $collection->insert(['foo' => 'bar']);
+        $collection->insert(['foo' => 'bar']);
+
+        $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'limit' => 0]));
+
+        $expected = [
+            'ok' => 1.0,
+            'nInserted' => 0,
+            'nMatched' => 0,
+            'nModified' => 0,
+            'nUpserted' => 0,
+            'nRemoved' => 2,
+        ];
+
+        $this->assertSame($expected, $batch->execute());
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(0, $newCollection->count());
+    }
+
+
+    public function testValidateItem()
+    {
+        $collection = $this->getCollection();
+        $batch = new \MongoDeleteBatch($collection);
+
+        $this->setExpectedException('Exception', 'invalid item');
+
+        $batch->add([]);
+    }
+}

+ 53 - 0
tests/Alcaeus/MongoDbAdapter/MongoInsertBatchTest.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace Alcaeus\MongoDbAdapter\Tests;
+
+class MongoInsertBatchTest extends TestCase
+{
+    public function testInsertBatch()
+    {
+        $batch = new \MongoInsertBatch($this->getCollection());
+
+        $this->assertTrue($batch->add(['foo' => 'bar']));
+        $this->assertTrue($batch->add(['bar' => 'foo']));
+
+        $expected = [
+            'ok' => 1.0,
+            'nInserted' => 2,
+            'nMatched' => 0,
+            'nModified' => 0,
+            'nUpserted' => 0,
+            'nRemoved' => 0,
+        ];
+
+        $this->assertSame($expected, $batch->execute());
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(2, $newCollection->count());
+        $record = $newCollection->findOne();
+        $this->assertNotNull($record);
+        $this->assertObjectHasAttribute('foo', $record);
+        $this->assertAttributeSame('bar', 'foo', $record);
+    }
+
+    public function testInsertBatchError()
+    {
+        $collection = $this->getCollection();
+        $batch = new \MongoInsertBatch($collection);
+        $collection->createIndex(['foo' => 1], ['unique' => true]);
+
+        $this->assertTrue($batch->add(['foo' => 'bar']));
+        $this->assertTrue($batch->add(['foo' => 'bar']));
+
+        $expected = [
+            'ok' => 0.0,
+            'nInserted' => 1,
+            'nMatched' => 0,
+            'nModified' => 0,
+            'nUpserted' => 0,
+            'nRemoved' => 0,
+        ];
+
+        $this->assertSame($expected, $batch->execute());
+    }
+}

+ 101 - 0
tests/Alcaeus/MongoDbAdapter/MongoUpdateBatchTest.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace Alcaeus\MongoDbAdapter\Tests;
+
+class MongoUpdateBatchTest extends TestCase
+{
+
+
+    public function testUpdateOne()
+    {
+        $collection = $this->getCollection();
+        $batch = new \MongoUpdateBatch($collection);
+
+        $collection->insert(['foo' => 'bar']);
+
+        $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'u' => ['$set' => ['foo' => 'foo']]]));
+
+        $expected = [
+            'ok' => 1.0,
+            'nInserted' => 0,
+            'nMatched' => 1,
+            'nModified' => 1,
+            'nUpserted' => 0,
+            'nRemoved' => 0,
+        ];
+
+        $this->assertSame($expected, $batch->execute());
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(1, $newCollection->count());
+        $record = $newCollection->findOne();
+        $this->assertNotNull($record);
+        $this->assertObjectHasAttribute('foo', $record);
+        $this->assertAttributeSame('foo', 'foo', $record);
+    }
+
+    public function testUpdateMany()
+    {
+        $collection = $this->getCollection();
+        $batch = new \MongoUpdateBatch($collection);
+
+        $collection->insert(['foo' => 'bar']);
+        $collection->insert(['foo' => 'bar']);
+
+
+        $this->assertTrue($batch->add(['q' => ['foo' => 'bar'], 'u' => ['$set' => ['foo' => 'foo']], 'multi' => true]));
+
+        $expected = [
+            'ok' => 1.0,
+            'nInserted' => 0,
+            'nMatched' => 2,
+            'nModified' => 2,
+            'nUpserted' => 0,
+            'nRemoved' => 0,
+        ];
+
+        $this->assertSame($expected, $batch->execute());
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(2, $newCollection->count());
+        $record = $newCollection->findOne();
+        $this->assertNotNull($record);
+        $this->assertObjectHasAttribute('foo', $record);
+        $this->assertAttributeSame('foo', 'foo', $record);
+    }
+
+    public function testUpsert()
+    {
+        $batch = new \MongoUpdateBatch($this->getCollection());
+
+        $this->assertTrue($batch->add(['q' => [], 'u' => ['$set' => ['foo' => 'bar']], 'upsert' => true]));
+
+        $expected = [
+            'ok' => 1.0,
+            'nInserted' => 0,
+            'nMatched' => 0,
+            'nModified' => 0,
+            'nUpserted' => 1,
+            'nRemoved' => 0,
+        ];
+
+        $this->assertSame($expected, $batch->execute());
+
+        $newCollection = $this->getCheckDatabase()->selectCollection('test');
+        $this->assertSame(1, $newCollection->count());
+        $record = $newCollection->findOne();
+        $this->assertNotNull($record);
+        $this->assertObjectHasAttribute('foo', $record);
+        $this->assertAttributeSame('bar', 'foo', $record);
+    }
+
+    public function testValidateItem()
+    {
+        $collection = $this->getCollection();
+        $batch = new \MongoUpdateBatch($collection);
+
+        $this->setExpectedException('Exception', 'invalid item');
+
+        $batch->add([]);
+    }
+}