ManyToMany IndexBy not working on multiple fields after upgrade to symfony 4.3 #6400

Open
opened 2026-01-22 15:32:32 +01:00 by admin · 5 comments
Owner

Originally created by @estebanolm on GitHub (Feb 13, 2020).

Bug Report

ManyToMany IndexBy not working on multiple fields after upgrade to symfony 4.3
Previously ther was no problem

Versions:
doctrine/annotations v1.8.0
doctrine/cache 1.10.0
doctrine/collections 1.6.4
doctrine/common 2.12.0
doctrine/dbal v2.10.1
doctrine/doctrine-bundle 2.0.7
doctrine/doctrine-migrations-bundle 2.1.2
doctrine/event-manager 1.1.0
doctrine/inflector 1.3.1
doctrine/instantiator 1.3.0
doctrine/lexer 1.2.0
doctrine/migrations 2.2.1
doctrine/orm v2.7.0
doctrine/persistence 1.3.6
doctrine/reflection v1.1.0

Summary

I have a many-to-many relation roles <-> user_roles <--> users

tables fields names (LOOK OUT THEY ARE DIFERENT):

[id_role] <---> [role_id, user_id] <--> [id]

with COMPOSITE PRIMARY index [user_id, role_id] (primary to be UNIQUE key), but it fails after upgrade

You can see more here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/tutorials/working-with-indexed-associations.html

Reason to do that is:

When calling Collection::containsKey($key) on one-to-many and many-to-many collections using indexBy and EXTRA_LAZY a query is now executed to check for the existance for the item. Prevoiusly this operation was performed in memory by loading all entities of the collection.
From https://doctrine2.readthedocs.io/en/latest/changelog/migration_2_5.html

Current behavior

Error:

request.CRITICAL: Uncaught PHP Exception Doctrine\ORM\Mapping\MappingException: "No mapping found for field 'role_id, user_id' on class 'App\Entity\.....\Role'." at /....../vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/MappingException.php line 163 {"exception":"[object] (Doctrine\\ORM\\Mapping\\MappingException(code: 0): No mapping found for field 'role_id, user_id' on class 'App\\Entity\\....\\Role'. at /...../vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/MappingException.php:163)"} []

ClassMetadtaInfo.getAssociationMapping() try to find $this->associationMappings['role_id, user_id']
as it thins "'role_id, user_id'" is one fieldname

How to reproduce

CLASS ROLE

	/**
	 * @ORM\ManyToMany(targetEntity="App\Entity\.....\User", mappedBy="id_role")
	 */
	protected $users;


CLASS USER
    /**
     * //indexBy="role_id, user_id", 
     * 
     * @ORM\ManyToMany(targetEntity="App\Entity\....\Role", **indexBy="role_id, user_id",** fetch="EXTRA_LAZY", inversedBy="users", cascade={"persist"})
     * @ORM\JoinTable(name="sec_user_roles", 
     *     joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)},
     *     inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id_role", nullable=false)}
     */
    protected $roles;

if you remove indexBy="role_id, user_id", IT WORKS.
Before update, here was no problem.

i tried:

indexBy="user_id, role_id"
indexBy="user_id"

none worked

Expected behavior

Not to fail getting user entity

