Composite Key Proxy Entity does not load (Potential Data Loss) #6473

Closed
opened 2026-01-22 15:33:50 +01:00 by admin · 0 comments
Owner

Originally created by @fyrye on GitHub (May 22, 2020).

Bug Report

Q A
BC Break no
Version 2.7.2
OS CentOS 7.7.1908 x64
RDBMS MySQL 8.0.17 x64

Summary

Updates

  1. Reduced complexity as PersistentCollection was not related to the issue
  2. Added note on changing the JoinColumns

When specifying a composite key entity, the proxy object is never loaded when the entity is retrieved directly from the entity manager prior to outputting the proxy value.

Current behavior

The proxy object is not loaded, resulting in an empty value and a proxy entity object with all null properties.

Output
Note: createdBy for the foo_bar_a_b entry is empty

{
    "bars": [
        {
            "body": "foo_bar_a_a",
            "createdBy": "A"
        },
        {
            "body": "foo_bar_a_b",
            "createdBy": ""
        }
    ]
}

Proxy Entity Object

object(DoctrineProxies\__CG__\Entity\FooUser)#123 (6) {
  ["__initializer__"]=>
  NULL
  ["__cloner__"]=>
  NULL
  ["__isInitialized__"]=>
  bool(true)
  ["id":"Entity\FooUser":private]=>
  NULL
  ["ref":"Entity\FooUser":private]=>
  NULL
  ["name":"Entity\FooUser":private]=>
  string(0) ""
}

How to reproduce

Entity Mappings

/* /Entity/FooUser.php */
namespace Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 * @ORM\Table(name="foo_user", uniqueConstraints={
 *      @ORM\UniqueConstraint(name="foo_user_id_ref", columns={"id", "ref"})
 * })
 */
class FooUser
{

    /**
     * @var int
     * @ORM\Id()
     * @ORM\Column(name="id", type="integer", nullable=false)
     */
    private $id;

    /**
     * @var string
     * @ORM\Id()
     * @ORM\Column(name="ref", type="string", length=50, nullable=false)
     */
    private $ref;

    /**
     * @var string
     * @ORM\Column(name="name", type="string")
     */
    private $name = '';

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getRef(): string
    {
        return $this->ref;
    }

    /**
     * @return string
     */
    public function getName(): string
    {
        return $this->name;
    }

}
/* /Entity/FooBar.php */

namespace Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity()
 * @ORM\Table(name="foo_bar")
 */
class FooBar
{

    /**
     * @var int
     * @ORM\Id()
     * @ORM\GeneratedValue(strategy="IDENTITY")
     * @ORM\Column(name="id", type="integer", nullable=true, unique=true)
     */
    private $id;

    /**
     * @var string
     * @ORM\Column(name="body", type="string")
     */
    private $body;

    /**
     * @var \Entity\FooUser
     * @ORM\ManyToOne(targetEntity="Entity\FooUser")
     * @ORM\JoinColumns({
     *     @ORM\JoinColumn(name="created_by_ref", referencedColumnName="ref"),
     *     @ORM\JoinColumn(name="created_by", referencedColumnName="id")
     * })
     */
    private $createdBy;

    /**
     * @return int
     */
    public function getId(): int
    {
        return $this->id;
    }

    /**
     * @return string
     */
    public function getBody(): string
    {
        return $this->body;
    }

    /**
     * @return \Entity\FooUser
     */
    public function getCreatedBy(): FooUser
    {
        return $this->createdBy;
    }

}

Dataset

INSERT INTO `foo_user` (`id`, `ref`, `name`) VALUES
	(1, 'user_id', 'A'),
	(2, 'user_id', 'B');

INSERT INTO `foo_bar` (`id`, `created_by`, `created_by_ref`, `body`) VALUES
	(1, 1, 'user_id', 'foo_bar_a_a'),
	(2, 2, 'user_id', 'foo_bar_a_b');

Code Example

use Doctrine\ORM\Tools\Setup;
use Doctrine\ORM\EntityManager;

