Collection is empty for one-to-many association using class table inheritance #6989

Open
opened 2026-01-22 15:42:42 +01:00 by admin · 7 comments
Owner

Originally created by @NordFox7 on GitHub (Jun 7, 2022).

Bug Report

Q A
BC Break no
Version 2.12.2

Summary

The Collection of a One-To-Many Association is empty when using a junction entity having Class Table Inheritance to connect 2 entities.

I have 2 entities (Field & Mapping) which I want to "connect" using junction entities which inherit of a Assignment entity which uses a composite key to connect them both.
Something like this:

  • FieldA <-> Assignment_FieldA <-> MappingA
  • FieldA <-> Assignment_FieldB <-> MappingA
  • FieldA <-> Assignment_FieldC <-> MappingA

I'm using a junction entity because I want to store additional data in the assignment/connection.

Also, I want to retrieve all the mappings a field is assigned/connected to, and also in the other direction retrieve all the fields a mapping has connected/assigned.
Therefore, I use a One-To-Many Association inside the Field & Mapping entity which should return a list of Assignment* entities but when calling, the Collection is empty.

I checked all association's and the stored database rows, also the fetching query, and executed them manually and all are correct.

How to reproduce

#[Entity, Table("Fields")]
class Field {

	#[Id]
	#[Column, GeneratedValue]
	public int $ID;

	#[OneToMany(targetEntity: Assignment::class, mappedBy: "Field", cascade: ["persist"], orphanRemoval: true)]
	public Collection $AssignedMappings;

	public function __construct() {
		$this->AssignedMappings = new ArrayCollection();
	}

}

#[Entity, Table("Mappings")]
class Mapping {

	#[Id]
	#[Column, GeneratedValue]
	public int $ID;

	#[OneToMany(targetEntity: Assignment::class, mappedBy: "Mapping", cascade: ["persist"], orphanRemoval: true)]
	public Collection $AssignedFields;

	public function __construct() {
		$this->AssignedFields = new ArrayCollection();
	}

}

#[Entity, Table("Assignments")]
#[
	InheritanceType("JOINED"),
	DiscriminatorColumn("Type"),
	DiscriminatorMap([
		"A" => Assignment_FieldA::class,
		"B" => Assignment_FieldB::class,
	])
]
abstract class Assignment {

	#[Id]
	#[ManyToOne(inversedBy: "AssignedFields")]
	public Mapping $Mapping;

	#[Id]
	#[ManyToOne(inversedBy: "AssignedMappings")]
	public Field $Field;

	public function __construct(Mapping $mapping, Field $field) {
		$this->Mapping = $mapping;
		$this->Field = $field;
	}

}

#[Entity, Table("Assignments_FieldA")]
class Assignment_FieldA extends Assignment {}

#[Entity, Table("Assignments_FieldB")]
class Assignment_FieldB extends Assignment {}

// PHPUnit Test
public function testAssignments() {
	// Create
	$fieldA = new Field();
	$fieldB = new Field();
	$mapping = new Mapping();

	$this->em->persist($fieldA);
	$this->em->persist($fieldB);
	$this->em->persist($mapping);
	$this->em->flush();

	$mapping->AssignedFields->add(new Assignment_FieldA($mapping, $fieldA));
	$mapping->AssignedFields->add(new Assignment_FieldB($mapping, $fieldB));

	$this->em->persist($mapping);
	$this->em->flush();
	$this->em->clear();

	// Fetch
	$fieldA = $this->em->find(Field::class, 1);
	$fieldB = $this->em->find(Field::class, 2);
	$mapping = $this->em->find(Mapping::class, 1);

	// assertCount will internally use `count()` to retrieve the count of the `Collection` which will also initialize the `Collection`.
	$this->assertCount(1, $fieldA->AssignedMappings);
	$this->assertCount(1, $fieldB->AssignedMappings);
	$this->assertCount(2, $mapping->AssignedFields);
}

My investigation and possible bug discovery/solution

I tried to investigate the problem myself and I noticed the query is correct, and the row data is received in the ObjectHydrator but the identifier is not detected, and the row is therefore discarded.

I noticed in ObjectHydrator.php:467 that the row got discared because the $nonemptyComponents was empty.

I also noticed when the Assignment is a single entity (without inheritance) everything works as expected and $nonemptyComponents was not empty and also the Collection had all entities correctly.