Originally created by @estebanolm on GitHub (Feb 13, 2020). ### Bug Report ManyToMany IndexBy not working on multiple fields after upgrade to symfony 4.3 Previously ther was no problem Versions: doctrine/annotations v1.8.0 doctrine/cache 1.10.0 doctrine/collections 1.6.4 doctrine/common 2.12.0 doctrine/dbal v2.10.1 doctrine/doctrine-bundle 2.0.7 doctrine/doctrine-migrations-bundle 2.1.2 doctrine/event-manager 1.1.0 doctrine/inflector 1.3.1 doctrine/instantiator 1.3.0 doctrine/lexer 1.2.0 doctrine/migrations 2.2.1 doctrine/orm v2.7.0 doctrine/persistence 1.3.6 doctrine/reflection v1.1.0 #### Summary I have a many-to-many relation roles <-> user_roles <--> users tables fields names (LOOK OUT THEY ARE DIFERENT): [id_role] <---> [role_id, user_id] <--> [id] with COMPOSITE PRIMARY index [user_id, role_id] (primary to be UNIQUE key), but it fails after upgrade You can see more here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/tutorials/working-with-indexed-associations.html Reason to do that is: _When calling Collection::containsKey($key) on one-to-many and many-to-many collections using indexBy and EXTRA_LAZY a query is now executed to check for the existance for the item. Prevoiusly this operation was performed in memory by loading all entities of the collection._ From https://doctrine2.readthedocs.io/en/latest/changelog/migration_2_5.html #### Current behavior Error: ``` request.CRITICAL: Uncaught PHP Exception Doctrine\ORM\Mapping\MappingException: "No mapping found for field 'role_id, user_id' on class 'App\Entity\.....\Role'." at /....../vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/MappingException.php line 163 {"exception":"[object] (Doctrine\\ORM\\Mapping\\MappingException(code: 0): No mapping found for field 'role_id, user_id' on class 'App\\Entity\\....\\Role'. at /...../vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/MappingException.php:163)"} [] ``` ClassMetadtaInfo.getAssociationMapping() try to find $this->associationMappings['role_id, user_id'] as it thins "'role_id, user_id'" is one fieldname #### How to reproduce ``` php CLASS ROLE /** * @ORM\ManyToMany(targetEntity="App\Entity\.....\User", mappedBy="id_role") */ protected $users; CLASS USER /** * //indexBy="role_id, user_id", * * @ORM\ManyToMany(targetEntity="App\Entity\....\Role", **indexBy="role_id, user_id",** fetch="EXTRA_LAZY", inversedBy="users", cascade={"persist"}) * @ORM\JoinTable(name="sec_user_roles", * joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)}, * inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id_role", nullable=false)} */ protected $roles; ``` if you remove **indexBy="role_id, user_id",** IT WORKS. Before update, here was no problem. i tried: ``` php indexBy="user_id, role_id" indexBy="user_id" ``` none worked #### Expected behavior Not to fail getting user entity
Author
Owner

@SenseException commented on GitHub (Feb 13, 2020):

Please use a propper markdown format for your code examples to make it easier to read for those who join to help you.

I took a short look over your code examples and mappedBy should point to a property, not a table column. I assume it's $roles.

indexBy or INDEX BY isn't something SQL related and AFAIK only supports one property.

@SenseException commented on GitHub (Feb 13, 2020): Please use a propper markdown format for your code examples to make it easier to read for those who join to help you. I took a short look over your code examples and `mappedBy` should point to a property, not a table column. I assume it's `$roles`. `indexBy` or [INDEX BY](https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/dql-doctrine-query-language.html#using-index-by) isn't something SQL related and AFAIK only supports one property.
Author
Owner

@beberlei commented on GitHub (Feb 13, 2020):

The indexBy must reference a property, not the columns. I am unsure how this has worked for you before, can you please post how you accessed it by these indexes before? So when doing $user->roles["..."] how did the lookup look like?

Edit: Also please post both ID declaration on User and Role entities.

@beberlei commented on GitHub (Feb 13, 2020): The indexBy must reference a *property*, not the columns. I am unsure how this has worked for you before, can you please post how you accessed it by these indexes before? So when doing $user->roles["..."] how did the lookup look like? Edit: Also please post both ID declaration on User and Role entities.
Author
Owner

@estebanolm commented on GitHub (Feb 14, 2020):

Sorry, it was my first or second question, and I didin't konw how to create markdown

This has worked for me since 4.2

In Migrations, was created, MAIN IMPORTANCE IS COMPOSED PRIMARY KEY:

$this->addSql('CREATE TABLE sec_user_roles (user_id INT NOT NULL, role_id VARCHAR(50) NOT NULL, INDEX IDX_5795ACE7A76ED395 (user_id), INDEX IDX_5795ACE7D60322AC (role_id), PRIMARY KEY(user_id, role_id)) DEFAULT CHARACTER SET UTF8 COLLATE UTF8_unicode_ci ENGINE = InnoDB');
/**
 * @ORM\Entity
 * @ORM\Table(name="sec_roles")
 * @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="region_entity")
 * 
 * IMPORTANT: PREVIOUS WAS  "class htRole extends Role"
 *            SEE https://github.com/symfony/symfony/pull/22048
 */