$paths = [__DIR__ . '/Entity'];
$dbParams = [
    'driver' => 'pdo_mysql',
    'user' => 'root',
    'password' => '',
    'dbname' => 'foo',
];
$config = Setup::createAnnotationMetadataConfiguration($paths, true, __DIR__ . '/tmp', null, false);
$em = EntityManager::create($dbParams, $config);

//issue no longer persists after removing initial retrieval
$user = $em->find(\Entity\FooUser::class, ['id' => 2, 'ref' => 'user_id']);

$bars = $em->getRepository(\Entity\FooBar::class)->findAll();
$data = [
    'bars' => [],
];
/** @var \Entity\FooBar $bar */
foreach ($bars as $bar) {
    $data['bars'][] = [
        'body' => $bar->getBody(),
        'createdBy' => $bar->getCreatedBy()->getName(),
    ];

    //dumping after initially called, results in a proxy entity object with all null properties
    var_dump($bar->getCreatedBy());
}

echo json_encode($data, \JSON_PRETTY_PRINT);

Expected behavior

For the proxy object to be retrieved from the initially retrieved entity, with the following output

Output

{
    "bars": [
        {
            "body": "foo_bar_a_a",
            "createdBy": "A"
        },
        {
            "body": "foo_bar_a_b",
            "createdBy": "B"
        }
    ]
}

Proxy Entity Object

object(Entity\FooUser)#117 (3) {
  ["id":"Entity\FooUser":private]=>
  int(2)
  ["ref":"Entity\FooUser":private]=>
  string(7) "user_id"
  ["name":"Entity\FooUser":private]=>
  string(1) "B"
}

Notes

This is the simplest approach to reproduce the issue I could come up with. However the Symfony environment, where I discovered the issue, does not allow for easily resolving the issue.

Tried retrieving the initial FooUser using $em->getReference() and $em->getRepository()->findOneBy() with the same results.

Also, the issue no longer persists when enabling EAGER loading, but due to the amount of records that can be associated in some of the displays, EAGER loading is not desired.

When switching the JoinColumns positions, the issue appears to be resolved.

    /**
     * @var \Entity\FooUser
     * @ORM\ManyToOne(targetEntity="Entity\FooUser")
     * @ORM\JoinColumns({
     *     @ORM\JoinColumn(name="created_by", referencedColumnName="id"),
     *     @ORM\JoinColumn(name="created_by_ref", referencedColumnName="ref")
     * })
     */
    private $createdBy;