Therefore, I checked how the $nonemptyComponents gets filled, which happens in AbstractHydrator.php:477 if the identifier for the row data is found and is not null. For the single entity that worked, but not for my inherited Assignment* entity.

Therefore, I checked how the identifier is detected and this happens in the select column query-building part where the column is checked if it is an id, which is different for single and inherited entities.

The single entity will use the BasicEntityPersister and at line 1348 if the column is an id, the ResultSetMapping will get informed by that, which will result in the $nonemptyComponents not being empty, and therefore the Collection not empty.

The inherited entity will use the JoinedSubclassPersister which inherits the AbstractEntityInheritancePersister class and at line 77 the column will be selected but the isIdentifier parameter which informs the ResultSetMapping for an found id column is hardcoded to false, which will result in an always empty $nonemptyComponents and therefore an always empty Collection.
I found that very strange, why is that hardcoded to false?

I confirmed that by adding the same id detection code for the JoinedSubclassPersister as it is in the BaseEntityPersister and the Collection was indeed correctly filled with desired entities.
But this is no permanent solution because I modified the vendor files.
I almost wanted to do an pull request with this fix, but because I don't fully understand the inner workings of doctrine, I was not sure if that is correct what I did. Because there must be a reason it is hardcoded to false right? Maybe the id column is detected somewhere else for joined rows?

Originally created by @NordFox7 on GitHub (Jun 7, 2022). ### Bug Report <!-- Fill in the relevant information below to help triage your issue. --> | Q | A |------------ | ------ | BC Break | no | Version | 2.12.2 #### Summary The `Collection` of a `One-To-Many Association` is empty when using a junction entity having `Class Table Inheritance` to connect 2 entities. I have 2 entities (`Field` & `Mapping`) which I want to "connect" using junction entities which inherit of a `Assignment` entity which uses a composite key to connect them both. Something like this: - FieldA <-> Assignment_FieldA <-> MappingA - FieldA <-> Assignment_FieldB <-> MappingA - FieldA <-> Assignment_FieldC <-> MappingA I'm using a junction entity because I want to store additional data in the assignment/connection. Also, I want to retrieve all the mappings a field is assigned/connected to, and also in the other direction retrieve all the fields a mapping has connected/assigned. Therefore, I use a `One-To-Many Association` inside the `Field` & `Mapping` entity which should return a list of `Assignment*` entities but when calling, the `Collection` is empty. I checked all association's and the stored database rows, also the fetching query, and executed them manually and all are correct. #### How to reproduce ``` PHP #[Entity, Table("Fields")] class Field { #[Id] #[Column, GeneratedValue] public int $ID; #[OneToMany(targetEntity: Assignment::class, mappedBy: "Field", cascade: ["persist"], orphanRemoval: true)] public Collection $AssignedMappings; public function __construct() { $this->AssignedMappings = new ArrayCollection(); } } #[Entity, Table("Mappings")] class Mapping { #[Id] #[Column, GeneratedValue] public int $ID; #[OneToMany(targetEntity: Assignment::class, mappedBy: "Mapping", cascade: ["persist"], orphanRemoval: true)] public Collection $AssignedFields; public function __construct() { $this->AssignedFields = new ArrayCollection(); } } #[Entity, Table("Assignments")] #[ InheritanceType("JOINED"), DiscriminatorColumn("Type"), DiscriminatorMap([ "A" => Assignment_FieldA::class, "B" => Assignment_FieldB::class, ]) ] abstract class Assignment { #[Id] #[ManyToOne(inversedBy: "AssignedFields")] public Mapping $Mapping; #[Id] #[ManyToOne(inversedBy: "AssignedMappings")] public Field $Field; public function __construct(Mapping $mapping, Field $field) { $this->Mapping = $mapping; $this->Field = $field; } } #[Entity, Table("Assignments_FieldA")] class Assignment_FieldA extends Assignment {} #[Entity, Table("Assignments_FieldB")] class Assignment_FieldB extends Assignment {} // PHPUnit Test public function testAssignments() { // Create $fieldA = new Field(); $fieldB = new Field(); $mapping = new Mapping(); $this->em->persist($fieldA); $this->em->persist($fieldB); $this->em->persist($mapping); $this->em->flush(); $mapping->AssignedFields->add(new Assignment_FieldA($mapping, $fieldA)); $mapping->AssignedFields->add(new Assignment_FieldB($mapping, $fieldB)); $this->em->persist($mapping); $this->em->flush(); $this->em->clear(); // Fetch $fieldA = $this->em->find(Field::class, 1); $fieldB = $this->em->find(Field::class, 2); $mapping = $this->em->find(Mapping::class, 1); // assertCount will internally use `count()` to retrieve the count of the `Collection` which will also initialize the `Collection`. $this->assertCount(1, $fieldA->AssignedMappings); $this->assertCount(1, $fieldB->AssignedMappings); $this->assertCount(2, $mapping->AssignedFields); } ``` #### My investigation and possible bug discovery/solution I tried to investigate the problem myself and I noticed the query is correct, and the row data is received in the `ObjectHydrator` but the identifier is not detected, and the row is therefore discarded. I noticed in `ObjectHydrator.php:467` that the row got discared because the `$nonemptyComponents` was empty. I also noticed when the `Assignment` is a single entity (without inheritance) everything works as expected and `$nonemptyComponents` was not empty and also the `Collection` had all entities correctly. Therefore, I checked how the `$nonemptyComponents` gets filled, which happens in `AbstractHydrator.php:477` if the identifier for the row data is found and is not `null`. For the single entity that worked, but not for my inherited `Assignment*` entity. Therefore, I checked how the identifier is detected and this happens in the select column query-building part where the column is checked if it is an id, which is different for single and inherited entities. The single entity will use the `BasicEntityPersister` and at line `1348` if the column is an id, the `ResultSetMapping` will get informed by that, which will result in the `$nonemptyComponents` not being empty, and therefore the `Collection` not empty. The inherited entity will use the `JoinedSubclassPersister` which inherits the `AbstractEntityInheritancePersister` class and at line `77` the column will be selected but the `isIdentifier` parameter which informs the `ResultSetMapping` for an found id column is hardcoded to `false`, which will result in an always empty `$nonemptyComponents` and therefore an always empty `Collection`. I found that very strange, why is that hardcoded to `false`? I confirmed that by adding the same id detection code for the `JoinedSubclassPersister` as it is in the `BaseEntityPersister` and the `Collection` was indeed correctly filled with desired entities. But this is no permanent solution because I modified the vendor files. I almost wanted to do an pull request with this fix, but because I don't fully understand the inner workings of doctrine, I was not sure if that is correct what I did. Because there must be a reason it is hardcoded to `false` right? Maybe the id column is detected somewhere else for joined rows?
Author
Owner