class htRole //extends Role // VER https://github.com/symfony/symfony/pull/22048
{
  //NOTE: THERE IS NO "users" VARIALBLE HAS I NEVER ACCESS USERS FROM ROLES, SO
  // I HAS NO NEED TO CREATE A users VARIABLE POINTING TO USERS CLASS

	/**
	 * @var string Role identifier
	 *
	 * @ORM\Id
	 * @ORM\Column(name="id_role", type="string", length=50, nullable=false, unique=true)
	 */
	protected $id_role;

       //NOTE: THER IS NO setRole() as Roles are in a table BBDD, NOT CREATED ON THE FLY.

	public function getRole()
	{
		return $this->id_role;
	}
	
	public function __toString(): string
	{
	    return $this->id_role;
	}
}

class htUser implements UserInterface, EquatableInterface, \Serializable
{

    /**
     * @ORM\ManyToMany(targetEntity="App\Entity\Security\htRole", indexBy="role_id, user_id", cascade={"persist"})
     * @ORM\JoinTable(name="sec_user_roles",
     *     joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)},
     *     inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id_role", nullable=false)}
     *)
     */
    protected $roles;

/**
     * Constructor
     */
    public function __construct()
    {
        $this->roles = new ArrayCollection();
    }

    /**
     * Returns the roles granted to the user.
     * 
     * IMPORTNATE: VER https://github.com/symfony/symfony/pull/22048
     *
     * @return Role[] The user roles
     */
    public function getRoles()
    {
        //IMPORTANTE: Sf2 requires this as array
        //http://www.maestrosdelweb.com/curso-symfony2-seguridad-de-acceso/
        $arr_objects = $this->roles->toArray();
        
        /* @var $value htRole */
        $array_ret = array_map(function($value){
            return $value->getRole();
        }, $arr_objects);
        return $array_ret;
    }
    
    public function getRolesObjects()
    {
        //IMPORTANTE: Sf2 requires this as array
        //http://www.maestrosdelweb.com/curso-symfony2-seguridad-de-acceso/
        return $this->roles->toArray();
    }

    /**
     * Add roles
     *
     * @param htRole $roles
     * @return htUser
     */
    public function addRole(htRole $roles)
    {
        //Avoid to add repeated role
        $user_actual_roles = $this->getRolesObjects();
        $role_found = false;
        /* @var $act_role htRole */ 
        foreach ($user_actual_roles as $act_role)
        {
            if ($act_role->getRole()==$roles->getRole())
            {
                $role_found = true;
            }
        }
        if (!$role_found)
        {
            $this->roles[] = $roles;
        }
        
        return $this;
    }
    
    /**
     * Remove roles
     *
     * @param htRole $roles
     */
    public function removeRole(htRole $roles)
    {
        $this->roles->removeElement($roles);
    }

}