Originally created by @fyrye on GitHub (May 22, 2020). ### Bug Report <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | BC Break | no | Version | 2.7.2 | OS | CentOS 7.7.1908 x64 | RDBMS | MySQL 8.0.17 x64 #### Summary **Updates** 1. _Reduced complexity as `PersistentCollection` was not related to the issue_ 2. _Added note on changing the JoinColumns_ When specifying a composite key entity, the proxy object is never loaded when the entity is retrieved directly from the entity manager prior to outputting the proxy value. #### Current behavior The proxy object is not loaded, resulting in an empty value and a proxy entity object with all `null` properties. **Output** _Note: `createdBy` for the `foo_bar_a_b` entry is empty_ ```json { "bars": [ { "body": "foo_bar_a_a", "createdBy": "A" }, { "body": "foo_bar_a_b", "createdBy": "" } ] } ``` **Proxy Entity Object** ``` object(DoctrineProxies\__CG__\Entity\FooUser)#123 (6) { ["__initializer__"]=> NULL ["__cloner__"]=> NULL ["__isInitialized__"]=> bool(true) ["id":"Entity\FooUser":private]=> NULL ["ref":"Entity\FooUser":private]=> NULL ["name":"Entity\FooUser":private]=> string(0) "" } ``` #### How to reproduce Entity Mappings ------------ ```php /* /Entity/FooUser.php */ namespace Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() * @ORM\Table(name="foo_user", uniqueConstraints={ * @ORM\UniqueConstraint(name="foo_user_id_ref", columns={"id", "ref"}) * }) */ class FooUser { /** * @var int * @ORM\Id() * @ORM\Column(name="id", type="integer", nullable=false) */ private $id; /** * @var string * @ORM\Id() * @ORM\Column(name="ref", type="string", length=50, nullable=false) */ private $ref; /** * @var string * @ORM\Column(name="name", type="string") */ private $name = ''; /** * @return int */ public function getId(): int { return $this->id; } /** * @return string */ public function getRef(): string { return $this->ref; } /** * @return string */ public function getName(): string { return $this->name; } } ``` ```php /* /Entity/FooBar.php */ namespace Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() * @ORM\Table(name="foo_bar") */ class FooBar { /** * @var int * @ORM\Id() * @ORM\GeneratedValue(strategy="IDENTITY") * @ORM\Column(name="id", type="integer", nullable=true, unique=true) */ private $id; /** * @var string * @ORM\Column(name="body", type="string") */ private $body; /** * @var \Entity\FooUser * @ORM\ManyToOne(targetEntity="Entity\FooUser") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="created_by_ref", referencedColumnName="ref"), * @ORM\JoinColumn(name="created_by", referencedColumnName="id") * }) */ private $createdBy; /** * @return int */ public function getId(): int { return $this->id; } /** * @return string */ public function getBody(): string { return $this->body; } /** * @return \Entity\FooUser */ public function getCreatedBy(): FooUser { return $this->createdBy; } } ``` Dataset --------- ```sql INSERT INTO `foo_user` (`id`, `ref`, `name`) VALUES (1, 'user_id', 'A'), (2, 'user_id', 'B'); INSERT INTO `foo_bar` (`id`, `created_by`, `created_by_ref`, `body`) VALUES (1, 1, 'user_id', 'foo_bar_a_a'), (2, 2, 'user_id', 'foo_bar_a_b'); ``` Code Example ---------- ```php use Doctrine\ORM\Tools\Setup; use Doctrine\ORM\EntityManager; $paths = [__DIR__ . '/Entity']; $dbParams = [ 'driver' => 'pdo_mysql', 'user' => 'root', 'password' => '', 'dbname' => 'foo', ]; $config = Setup::createAnnotationMetadataConfiguration($paths, true, __DIR__ . '/tmp', null, false); $em = EntityManager::create($dbParams, $config); //issue no longer persists after removing initial retrieval $user = $em->find(\Entity\FooUser::class, ['id' => 2, 'ref' => 'user_id']); $bars = $em->getRepository(\Entity\FooBar::class)->findAll(); $data = [ 'bars' => [], ]; /** @var \Entity\FooBar $bar */ foreach ($bars as $bar) { $data['bars'][] = [ 'body' => $bar->getBody(), 'createdBy' => $bar->getCreatedBy()->getName(), ]; //dumping after initially called, results in a proxy entity object with all null properties var_dump($bar->getCreatedBy()); } echo json_encode($data, \JSON_PRETTY_PRINT); ``` #### Expected behavior For the proxy object to be retrieved from the initially retrieved entity, with the following output **Output** ```json { "bars": [ { "body": "foo_bar_a_a", "createdBy": "A" }, { "body": "foo_bar_a_b", "createdBy": "B" } ] } ``` **Proxy Entity Object** ``` object(Entity\FooUser)#117 (3) { ["id":"Entity\FooUser":private]=> int(2) ["ref":"Entity\FooUser":private]=> string(7) "user_id" ["name":"Entity\FooUser":private]=> string(1) "B" } ``` #### Notes This is the simplest approach to reproduce the issue I could come up with. <strike>However the Symfony environment, where I discovered the issue, does not allow for easily resolving the issue.</strike> Tried retrieving the initial `FooUser` using `$em->getReference()` and `$em->getRepository()->findOneBy()` with the same results. Also, the issue no longer persists when enabling `EAGER` loading, but due to the amount of records that can be associated in some of the displays, `EAGER` loading is not desired. When switching the `JoinColumns` positions, the issue appears to be resolved. ```php /** * @var \Entity\FooUser * @ORM\ManyToOne(targetEntity="Entity\FooUser") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="created_by", referencedColumnName="id"), * @ORM\JoinColumn(name="created_by_ref", referencedColumnName="ref") * }) */ private $createdBy; ```
admin closed this issue 2026-01-22 15:33:50 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6473