@oojacoboo commented on GitHub (Oct 27, 2022):

Having a very similar issue with a OneToMany relationship where the targetEntity is using inheritance mapping. The Collection is just empty. When looking at the query logs, I don't actually see any attempts to even query the database. It's as if Doctrine considers the Collection to be initialized already. The fetch mode has no bearing on it and I tried even specifying one of the sub entities in the discriminator map as the target entity for the relationship. That doesn't work either.

@FoxLess can you share your modification code for this?

@oojacoboo commented on GitHub (Oct 27, 2022): Having a very similar issue with a `OneToMany` relationship where the `targetEntity` is using inheritance mapping. The Collection is just empty. When looking at the query logs, I don't actually see any attempts to even query the database. It's as if Doctrine considers the Collection to be initialized already. The `fetch` mode has no bearing on it and I tried even specifying one of the sub entities in the discriminator map as the `target` entity for the relationship. That doesn't work either. @FoxLess can you share your modification code for this?
Author
Owner

@NordFox7 commented on GitHub (Oct 28, 2022):

I actually made a bugfix which I wanted to publish as a pull request... but as I stated in my post I was not sure if my change is correct... Also it would probably take a while until it is merged into the main branch and I needed a faster fix. (Also it seemed that nobody else had any issue with it)

For my case, I just changed my code from a Composite identifier to a Single identifier by changing the code from:

...
abstract class Assignment {

	#[Id]
	#[ManyToOne(inversedBy: "AssignedFields")]
	public Mapping $Mapping;

	#[Id]
	#[ManyToOne(inversedBy: "AssignedMappings")]
	public Field $Field;

	...

}

to this:

...
abstract class Assignment {

	#[Id]
	#[Column(type: "integer", options: ["unsigned" => true]), GeneratedValue]
	public int $ID;

