Doctrine 2 - ORM
  1. Doctrine 2 - ORM
  2. DDC-2411

Null values get reset when rehydrating an already managed entity

    Details

    • Type: Bug Bug
    • Status: Open
    • Priority: Major Major
    • Resolution: Unresolved
    • Affects Version/s: 2.3
    • Fix Version/s: None
    • Component/s: ORM
    • Security Level: All
    • Labels:

      Description

      Scenario:

      1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

      2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

      3) You set that Book's illustrator to null.

      4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

      5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

      There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

      The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

      If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

      If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

      Code example:

      <?php

      // Create some entities

      $john = new Person();
      $john->setName('John Smith');

      $jane = new Person();
      $jane->setName('Jane Jones');

      $joe = new Person();
      $joe->setName('Joe Bloggs');

      $book = new Book();
      $book->setId(123);
      $book->setTitle('Book Title');
      $book->setIllustrator($john);
      $book->setAuthor($jane);

      $em->persist($john);
      $em->persist($jane);
      $em->persist($joe);
      $em->persist($book);
      $em->flush();

      // Now let's try modifying the book

      $book = $bookRepository->find(123);
      $book->getIllustrator(); // returns Person "John Smith"
      $book->getAuthor(); // returns Person "Jane Jones"

      // make some changes
      $book->setIllustrator(null); // illustrator is now null
      $book->setAuthor($joe); // author is now "Joe Bloggs"

      // now validate our changes with Symfony Validator
      // note: the same effect can also be observed with
      // $test = $bookRepository->findBy('title', 'Book Title');
      $validator->validate($book);

      // what happened to our book??
      $book->getIllustrator(); // returns Person "John Smith" <- should be null
      $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value

        Activity

        Simon Garner created issue -
        Simon Garner made changes -
        Field Original Value New Value
        Description Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/master/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example (Symfony):

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // set something to null
        $book->setIllustrator(null);
        $book->getIllustrator(); // returns null

        // set something to a new value
        $book->setAuthor($joe);
        $book->getAuthor(); // returns Person "Joe Bloggs"

        // now validate our changes with Symfony Validator
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example:

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // set something to null
        $book->setIllustrator(null);
        $book->getIllustrator(); // returns null

        // set something to a new value
        $book->setAuthor($joe);
        $book->getAuthor(); // returns Person "Joe Bloggs"

        // now validate our changes with Symfony Validator
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Labels hydration
        Simon Garner made changes -
        Description Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example:

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // set something to null
        $book->setIllustrator(null);
        $book->getIllustrator(); // returns null

        // set something to a new value
        $book->setAuthor($joe);
        $book->getAuthor(); // returns Person "Joe Bloggs"

        // now validate our changes with Symfony Validator
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example:

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // set something to null
        $book->setIllustrator(null); // illustrator is now null

        // set something to a new value
        $book->setAuthor($joe); // author is now "Joe Bloggs"

        // now validate our changes with Symfony Validator
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Simon Garner made changes -
        Description Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example:

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // set something to null
        $book->setIllustrator(null); // illustrator is now null

        // set something to a new value
        $book->setAuthor($joe); // author is now "Joe Bloggs"

        // now validate our changes with Symfony Validator
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example:

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // make some changes
        $book->setIllustrator(null); // illustrator is now null
        $book->setAuthor($joe); // author is now "Joe Bloggs"

        // now validate our changes with Symfony Validator
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Simon Garner made changes -
        Description Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example:

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // make some changes
        $book->setIllustrator(null); // illustrator is now null
        $book->setAuthor($joe); // author is now "Joe Bloggs"

        // now validate our changes with Symfony Validator
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example:

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // make some changes
        $book->setIllustrator(null); // illustrator is now null
        $book->setAuthor($joe); // author is now "Joe Bloggs"

        // now validate our changes with Symfony Validator
        // note: the same effect can also be observed with
        // $book = $bookRepository->findBy('title', 'Book Title');
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Simon Garner made changes -
        Description Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example:

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // make some changes
        $book->setIllustrator(null); // illustrator is now null
        $book->setAuthor($joe); // author is now "Joe Bloggs"

        // now validate our changes with Symfony Validator
        // note: the same effect can also be observed with
        // $book = $bookRepository->findBy('title', 'Book Title');
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Scenario:

        1) You have an entity with a ManyToOne relation (and probably other kinds too, but this is all I have tested) to another entity which is nullable. For example, let's say you have a Book entity which has an "illustrator" field which refers to a Person entity, representing the person who illustrated the book. If the book is not illustrated then you set the field to null.

        2) You fetch a Book (by ID) which has its illustrator set to a particular Person.

        3) You set that Book's illustrator to null.

        4) Without flushing, you fetch the Book again, using different criteria: for example, by title. Because entities are Identity Mapped, this will run a query but then locate the same instance in memory, and try to hydrate that instance with the old data it just fetched.

        5) Any fields on the instance that have modified values retain their new values (for example, if we changed the illustrator to a different Person, this would be retained), BUT any fields on the instance which are null get overwritten with the old data (so if we previously set the illustrator to null, without flushing, it would now be reset to the Person value that it had before).

        There seems to be a mistaken assumption here that null values are fields that have not been hydrated, when this is not necessarily the case. Is this the intended behaviour?

        The code that causes this behaviour is here: https://github.com/doctrine/doctrine2/blob/e561f47cb2205565eb873f0643637477bfcfc2ff/lib/Doctrine/ORM/Internal/Hydration/ObjectHydrator.php#L471

        If you are wondering why anybody would want to fetch the entity again in step 4, my use case for this is the Symfony Validator (but I presume there could be others).

        If there are any unique constraints (Symfony ones, not Doctrine ones) on the entity, e.g. if we had a unique constraint on the Book title field, then when validating the Book the Symfony Validator would check if there are already any Book entities with the same title as the Book we're validating. It will find the Book that we are working with, and because entities are identity mapped, it will act upon the same instance, and the situation above occurs.

        Code example:

        <?php

        // Create some entities

        $john = new Person();
        $john->setName('John Smith');

        $jane = new Person();
        $jane->setName('Jane Jones');

        $joe = new Person();
        $joe->setName('Joe Bloggs');

        $book = new Book();
        $book->setId(123);
        $book->setTitle('Book Title');
        $book->setIllustrator($john);
        $book->setAuthor($jane);

        $em->persist($john);
        $em->persist($jane);
        $em->persist($joe);
        $em->persist($book);
        $em->flush();

        // Now let's try modifying the book

        $book = $bookRepository->find(123);
        $book->getIllustrator(); // returns Person "John Smith"
        $book->getAuthor(); // returns Person "Jane Jones"

        // make some changes
        $book->setIllustrator(null); // illustrator is now null
        $book->setAuthor($joe); // author is now "Joe Bloggs"

        // now validate our changes with Symfony Validator
        // note: the same effect can also be observed with
        // $test = $bookRepository->findBy('title', 'Book Title');
        $validator->validate($book);

        // what happened to our book??
        $book->getIllustrator(); // returns Person "John Smith" <- should be null
        $book->getAuthor(); // returns Person "Joe Bloggs" <- correctly retains the new value
        Hide
        Fabio B. Silva added a comment -

        Hi Simon,

        Could you please try to write a failing test case or paste your entities ?

        Cheers

        Show
        Fabio B. Silva added a comment - Hi Simon, Could you please try to write a failing test case or paste your entities ? Cheers
        Hide
        Benjamin Eberlei added a comment -

        Verified by code review that this issue exists, but it will be very tricky to fix, because the null check is there for other reasons as well.

        Show
        Benjamin Eberlei added a comment - Verified by code review that this issue exists, but it will be very tricky to fix, because the null check is there for other reasons as well.

        This list may be incomplete, as errors occurred whilst retrieving source from linked applications:

        • Request to http://www.doctrine-project.org/fisheye/ failed: Error in remote call to 'FishEye 0 (http://www.doctrine-project.org/fisheye/)' (http://www.doctrine-project.org/fisheye) [AbstractRestCommand{path='/rest-service-fe/search-v1/crossRepositoryQuery', params={query=DDC-2411, expand=changesets[0:20].revisions[0:29],reviews}, methodType=GET}] : Received status code 503 (Service Temporarily Unavailable)

          People

          • Assignee:
            Benjamin Eberlei
            Reporter:
            Simon Garner
          • Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

            Dates

            • Created:
              Updated: