doctrine / mongodb-odm

The Official PHP MongoDB ORM/ODM
https://www.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/
MIT License
1.09k stars 504 forks source link

How to update a field annotated as ReferenceMany by using setNewObj in findAndUpdate of QueryBuilder #2415

Closed davidemerlitti closed 2 years ago

davidemerlitti commented 2 years ago
Q A
Version 2.3.1

Support Question

I have this class (a semplified version of my real class...):

/**
 * Description of A
 *
 * @ODM\Document(collection="A")
 */
class A {

  /** @ODM\Id(name="_id") */
  private $id;

  /**
   * @ODM\Field(type="collection")
   */
  private $names;

  /**
   * @ODM\ReferenceMany(targetDocument="B", storeAs="id", cascade={"persist"}) 
   */
  private $hasManyB;
}

I want to update the A object identified by $oid with hasManyB property set to an array of two objects of class B identified by $oid1 and $oid2:

$oid = new \MongoDB\BSON\ObjectId('5e8af06ec8a067559836e856');
$oid1 = new \MongoDB\BSON\ObjectId('5e8af06ec8a067559836e855');
$oid2 = new \MongoDB\BSON\ObjectId('5e8af066c8a067559836e854');

$newObj = [
  'id' => $oid,
  'names' => ['alfa','beta','gamma'],
  'hasManyB' => [$oid1, $oid2]
];

$query_builder = $this->_dm->createQueryBuilder(A::class);
$object = $query_builder->findAndUpdate()
      ->field('id')->equals($oid)
      ->setNewObj($newObj)
      ->returnNew()
      ->getQuery()  
      ->execute();

The method prepareQueryOrNewObj() of DocumentPersister calls the prepareQueryElement() for each element in $newObj and the array of two ObjectId is passed to getDatabaseIdentifierValue() that returns a new ObjectId and this is the resulting object inside the mongo db after saving:

{
  "_id" : ObjectId("5e8af06ec8a067559836e856"),
  "hasManyB" : ObjectId("5c3f21b67d97cb3a08e7411f"),
  "names": [
    "alfa",
    "beta",
    "gamma"
  ]
}

To me this makes no sense. Obviously I'm doing something wrong. What is the correct way to update a ReferenceMany property using the setNewObj in findAndUpdate of QueryBuilder? Please note that I'm not interested in alternative methods based on manipulating the php object and saving it using persist() of DocumentManager (I know it works). In mongo shell the correct/equivalent command is:

db.A.findOneAndUpdate({
    _id:ObjectId("5e8af06ec8a067559836e856")
  }, {
    $set:{
      names:["alfa","beta","gamma"],
      hasManyB:[
        ObjectId("5e8af06ec8a067559836e855"),
        ObjectId('5e8af066c8a067559836e854')
      ]
    }
  })
malarzm commented 2 years ago

Have you tried passing actual documents (or just proxies obtained with DocumentManager::getReference) to the refB array? Given ODM tries to prepare values and gets to reading identifiers, it could work. If it does not please try asking on StackOverflow under doctrine-odm tag, you'll have better chances to have your question answered :)

dmerlitti commented 2 years ago

Dear @malarzm

thank you for you suggestion. I tried to pass both documents returned from DocumentManager::find () and proxies returned from DocumentManager::getReference (). I get the same wrong result.

davidemerlitti commented 2 years ago

Hi @malarzm,

the only way to fix the problem was to patch the function DocumentPersister::prepareQueryElement(...) at line 1222 in this way:

            if (! $this->hasQueryOperators($value) || $this->hasDBRefFields($value)) {
                //return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
                if (is_array($value)) {
                    $ids = [];
                    foreach($value as $item) {
                        $ids[] = $targetClass->getDatabaseIdentifierValue($item);                    
                    }
                    return [[$fieldName, $ids]];
                } else {
                  return [[$fieldName, $targetClass->getDatabaseIdentifierValue($value)]];
                }                
            }