	#[ManyToOne(inversedBy: "AssignedFields")]
	public Mapping $Mapping;

	#[ManyToOne(inversedBy: "AssignedMappings")]
	public Field $Field;

	...

}

Which may or may not an ideal case for you. @oojacoboo

The underlaying issue is that doctrine does not detect a composite key as an identifier and does not load the collection then.

I may actually do that pull request in case somebody else is stumbling on that issue.

@NordFox7 commented on GitHub (Oct 28, 2022): I actually made a bugfix which I wanted to publish as a pull request... but as I stated in my post I was not sure if my change is correct... Also it would probably take a while until it is merged into the main branch and I needed a faster fix. (Also it seemed that nobody else had any issue with it) For my case, I just changed my code from a Composite identifier to a Single identifier by changing the code from: ``` ... abstract class Assignment { #[Id] #[ManyToOne(inversedBy: "AssignedFields")] public Mapping $Mapping; #[Id] #[ManyToOne(inversedBy: "AssignedMappings")] public Field $Field; ... } ``` to this: ``` ... abstract class Assignment { #[Id] #[Column(type: "integer", options: ["unsigned" => true]), GeneratedValue] public int $ID; #[ManyToOne(inversedBy: "AssignedFields")] public Mapping $Mapping; #[ManyToOne(inversedBy: "AssignedMappings")] public Field $Field; ... } ``` Which may or may not an ideal case for you. @oojacoboo The underlaying issue is that doctrine does not detect a composite key as an identifier and does not load the collection then. I may actually do that pull request in case somebody else is stumbling on that issue.
Author
Owner

@oojacoboo commented on GitHub (Oct 28, 2022):

I see @FoxLess. So, it looks like your issue is related to composite identifiers. In our case, it's strictly related to class table inheritance.

@oojacoboo commented on GitHub (Oct 28, 2022): I see @FoxLess. So, it looks like your issue is related to composite identifiers. In our case, it's strictly related to class table inheritance.
Author
Owner

@NordFox7 commented on GitHub (Oct 28, 2022):

Well yes, my issue occurs with class table inheritance using composite identifiers because they are not detected in this combination.

I made a pull request, but it seems to have breaking changes, so don't expect it to be in doctrine for a while. But you can check the code for the changes I made, in case you use a fork of doctrine. @oojacoboo

@NordFox7 commented on GitHub (Oct 28, 2022): Well yes, my issue occurs with class table inheritance using composite identifiers because they are not detected in this combination. I made a pull request, but it seems to have breaking changes, so don't expect it to be in doctrine for a while. But you can check the code for the changes I made, in case you use a fork of doctrine. @oojacoboo
Author
Owner

@oojacoboo commented on GitHub (Oct 29, 2022):

@FoxLess thanks. I was actually able to test this. It doesn't resolve the issue I'm having. But thanks for sharing. Your PR seems reasonable to me and hopefully it can be included in the next larger release.

@oojacoboo commented on GitHub (Oct 29, 2022): @FoxLess thanks. I was actually able to test this. It doesn't resolve the issue I'm having. But thanks for sharing. Your PR seems reasonable to me and hopefully it can be included in the next larger release.
Author
Owner

@TracKer commented on GitHub (Mar 29, 2023):

@oojacoboo Did you find any working solution for the issue?

@TracKer commented on GitHub (Mar 29, 2023): @oojacoboo Did you find any working solution for the issue?
Author
Owner

@oojacoboo commented on GitHub (Mar 29, 2023):

@TracKer I don't even recall. We don't currently have an outstanding issue, so we either resolved it or found a work-around. Not sure if you're using any SQL filters with your entities, but Doctrine does a terrible job with them in respect to relational entities and query caching. You're going to need to log the actual SQL statements issued from Doctrine to really get to the bottom of things. We just enable query logging at the rdbms level to see what SQL statements Doctrine is cooking up.

@oojacoboo commented on GitHub (Mar 29, 2023): @TracKer I don't even recall. We don't currently have an outstanding issue, so we either resolved it or found a work-around. Not sure if you're using any SQL filters with your entities, but Doctrine does a terrible job with them in respect to relational entities and query caching. You're going to need to log the actual SQL statements issued from Doctrine to really get to the bottom of things. We just enable query logging at the rdbms level to see what SQL statements Doctrine is cooking up.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6989