@estebanolm commented on GitHub (Feb 14, 2020): Sorry, it was my first or second question, and I didin't konw how to create markdown This has worked for me since 4.2 In Migrations, was created, MAIN IMPORTANCE IS COMPOSED PRIMARY KEY: ``` php $this->addSql('CREATE TABLE sec_user_roles (user_id INT NOT NULL, role_id VARCHAR(50) NOT NULL, INDEX IDX_5795ACE7A76ED395 (user_id), INDEX IDX_5795ACE7D60322AC (role_id), PRIMARY KEY(user_id, role_id)) DEFAULT CHARACTER SET UTF8 COLLATE UTF8_unicode_ci ENGINE = InnoDB'); ``` ``` php /** * @ORM\Entity * @ORM\Table(name="sec_roles") * @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="region_entity") * * IMPORTANT: PREVIOUS WAS "class htRole extends Role" * SEE https://github.com/symfony/symfony/pull/22048 */ class htRole //extends Role // VER https://github.com/symfony/symfony/pull/22048 { //NOTE: THERE IS NO "users" VARIALBLE HAS I NEVER ACCESS USERS FROM ROLES, SO // I HAS NO NEED TO CREATE A users VARIABLE POINTING TO USERS CLASS /** * @var string Role identifier * * @ORM\Id * @ORM\Column(name="id_role", type="string", length=50, nullable=false, unique=true) */ protected $id_role; //NOTE: THER IS NO setRole() as Roles are in a table BBDD, NOT CREATED ON THE FLY. public function getRole() { return $this->id_role; } public function __toString(): string { return $this->id_role; } } class htUser implements UserInterface, EquatableInterface, \Serializable { /** * @ORM\ManyToMany(targetEntity="App\Entity\Security\htRole", indexBy="role_id, user_id", cascade={"persist"}) * @ORM\JoinTable(name="sec_user_roles", * joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)}, * inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id_role", nullable=false)} *) */ protected $roles; /** * Constructor */ public function __construct() { $this->roles = new ArrayCollection(); } /** * Returns the roles granted to the user. * * IMPORTNATE: VER https://github.com/symfony/symfony/pull/22048 * * @return Role[] The user roles */ public function getRoles() { //IMPORTANTE: Sf2 requires this as array //http://www.maestrosdelweb.com/curso-symfony2-seguridad-de-acceso/ $arr_objects = $this->roles->toArray(); /* @var $value htRole */ $array_ret = array_map(function($value){ return $value->getRole(); }, $arr_objects); return $array_ret; } public function getRolesObjects() { //IMPORTANTE: Sf2 requires this as array //http://www.maestrosdelweb.com/curso-symfony2-seguridad-de-acceso/ return $this->roles->toArray(); } /** * Add roles * * @param htRole $roles * @return htUser */ public function addRole(htRole $roles) { //Avoid to add repeated role $user_actual_roles = $this->getRolesObjects(); $role_found = false; /* @var $act_role htRole */ foreach ($user_actual_roles as $act_role) { if ($act_role->getRole()==$roles->getRole()) { $role_found = true; } } if (!$role_found) { $this->roles[] = $roles; } return $this; } /** * Remove roles * * @param htRole $roles */ public function removeRole(htRole $roles) { $this->roles->removeElement($roles); } } ```
Author
Owner

@beberlei commented on GitHub (Feb 15, 2020):

I don‘t understand, you dont even use the roles collection as indexed by and you don‘t call containsKey() either. Instead you iterate over the collection and do a comparison by name/id. I believe you can just remove the indexBy instruction., unless you use this differently at another part in the code.

This might have worked before by accident, but it must be indexBy=„id_role“ using the htRole id property name.

@beberlei commented on GitHub (Feb 15, 2020): I don‘t understand, you dont even use the roles collection as indexed by and you don‘t call containsKey() either. Instead you iterate over the collection and do a comparison by name/id. I believe you can just remove the indexBy instruction., unless you use this differently at another part in the code. This might have worked before by accident, but it must be indexBy=„id_role“ using the htRole id property name.
Author
Owner

@estebanolm commented on GitHub (Feb 24, 2020):

I don‘t understand, you dont even use the roles collection as indexed by and you don‘t call containsKey() either. Instead you iterate over the collection and do a comparison by name/id. I believe you can just remove the indexBy instruction., unless you use this differently at another part in the code.

This might have worked before by accident, but it must be indexBy=„id_role“ using the htRole id property name.

This code is from Symphony 2 after many upgrades. I don't remember, but I think I had problems becouse I used entities instead of strings, or there was extra calls to the database.

Indeed it worked, so yes, I think by accident.

@estebanolm commented on GitHub (Feb 24, 2020): > > > I don‘t understand, you dont even use the roles collection as indexed by and you don‘t call containsKey() either. Instead you iterate over the collection and do a comparison by name/id. I believe you can just remove the indexBy instruction., unless you use this differently at another part in the code. > > This might have worked before by accident, but it must be indexBy=„id_role“ using the htRole id property name. This code is from Symphony 2 after many upgrades. I don't remember, but I think I had problems becouse I used entities instead of strings, or there was extra calls to the database. Indeed it worked, so yes, I think by accident.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: doctrine/archived-orm#6400