简体   繁体   中英

Doctrine: ManyToX relation between entities with composite keys

I have three entites, let's call them Site , Category and Tag . In this scenario, Category and Tag have a composite ID generated from the Site entity and an external ID which is not unique ( site and id together are unique). Category has a many-to-many relation on Tag . (Although my issue is also reproducible with ManyToOne relations.)

The Site entity:

/**
 * @ORM\Entity
 */
class Site
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    private $id;

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

The Category entity:

/**
 * @ORM\Entity
 */
class Category extends AbstractSiteAwareEntity
{
    /**
     * @ORM\Column(type="string")
     */
    private $name;

    /**
     * @ORM\ManyToMany(targetEntity="Tag")
     */
    private $tags;
}

The Tag entity:

/**
 * @ORM\Entity
 */
class Tag extends AbstractSiteAwareEntity
{
    /**
     * @ORM\Column(type="string")
     */
    private $name;
}

The last two entities inherit from the AbstractSiteAwareEntity class, which defines the composite index:

abstract class AbstractSiteAwareEntity
{
    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Site")
     */
    public $site;

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    public $id;
}

As an example of a ManyToOne relation, let's imagine a Post entity, which has an auto-increment ID and a reference to Category :

/**
 * @ORM\Entity
 */
class Post
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    public $id;

    /**
     * @ORM\ManyToOne(targetEntity="Category")
     */
    private $category;

    /**
     * @ORM\Column(type="string")
     */
    private $title;

    /**
     * @ORM\Column(type="string")
     */
    private $content;
}

When updating the schema with bin/console doctrine:schema:update --dump-sql --force --complete , I get the following exception:

An exception occurred while executing 'ALTER TABLE category_tag ADD CONSTRAINT FK_D80F351812469DE2 FOREIGN KEY (category_id) REFERENCES Category (id) ON DELETE CASCADE':

SQLSTATE[HY000]: General error: 1005 Can't create table xxxxxxx . #sql-3cf_14b6 (errno: 150 "Foreign key constraint is incorrectly formed")

I'm not sure what might be wrong … is there an error in my entity definitions? Or is this a Doctrine bug?

NOTE: I'm using doctrine/dbal 2.6.3 on Symfony 3.4.

You have to define the joined columns in the annotation to make it work:

For a ManyToOne relation:

/**
 * @ORM\ManyToOne(targetEntity="Tag")
 * @ORM\JoinColumns({
 *     @ORM\JoinColumn(name="tag_site_id", referencedColumnName="site_id"),
 *     @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
 * })
 **/
protected $tag;

For a ManyToMany relation:

/**
 * @ORM\ManyToMany(targetEntity="Tag")
 * @ORM\JoinTable(name="category_tags",
 *      joinColumns={
 *     @ORM\JoinColumn(name="category_site_id", referencedColumnName="site_id"),
 *     @ORM\JoinColumn(name="category_id", referencedColumnName="id")
 *     },
 *      inverseJoinColumns={
 *     @ORM\JoinColumn(name="tag_site_id", referencedColumnName="site_id"),
 *     @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
 *     }
 *      )
 **/
 protected $tags;

How it works: 2 columns are created as a FOREIGN KEY that REFERENCES the composite foreign key.

Note: In "@ORM\\JoinColumn" setting:

  • "referencedColumnName" setting is an id column name on the related table, so this should already exist.

  • "name" is the column name that stores the foreign key and to be created and in case of ManyToOne, it should not conflict with any of the columns of the entity.

The order of your joinColumns definitions is important. The composite foreign key should start with a column that has an index in the referenced table. In this case, "@ORM\\JoinColumn" to "site_id" should be first . Reference

Based on Jannis' answer , I was finally able to find a solution for this. The solution consists of two parts:

The first part is bit weird: Apparently Doctrine needs the $id before the $site reference in the composite primary key defined in AbstractSiteEntity . This sounds strange, but it's definitely true, because I tried swapping them several times back and forth, and only this order works for some reason.

abstract class AbstractSiteAwareEntity
{
    // $id must come before $site

    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    public $id;

    /**
     * @ORM\Id
     * @ORM\ManyToOne(targetEntity="Site")
     */
    public $site;
}

For ManyToMany relations, the indices in the JoinTable must be declared explicitely, because Doctrine failed to create a composite foreign key by itself. I used Jannis' proposal for this with a few modifications (column names aren't actually neccessary):

/**
 * @ORM\ManyToMany(targetEntity="Tag")
 * @ORM\JoinTable(
 *  joinColumns={
 *      @ORM\JoinColumn(referencedColumnName="id"),
 *      @ORM\JoinColumn(referencedColumnName="site_id")
 *  },
 *  inverseJoinColumns={
 *      @ORM\JoinColumn(referencedColumnName="id"),
 *      @ORM\JoinColumn(referencedColumnName="site_id")
 *  }
 * )
*/
private $tags;

You can't use composite PrK key for relations.
Instead, define an ID, and a composite UNI key (or just UNI key in this case)

Composite UNI key example

/**
 * SiteAware
 *
 * @ORM\Table(name="site_aware",
 *     uniqueConstraints={@ORM\UniqueConstraint(columns={"site_id", "random_entity_id"})})
 * @UniqueEntity(fields={"site", "randomEntity"}, message="This pair combination is already listed")
 */
abstract class AbstractSiteAwareEntity
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    public $id;
    /**
     * @ORM\ManyToOne(targetEntity="Site")
     */
    public $site;
    /**
     * @ORM\ManyToOne(targetEntity="RandomEntity")
     */
    public $randomEntity;
}

Simple unique key

abstract class AbstractSiteAwareEntity
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    public $id;
    /**
     * @ORM\ManyToOne(targetEntity="Site")
     * @ORM\JoinColumn(unique=true)
     */
    public $site;
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM