Commits

Jon Langevin committed eff7677

Mongo document saving will now update only the elements that changed for the object being updated, by comparing new data against the originally requested data.
Mongo fetch & update also uses dot notation so that exact document matching isn't necessary, also avoids data loss due to updates that occur across each other.

Three new static methods have been added to the mongo Object: dotNotation(), recurseDiff(), and arrayObject(); these methods are respectively used for conversion of an array/object to dot notation syntax, recursively comparing two or more variables, and converting an array to an object (stdclass). The arrayObject() method is currently unusued, and may be removed.

Document has been updated so that calling a relation using magic methods, can now additionally provide PKs to filter the results by.
Changed how relations are managed & stored. Removed redundant criteria parameter from getRelated().
Updated stringify method to sort arrays by key before converting to string; additionally converting numeric values to strings for consistency.

Comments (0)

Files changed (2)

         if (isset($this->getMetaData()->relations->$name)) {
             if (empty($parameters))
                 return $this->getRelated($name, false);
-            else
+            elseif (isset($parameters[0]) && isset($parameters[1]))
+                return $this->getRelated($name, false, $parameters[0], $parameters[1]);
+            elseif (isset($parameters[0]))
                 return $this->getRelated($name, false, $parameters[0]);
         }
 
      * @param string           $name        The relation name (see {@link relations})
      * @param array            $indexes     Array of 'indexName'=>'searchValue' to search by
      * @param bool             $refresh     Whether to force reload objects from db
-     * @param array            $params      Additional parameters to customize query
+     * @param array|Criteria   $criteria      Additional parameters to customize query
      * @param array            $keys        Array of keys to additionally filter by
-     * @param array|Criteria   $criteria    Optional criteria to further customize relation query
      *
      * @return array
      */
-    public function &getRelatedByIndex($name, array $indexes, $refresh = false, array $params = array(), array $keys = array(), $criteria = array()) {
+    public function &getRelatedByIndex($name, array $indexes, $refresh = false, $criteria = array(), array $keys = array()) {
         $pks = $this->getRelatedKeysByIndex($name, $indexes, $keys);
         if ($pks === array())
             return $pks;
         Yii::trace('Requesting related records for relation ' . get_class($this) . '.' . $name . ', filtered by ' . \CVarDumper::dumpAsString($indexes), 'ext.activedocument.document.getRelatedByIndex');
-        $related = $this->getRelated($name, $refresh, $params, $pks, $criteria);
+        $related = $this->getRelated($name, $refresh, $criteria, $pks);
         return $related;
     }
 
      *
      * @param string           $name        the relation name (see {@link relations})
      * @param boolean          $refresh     Optional whether to reload the related objects from database. Defaults to false.
-     * @param array            $params      Optional additional parameters that customize the query conditions as specified in the relation declaration.
+     * @param array|Criteria   $criteria    Optional criteria to customize the relation query.
      * @param array            $keys        Optional Array of encoded primary keys to filter by on HasMany or ManyMany relations
-     * @param array|Criteria   $criteria    Optional criteria to further customize relation query
      *
      * @return mixed the related object(s).
      * @throws Exception if the relation is not specified in {@link relations}.
      */
-    public function &getRelated($name, $refresh = false, array $params = array(), array $keys = array(), $criteria = array()) {
+    public function &getRelated($name, $refresh = false, $criteria = array(), array $keys = array()) {
         if ($keys !== array() && isset($this->getMetaData()->relations[$name]))
             $keys = array_combine(array_map(array('self','stringify'), $keys),
                 array_map(array(Document::model($this->getMetaData()->relations[$name]->className), 'typecastPk'), $keys));
-        if (!$refresh && $params === array() && $criteria === array() && (isset($this->_related[$name]) || array_key_exists($name, $this->_related))) {
+        if (!$refresh && $criteria === array() && (isset($this->_related[$name]) || array_key_exists($name, $this->_related))) {
             if ($keys !== array() && is_array($this->_related[$name])) {
                 $related = array_filter($this->_related[$name], function(Document $document) use($keys) {
                     return in_array($document->getPrimaryKey(), $keys, true);
             return $_r;
         }
 
-        if ($params !== array() || $criteria !== array() || ($relation instanceof HasManyRelation && $keys !== array())) { // dynamic query
+        if ($criteria !== array() || ($relation instanceof HasManyRelation && $keys !== array())) { // dynamic query
             $exists = $this->hasRelated($name);
             if ($exists)
                 $save = $this->_related[$name];
         $data = $this->getObject()->data;
         if (isset($data[$name])) {
             $finder    = Document::model($relation->className);
-            $_criteria = new Criteria();
-            /**
-             * @todo The solution below for merging relation settings into standard criteria, could use more elegance
-             */
-            $relCriteria = array();
-            array_map(function($key) use(&$relCriteria, $relation) {
-                if (isset($relation->$key))
-                    $relCriteria[$key] = $relation->$key;
-            }, array_keys($_criteria->toArray()));
+            $finder->resetScope();
 
-            if ($relCriteria !== array())
-                $_criteria->mergeWith($relCriteria);
-
-            if ($criteria !== array())
-                $_criteria->mergeWith($criteria);
-
-            if ($relation instanceof Relation && $relation->nested === true && $params === array() && $criteria === array()) {
+            if ($relation instanceof Relation && $relation->nested === true && $criteria === array()) {
                 Yii::trace('Loading nested ' . get_class($this) . '.' . $name, 'ext.activedocument.document.getRelated');
                 if ($relation instanceof HasManyRelation) {
                     if ($keys !== array())
                     } else {
                         $pks = $data[$name];
                         if ($keys !== array())
-                            $pks = array_intersect($pks, $keys);
+                            $pks = array_intersect_key($data[$name], $keys);
                     }
-                    $this->_related[$name] = $finder->findAllByPk(array_filter($pks), $_criteria, $params);
+                    $this->_related[$name] = $finder->findAllByPk(array_filter($pks), $criteria);
                 } elseif ($relation instanceof StatRelation) {
-                    $this->_related[$name] = $finder->count($_criteria, $params);
+                    $this->_related[$name] = $finder->count($criteria);
                 } else {
                     if ($relation->nested === true) {
                         /**
                             $finder->getContainer()->getObject(null, $data[$name], true)
                         );
                         if ($obj !== null)
-                            $this->_related[$name] = $finder->findByPk($obj->getPrimaryKey(), $_criteria, $params);
+                            $this->_related[$name] = $finder->findByPk($obj->getPrimaryKey(), $criteria);
                     } else {
-                        $this->_related[$name] = $finder->findByPk($data[$name], $_criteria, $params);
+                        $this->_related[$name] = $finder->findByPk($data[$name], $criteria);
                     }
                 }
             }
                 $this->_related[$name] = null;
         }
 
-        if ($params !== array() || $criteria !== array() || ($relation instanceof HasManyRelation && $keys !== array())) {
+        if ($criteria !== array() || ($relation instanceof HasManyRelation && $keys !== array())) {
             $results = $this->_related[$name];
             if ($exists)
                 $this->_related[$name] = $save;
      * @return string
      */
     public static function stringify($var) {
-        if (is_array($var))
+        if (is_array($var)) {
+            /**
+             * Sort by key to ensure consistent output
+             */
+            ksort($var);
             return \CJSON::encode(array_map(array('self', 'stringify'), $var));
-        if (is_object($var)) {
-            if (method_exists($var, '__toString'))
-                return $var->__toString();
-            else
-                return \CJSON::encode($var);
         }
-        return (string)$var;
+        if (is_object($var) && !(method_exists($var, '__toString')))
+            return \CJSON::encode($var);
+        /**
+         * Test for int numbers, ensure storage as string
+         */
+        if (is_numeric($var) && is_int($var+0) && substr_compare($var, 0, 0, 1)!==0)
+            return '0'.strval($var);
+        return \CPropertyValue::ensureString($var);
     }
 
     /* public function cache($duration, $dependency=null, $queryCount=1) {
                      * If the relation was already set, skip
                      */
                     if (isset($this->getObject()->data[$name]) && !$model->getIsNewRecord() &&
-                        ((!$relations[$name]->nested && in_array($model->getPrimaryKey(), $this->getObject()->data[$name], true)) ||
-                            ($relations[$name]->nested && array_key_exists($model->encodedPk, $this->getObject()->data[$name]) &&
-                                !$model->isModified))
+                        array_key_exists($model->encodedPk, $this->getObject()->data[$name]) && (!$relations[$name]->nested ||
+                            ($relations[$name]->nested && !$model->isModified))
                     ) {
                         continue;
                     }
      */
     public function appendRelation(Document $relationModel, $relationName) {
         $pk = $relationModel->getPrimaryKey();
-        if (empty($pk))
-            throw new Exception(Yii::t('yii', 'Related model primary key must not be empty!') .
-                PHP_EOL . 'Model: ' . \CVarDumper::dumpAsString($relationModel->getAttributes()));
+        if (!isset($pk))
+            throw new Exception(Yii::t('yii', 'Related model primary key must not be empty for relation ' . get_class($this) . '.' . $relationName . '!')
+                /*. PHP_EOL . 'This Class: ' . get_class($this)
+                . PHP_EOL . 'Relation Class: ' . get_class($relationModel)
+                . PHP_EOL . 'Related model attributes: ' . \CVarDumper::dumpAsString($relationModel->getAttributes())
+                . PHP_EOL . 'Related model raw data: ' . \CVarDumper::dumpAsString($relationModel->object->data)*/
+            );
+        $encodedPk = $relationModel->getEncodedPk();
 
         /**
          * @var \ext\activedocument\Relation
                         $this->getObject()->data[$indexName] = array();
                     if (!isset($this->getObject()->data[$indexName][$indexValue]) || !is_array($this->getObject()->data[$indexName][$indexValue]))
                         $this->getObject()->data[$indexName][$indexValue] = array();
-                    if (!in_array($pk, $this->getObject()->data[$indexName][$indexValue], true))
-                        $this->getObject()->data[$indexName][$indexValue][] = $pk;
+                    if (!isset($this->getObject()->data[$indexName][$indexValue][$encodedPk]))
+                        $this->getObject()->data[$indexName][$indexValue][$encodedPk] = $pk;
                 }
             }
             if (!isset($this->getObject()->data[$relationName]) || !is_array($this->getObject()->data[$relationName]))
                 $this->getObject()->data[$relationName] = array();
             if ($relation->nested === true) {
-                $this->getObject()->data[$relationName][$relationModel->getEncodedPk()] = $relationModel->getObject()->data;
-            } elseif (!in_array($pk, $this->getObject()->data[$relationName], true))
-                $this->getObject()->data[$relationName][] = $pk;
+                $this->getObject()->data[$relationName][$encodedPk] = $relationModel->getObject()->data;
+            } elseif (!array_key_exists($encodedPk, $this->getObject()->data[$relationName]))
+                $this->getObject()->data[$relationName][$encodedPk] = $pk;
         } else
             $this->getObject()->data[$relationName] = $relation->nested ? $relationModel->getObject()->data : $pk;
 
             return;
 
         $pk = $relationModel->getPrimaryKey();
-        if (empty($pk))
+        if (!isset($pk))
             throw new Exception(Yii::t('yii', 'Related model primary key must not be empty!'));
+        $encodedPk = $relationModel->getEncodedPk();
 
         /**
          * @var \ext\activedocument\Relation
         $relation = $this->getMetaData()->relations->$relationName;
 
         if ($relation instanceof HasManyRelation && is_array($this->getObject()->data[$relationName])) {
-            $key = $relation->nested ? $relationModel->getEncodedPk() : array_search($pk, $this->getObject()->data[$relationName]);
-            if ($key !== false)
-                unset($this->getObject()->data[$relationName][$key]);
+            unset($this->getObject()->data[$relationName][$encodedPk]);
             /**
              * Remove related model pk from autoindices
              */
                     $indexName = $relationName . '_' . $index;
                     if (!isset($this->getObject()->data[$indexName]) || !is_array($this->getObject()->data[$indexName]))
                         continue;
-                    array_walk($this->getObject()->data[$indexName], function(&$v, $k) use($pk) {
-                        if (($_k = array_search($pk, $v)) !== false)
-                            unset($v[$_k]);
+                    array_walk($this->getObject()->data[$indexName], function(&$v, $k) use($encodedPk) {
+                        if (isset($v[$encodedPk]))
+                            unset($v[$encodedPk]);
                     });
                 }
             }
         } else
-            unset($this->getObject()->data[$relationName]);
+            $this->getObject()->data[$relationName] = null;
 
         $this->_modifiedAttributes[] = $relationName;
     }
      * @param string $relationName
      */
     public function clearRelation($relationName) {
-        unset($this->getObject()->data[$relationName]);
-
         /**
          * @var \ext\activedocument\Relation
          */
         $relation = $this->getMetaData()->relations->$relationName;
 
         if ($relation instanceof HasManyRelation) {
+            $this->getObject()->data[$relationName] = array();
             /**
              * Remove autoindices
              */
                 foreach ($relation->autoIndices as $index) {
                     $indexName = $relationName . '_' . $index;
                     if (isset($this->getObject()->data[$indexName]))
-                        unset($this->getObject()->data[$indexName]);
+                        $this->getObject()->data[$indexName] = array();
                 }
             }
+        } else {
+            $this->getObject()->data[$relationName] = null;
         }
 
         $this->_modifiedAttributes[] = $relationName;
             $this->_object->data[$name] = $value;
         }
         $this->_object->setKey($this->_pk);
+        Yii::trace('Storing object of model "'. get_class($this) .'" with content: '. \CVarDumper::dumpAsString($this->_object->data), 'ext.activedocument.Document');
         return $this->_object->store();
     }
 
 
     /**
      * @param Criteria|array   $condition Optional. Default: null
-     * @param array            $params
      *
      * @return int
      */
-    public function count($condition = null, array $params = array()) {
+    public function count($condition = null) {
         Yii::trace(get_class($this) . '.count()', 'ext.activedocument.Document');
-        $criteria = $this->buildCriteria($condition, $params);
+        $criteria = $this->buildCriteria($condition);
         $this->applyScopes($criteria);
         return $this->_container->count($criteria);
     }
 
     /**
      * Checks whether there is row satisfying the specified condition.
-     * See {@link find()} for detailed explanation about $condition and $params.
+     * See {@link find()} for detailed explanation about $condition.
      *
      * @param Criteria|array   $condition Optional. Default: null
-     * @param array            $params    Optional.
      *
      * @return boolean whether there is row satisfying the specified condition.
      */
-    public function exists($condition = null, array $params = array()) {
+    public function exists($condition = null) {
         Yii::trace(get_class($this) . '.exists()', 'ext.activedocument.Document');
-        $criteria        = $this->buildCriteria($condition, $params);
+        $criteria        = $this->buildCriteria($condition);
         $criteria->limit = 1;
         $this->applyScopes($criteria);
         return $this->_container->count($criteria) > 0;
 
     /**
      * @param Criteria|array   $condition Optional. Default: null
-     * @param array            $params    Optional.
      *
      * @return Document|null
      */
-    public function find($condition = null, array $params = array()) {
+    public function find($condition = null) {
         Yii::trace(get_class($this) . '.find()', 'ext.activedocument.Document');
-        return $this->query($this->buildCriteria($condition, $params));
+        return $this->query($this->buildCriteria($condition));
     }
 
     /**
      * @param mixed            $key
      * @param Criteria|array   $condition Optional. Default: null
-     * @param array            $params    Optional.
      *
      * @return Document|null
      */
-    public function findByPk($key, $condition = null, array $params = array()) {
+    public function findByPk($key, $condition = null) {
         Yii::trace(get_class($this) . '.findByPk()', 'ext.activedocument.Document');
-        return $this->query($this->buildCriteria($condition, $params), false, array($key));
+        return $this->query($this->buildCriteria($condition), false, array($key));
     }
 
     /**
      * Finds a single document that has the specified attribute values.
-     * See {@link find()} for detailed explanation about $condition and $params.
+     * See {@link find()} for detailed explanation about $condition.
      *
      * @param array            $attributes list of attribute values (indexed by attribute names) that the documents should match.
      *                                     An attribute value can be an array which will be used to generate an array (IN) condition.
      * @param Criteria|array   $condition  Optional. Default: null
-     * @param array            $params     Optional.
      *
      * @return Document|null the record found. Null if none is found.
      */
-    public function findByAttributes($attributes, $condition = null, array $params = array()) {
+    public function findByAttributes($attributes, $condition = null) {
         Yii::trace(get_class($this) . '.findByAttributes()', 'ext.activedocument.Document');
-        return $this->query($this->buildCriteria($condition, $params, $attributes), false);
+        return $this->query($this->buildCriteria($condition, $attributes), false);
     }
 
     /**
      * @param Criteria|array   $condition Optional. Default: null
-     * @param array            $params    Optional.
      *
      * @return array|Document|null
      */
-    public function findAll($condition = null, array $params = array()) {
+    public function findAll($condition = null) {
         Yii::trace(get_class($this) . '.findAll()', 'ext.activedocument.Document');
-        return $this->query($this->buildCriteria($condition, $params), true);
+        return $this->query($this->buildCriteria($condition), true);
     }
 
     /**
      * @param array            $keys
      * @param Criteria|array   $condition Optional. Default: null
-     * @param array            $params    Optional.
      *
      * @return array|Document|null
      */
-    public function findAllByPk(array $keys, $condition = null, array $params = array()) {
+    public function findAllByPk(array $keys, $condition = null) {
         Yii::trace(get_class($this) . '.findAllByPk()', 'ext.activedocument.Document');
-        return $this->query($this->buildCriteria($condition, $params), true, $keys);
+        return $this->query($this->buildCriteria($condition), true, $keys);
     }
 
     /**
      * Finds all documents that have the specified attribute values.
-     * See {@link find()} for detailed explanation about $condition and $params.
+     * See {@link find()} for detailed explanation about $condition.
      *
      * @param array            $attributes list of attribute values (indexed by attribute names) that the documents should match.
      *                                     An attribute value can be an array which will be used to generate an array (IN) condition.
      * @param Criteria|array   $condition  Optional. Default: null
-     * @param array            $params     Optional.
      *
      * @return Document|null the record found. Null if none is found.
      */
-    public function findAllByAttributes($attributes, $condition = null, array $params = array()) {
+    public function findAllByAttributes($attributes, $condition = null) {
         Yii::trace(get_class($this) . '.findAllByAttributes()', 'ext.activedocument.Document');
-        return $this->query($this->buildCriteria($condition, $params, $attributes), true);
+        return $this->query($this->buildCriteria($condition, $attributes), true);
     }
 
     /**
 
     /**
      * @param Criteria|array   $condition
-     * @param array            $params
      * @param array            $attributes
      *
      * @return Criteria
      */
-    protected function buildCriteria($condition, array $params = array(), array $attributes = array()) {
+    protected function buildCriteria($condition, array $attributes = array()) {
         if (is_array($condition))
             $criteria = new Criteria($condition);
         else if ($condition instanceof Criteria)
         else
             $criteria = new Criteria;
 
-        if ($params !== array())
-            $criteria->mergeWith(array('params' => $params));
-
         if ($attributes !== array())
             array_walk($attributes, function($val, $key) use($criteria) {
                 /**

drivers/mongo/Object.php

         $data = null;
         if ($this->getKey() !== null && !$new) {
             \Yii::trace('Mongo FindByPk query: ' . \CVarDumper::dumpAsString($this->getKey()), 'ext.activedocument.drivers.mongo.Object');
-            $data = $this->_container->getContainerInstance()->findOne(array('_id' => $this->getKey()));
+            /**
+             * @todo Using dotnotation here allows partial id matching, may need to revert back to exact matching (need tests)
+             */
+            $data = $this->_container->getContainerInstance()->findOne(self::dotNotation($this->getKey(), '_id', null));
         }
         if ($data == null)
             $data = array();
      * @return bool
      */
     protected function storeInternal() {
+        $origData = $this->getObjectData();
         $this->setObjectData($this->data);
         try {
             /**
              * Ensure _id is not null or empty string. Empty string is a valid key in mongo
              */
-            if(isset($this->_objectInstance->_id) && ($this->_objectInstance->_id===null && $this->_objectInstance->_id!==''))
+            if (property_exists($this->_objectInstance, '_id') && ($this->_objectInstance->_id === null || $this->_objectInstance->_id === ''))
                 unset($this->_objectInstance->_id);
 
             /**
              * When we aren't specifying a pk, we should insert, which will update _objectInstance with new pk
              */
-            if(!isset($this->_objectInstance->_id))
-                $this->_container->getContainerInstance()->insert($this->_objectInstance, array('safe'=>true));
-            else
-                $this->_container->getContainerInstance()->save($this->_objectInstance, array('safe'=>true));
-            $this->data = $this->getObjectData();
-        }catch(\MongoException $e) {
-            /**
-             * @todo Throw custom exception
-             */
-            return false;
+            if (!isset($this->_objectInstance->_id)) {
+                \Yii::trace('Inserted the value: ' . \CVarDumper::dumpAsString($this->_objectInstance), 'ext.activedocument.drivers.mongo.Object');
+                $this->_container->getContainerInstance()->insert($this->_objectInstance, array('safe' => true));
+            } else {
+                $dataDiff = self::recurseDiff($this->getObjectData(), $origData);
+                $criteria = array('_id'=>$this->_objectInstance->_id);
+
+                if ($dataDiff!==array())
+                    $dataDiff = self::dotNotation($dataDiff);
+
+                if ($dataDiff===array()) {
+                    $result = $this->_container->getContainerInstance()->findOne($criteria);
+                    if ($result === null) {
+                        #\Yii::trace('Inserted value: ' . \CVarDumper::dumpAsString($this->_objectInstance), 'ext.activedocument.drivers.mongo.Object');
+                        $this->_container->getContainerInstance()->insert($this->_objectInstance, array('safe'=>true));
+                    }
+                } elseif (count($dataDiff)>1) {
+                    /**
+                     * Make sure that no field exists in more than one procedure
+                     */
+                    $splitQueries = array();
+                    $splitIndex = array();
+                    array_walk($dataDiff, function($dataArr, $outerKey)use(&$splitQueries, &$splitIndex){
+                        array_walk($dataArr, function($dataArr, $dataKey)use(&$splitQueries, &$splitIndex, $outerKey){
+                            $i=0;
+                            while(true) {
+                                if (!isset($splitQueries[$i]))
+                                    $splitQueries[$i] = $splitIndex[$i] = array();
+                                if (!in_array($dataKey, $splitIndex[$i])) {
+                                    $splitQueries[$i][$outerKey][$dataKey] = $dataArr;
+                                    $splitIndex[$i][] = $dataKey;
+                                    break;
+                                }
+                                $i++;
+                            }
+                        });
+                    });
+                    unset($dataDiff);
+
+                    /**
+                     * Sorting queries to ensure $pull requests issue first
+                     */
+                    usort($splitQueries, function($a, $b){
+                        $remove = array('$pull','$pullAll');
+                        if (array()!==array_intersect($remove, array_keys($a)))
+                            return -1;
+                        elseif (array()!==array_intersect($remove, array_keys($b)))
+                            return 1;
+                        else
+                            return 0;
+                    });
+
+                    #\Yii::trace('Stored multiple requests: ' . \CVarDumper::dumpAsString($splitQueries), 'ext.activedocument.drivers.mongo.Object');
+                    $instance = $this->_container->getContainerInstance();
+                    array_map(function($query)use($instance, $criteria){
+                        $instance->update($criteria, $query, array('safe'=>true, 'upsert'=>true));
+                    }, $splitQueries);
+                } else {
+                    #\Yii::trace('Stored the value: ' . \CVarDumper::dumpAsString($dataDiff), 'ext.activedocument.drivers.mongo.Object');
+                    $this->_container->getContainerInstance()->update($criteria, $dataDiff, array('safe'=>true, 'upsert'=>true));
+                }
+            }
+            $this->reloadInternal();
+        } catch (\MongoException $e) {
+            throw new \ext\activedocument\Exception('MongoDB threw an exception: ' . $e->getMessage(), 0, $e);
         }
         return true;
     }
     protected function deleteInternal() {
         $this->setObjectData($this->data);
         try {
-            $this->_container->getContainerInstance()->remove(array('_id'=> $this->getKey()), array('safe'=>true));
-        }catch(\MongoException $e) {
+            $this->_container->getContainerInstance()->remove(array('_id' => $this->getKey()), array('safe' => true));
+        } catch (\MongoException $e) {
             /**
              * @todo Throw custom exception
              */
     protected function reloadInternal() {
         $this->setObjectData($this->data);
         $this->_objectInstance = $this->loadObjectInstance(false);
-        $this->data = $this->getObjectData();
+        $this->data            = $this->getObjectData();
         return true;
     }
 
      * @return \MongoId|null|string
      */
     public function getKey() {
-        if($this->_objectInstance instanceof \ArrayObject && isset($this->_objectInstance->_id))
+        if ($this->_objectInstance instanceof \ArrayObject && isset($this->_objectInstance->_id))
             $key = $this->_objectInstance->_id;
         else
             $key = parent::getKey();
         $key = self::properId($key);
-        \Yii::trace('Mongo getKey(): ' . \CVarDumper::dumpAsString($key), 'ext.activedocument.drivers.mongo.Object');
+        #\Yii::trace('Mongo getKey(): ' . \CVarDumper::dumpAsString($key), 'ext.activedocument.drivers.mongo.Object');
 
         return $key;
     }
      */
     public function setKey($value) {
         $value = self::properId($value);
-        \Yii::trace('Mongo setKey(): ' . \CVarDumper::dumpAsString($value), 'ext.activedocument.drivers.mongo.Object');
-        if($this->_objectInstance instanceof \ArrayObject) {
+        #\Yii::trace('Mongo setKey(): ' . \CVarDumper::dumpAsString($value), 'ext.activedocument.drivers.mongo.Object');
+        if ($this->_objectInstance instanceof \ArrayObject) {
             $this->_objectInstance->_id = $value;
         }
         return parent::setKey($value);
 
     /**
      * @param mixed $id
+     *
      * @return \MongoId|mixed
      */
     public static function properId($id) {
             return $id;
         if (is_array($id) && isset($id['$id']))
             return new \MongoId($id['$id']);
-        elseif (is_string($id) && ($mId = new \MongoId($id)) && $id === (string) $mId)
+        elseif (is_string($id) && ($mId = new \MongoId($id)) && $id === (string)$mId)
             return $mId;
         return $id;
     }
      * @return mixed
      */
     protected function getObjectData() {
-        return (array) $this->_objectInstance;
+        return (array)$this->_objectInstance;
     }
 
     /**
     protected function setObjectData($data) {
         $this->_objectInstance->exchangeArray(array_merge((array)$this->_objectInstance, (array)$data));
     }
+
+    /**
+     * Converts result of recurseDiff to mongodb dot notation format, which is safer for updating fields
+     *
+     * @static
+     * @param mixed $arr
+     * @param string $prefix
+     * @param string $action
+     * @param array $base
+     * @return array
+     */
+    public static function dotNotation($arr, $prefix = '', $action = '$set', &$base = array()) {
+        if ((is_array($arr) && !is_int(key($arr))) || (is_object($arr) && !method_exists($arr, '__toString'))) {
+            foreach ((array)$arr as $k => $v) {
+                $p = $prefix;
+                $a = $action;
+                switch ($k) {
+                    case '$set':
+                    case '$addToSet':
+                    case '$unset':
+                    case '$pull':
+                    case '$pullAll':
+                    case '$push':
+                    case '$pushAll':
+                        $a = $k;
+                        break;
+                    case '$each':
+                        /**
+                         * For $each, set the value and skip recursion
+                         */
+                        $base[$a][$p][$k] = $v;
+                        continue 2; /* skips to the next iteration of the foreach */
+                    default:
+                        if ($p !== '')
+                            $p .= '.';
+                        $p .= $k;
+                        break;
+                }
+                self::dotNotation($v, $p, $a, $base);
+            }
+        } else {
+            if (isset($action))
+                $base[$action][$prefix] = $arr;
+            else
+                /**
+                 * Just return basic dot notation in flat array
+                 */
+                $base[$prefix] = $arr;
+        }
+        return $base;
+    }
+
+    public static function arrayObject($var) {
+        if (is_array($var) || (is_object($var) && !method_exists($var, '__toString'))) {
+            $arrObj = function($var){
+                $obj = new \stdClass;
+                foreach ((object) $var as $k=>$v)
+                    $obj->$k = \ext\activedocument\drivers\mongo\Object::arrayObject($v);
+                return $obj;
+            };
+            $var = $arrObj($var);
+        }
+        return $var;
+    }
+
+    /**
+     * Method for comparing arrays to determine what is diff between array1 and following arrays
+     * Method is recursive
+     *
+     * While this method is currently built for the specific needs of Mongo, it could
+     * be abstracted for general array diff purposes
+     *
+     * @todo Optimize!
+     *
+     * @static
+     * @return array
+     */
+    public static function recurseDiff() {
+        $castToArray = function($arr) {
+            return (array)$arr;
+        };
+        $checkExists = function($arr, $value, $key) {
+            if (is_string($key))
+                return array_key_exists($key, $arr);
+            else
+                return in_array($value, $arr);
+        };
+
+        $arrays = func_get_args();
+        $array1 = array_shift($arrays);
+
+        /**
+         * We were using array_diff* methods, but weren't flexible enough
+         */
+        $added = $removed = $remaining = array();
+        array_walk($castToArray($array1), function($v, $k) use(&$added, $arrays, $castToArray, $checkExists) {
+            $itemExists = array_filter(array_map($castToArray, $arrays), function($arr) use($v, $k, $checkExists) {
+                return $checkExists($arr, $v, $k);
+            });
+            /**
+             * If $itemExists is empty, then a new element has been added
+             */
+            if ($itemExists === array())
+                $added[$k] = $v;
+        });
+        array_walk($castToArray(current($arrays)), function($v, $k) use(&$removed, $array1, $castToArray, $checkExists) {
+            if (!$checkExists($castToArray($array1), $v, $k))
+                $removed[$k] = $v;
+        });
+        array_walk($castToArray($array1), function($v, $k) use($added, $removed, &$remaining, $castToArray, $checkExists) {
+            $itemExists = array_filter(array_map($castToArray, array($added, $removed)), function($arr) use($v, $k, $checkExists) {
+                return $checkExists($arr, $v, $k);
+            });
+            if ($itemExists === array())
+                $remaining[$k] = $v;
+        });
+
+        $changed    = array();
+        $keysToDiff = array();
+        array_walk($remaining, function($val, $k) use(&$changed, &$keysToDiff, $arrays) {
+            if ((is_array($val) || (is_object($val) && !method_exists($val, '__toString')))) {
+                $chArr = call_user_func_array(array('\ext\activedocument\drivers\mongo\Object', 'recurseDiff'), array_merge(array($val), array_map(function($v) use($k, $val) {
+                    if (is_array($v) || is_object($v)) {
+                        $v = (array)$v;
+                        if (is_int($k)) {
+                            if (($_k = array_search($val, $v)))
+                                return $v[$_k];
+                        } elseif (array_key_exists($k, $v))
+                            return $v[$k];
+                    }
+                    return array();
+                }, $arrays)));
+                if ($chArr !== array()) {
+                    if (is_int($k)) {
+                        $changed[$k] = $val;
+                    } else {
+                        if (isset($chArr['$addToSet'])) {
+                            if (!isset($changed['$addToSet']))
+                                $changed['$addToSet'] = array();
+                            if (!isset($changed['$addToSet'][$k]))
+                                $changed['$addToSet'][$k] = array();
+                            if (!isset($changed['$addToSet'][$k]['$each']))
+                                $changed['$addToSet'][$k]['$each'] = array();
+                            $changed['$addToSet'][$k]['$each'] += $chArr['$addToSet'];
+                            unset($chArr['$addToSet']);
+                        }
+                        if (isset($chArr['$pullAll'])) {
+                            if (!isset($changed['$pullAll']))
+                                $changed['$pullAll'] = array();
+                            if (!isset($changed['$pullAll'][$k]))
+                                $changed['$pullAll'][$k] = array();
+                            $changed['$pullAll'][$k] += $chArr['$pullAll'];
+                            unset($chArr['$pullAll']);
+                        }
+                        if ($chArr !== array())
+                            $changed[$k] = $chArr;
+                    }
+                }
+            } else
+                $keysToDiff[] = $k;
+        });
+
+        /**
+         * @todo Need to check if this logic works as intended with int keys
+         */
+        if ($keysToDiff !== array()) {
+            $changed = array_merge($changed,
+                call_user_func_array('array_diff_assoc', array_merge(array(array_intersect_key($remaining, array_flip($keysToDiff))), array_map($castToArray, $arrays)))
+            );
+        }
+
+        $return = array();
+        if ($added !== array() || $changed !== array()) {
+            if (isset($changed['$addToSet'])) {
+                $return['$addToSet'] = $changed['$addToSet'];
+                unset($changed['$addToSet']);
+            }
+            if (isset($changed['$pullAll'])) {
+                $return['$pullAll'] = $changed['$pullAll'];
+                unset($changed['$pullAll']);
+            }
+            array_walk(array_merge($added, $changed), function($v, $k) use(&$return) {
+                if (is_int($k))
+                    $return['$addToSet'][$k] = $v;
+                else
+                    $return['$set'][$k] = $v;
+            });
+        }
+        if ($removed !== array()) {
+            array_walk($removed, function($v, $k) use(&$return) {
+                if (is_int($k)) {
+                    $return['$pullAll'][$k] = $v;
+                } else
+                    $return['$unset'][$k] = true;
+            });
+        }
+        return $return;
+    }
+
 }