phpsymfonydoctrine-ormclass-table-inheritance

Doctrine class table inheritance mapping: why a foreign key to parent table?


Short question: can I avoid generating foreign keys for inherited classes to parent classes? Can a discriminator column for inherintance mapping be set in a relation owner instead of a parent class?

Explanation:

I am designing a model for invoices, where invoice subject can be 1 of 3 types:

After reading Doctrine inheritance mapping I think Class table inheritance is the one that best fits my needs.

Mapped superclass could better fit my needs if I could draw a relation from Invoice to InvoiceSubject, but I think I can't:

A mapped superclass cannot be an entity, it is not query-able and persistent relationships defined by a mapped superclass must be unidirectional (with an owning side only). This means that One-To-Many associations are not possible on a mapped superclass at all. Furthermore Many-To-Many associations are only possible if the mapped superclass is only used in exactly one entity at the moment.

Also, using an Interface could be a solution, but an interface in a relation can only be mapped to one entity.

So, this is my model:

/**
 * @ORM\Entity
 */
class Invoice
{
    /**
     * @ORM\ManyToOne(targetEntity="InvoiceSubject")
     * @var InvoiceSubject
     */
    protected $subject;
}

/**
 * @ORM\Entity
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="invoicesubject_type", type="string")
 * @ORM\DiscriminatorMap({"invoice-subject" = "InvoiceSubject", "contract" = "Contract", "provider" = "Provider", "client" = "Client"})
 */
class InvoiceSubject
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    protected $id;
}

/**
 * @ORM\Entity
 */
class Contract extends InvoiceSubject{}

/**
 * @ORM\Entity
 */
class Provider extends InvoiceSubject implements ProviderInterface{}

/**
 * @ORM\Entity
 */
class Client extends InvoiceSubject{}

But, when I try to generate the model (bin/console doctrine:schema:update --dump-sql) I see its trying to create foreign keys from child classes to parent (tables already exist and have data):

ALTER TABLE contract ADD CONSTRAINT FK_E9CCE71ABF396750 FOREIGN KEY (id) REFERENCES invoice_subject (id) ON DELETE CASCADE;
ALTER TABLE provider ADD CONSTRAINT FK_B2F1AF1BBF396750 FOREIGN KEY (id) REFERENCES invoice_subject (id) ON DELETE CASCADE;
ALTER TABLE client ADD CONSTRAINT FK_B2F1AF1BBF396750 FOREIGN KEY (id) REFERENCES invoice_subject (id) ON DELETE CASCADE;

This means that there will be collisions between id's coming from different tables or I will need to use different values in every table, none of the solutions are good. So, the question is:


Solution

  • okay, I probably misread part of your question:

    yes, the three child entities have a foreign key to the same id field in the parent entity (table), and it's intended. The idea is, that the core entity is the invoice subject. That entity has the id. the child entities inherit that id and gain more attributes (in the child table) by extending the parent entity. That's what inheritance means. You have essentially a core entity, that has different subtypes with extra attributes.

    (note: you could do this manually as well, add an association mapping to potential "extra data of the contract/client/provider variety" to some invoicesubject entity)

    That also means, you don't really have to take care of collisions, since the parent table entry is always created first by doctrine. When you create a new InvoiceSubject of any subtype, you effectively create an InvoiceSubject (has an id) and extend it. So, your contract/client/provider entities will just not have the same id (unless you manually set it with SQL).

    old answer

    This is a very opinionated answer. I mean ... technically it's a question of taste. I would always prefer not to do inheritance mapping if it can be reasonably avoided and there are no good reasons to do it.

    The questions are: do you have exactly one form to enter them? Do you (already) have single fields that take any of those entities? Do the entities provide the same semantics? is it lazyness that drives you to only wanting to deal with one "type" of entity? Are there so many places, that want to be extremely agnostic to what kind of subject it is and can't that be solved by a well-defined interface? When are they truly treated the same?

    Personally, looking at your use case, I probably would stay on the three entities, and Invoice with three fields, one for each entity. It's simple, it's fast, discriminator columns suck (IMHO, semantically, not technically).

    Add a function to your Invoice like

    function getSubject() {
        return $this->contract ?? $this->provider ?? $this->client;
    }
    

    setting is a tad more difficult ... but if you don't want three different setters (I honestly doubt that you create a Client and when setting the subject, you forget it's a client and want to treat it as an InvoiceSubject)

    function setSubject(InvoiceSubject $subject) {
        if($subject instanceof Client) {
             $this->client = $subject;
        } elseif (...) {} elseif (...) {}
        //... technically you should unset the others, if it can only ever be one
    }
    

    Almost all concepts that you might want to use via inheritance mapping can be solved in code with little to no overhead, but it will so much simplify lots of other stuff. And most of the time you can probably use the interface in code.

    Inheritance mapping IMHO is more trouble than it's worth. Unless you have really strong reasons to actually need it: don't do it. Dealing with uniquely different entities is so much easier than dealing with some abstract entity, where you always have to check which kind it is and pay attention ... it's really annoying. On the other hand, when do you treat the three entities exactly the same? I bet each of those have some unique stuff going on and there are always switch-cases anyway. if not: interface.

    keep it simple.