Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple OneToMany relations to one Entity

Once upon a time in the dark abyss somewere deep in the lands of Symfony there was a frustrated programmer. He tried and tried but somehow the evil doctrine striked again and again. Also the villains Joins, Associative tables and One-to-Many/Many-to-One were giving him a hard time. Then, on a late afternoon StackOverflow and it's community came to the rescue.

Enough fairytales. My problem is that I have three tables that should all refer to the same table to get attachments.

- Mail
- Order
- Ticket

Each of these three entities can have attachments. So I made an Attachment entity.

Now, my database contains the following

Table: mails
- id
- from
- to
- message

Table attachments
- id
- name
- path

Table: orders
- id
- ...

Table: tickets
- id
- name
- description
- ...

Table attachment_associations
- id
- type
- parent_id
- attachment_id

What I wold like to do is to be able to map orders, tickets and mails to the same attachments table.

However, i'm stuck on how to do this in doctrine.

Update

I tried using the following method. This does seem to get the record I am looking for. But I don't know how to automatically create, update or delete the record in the associative table (join table) using this method.

/**
 * @ORM\ManyToMany(targetEntity="\...\...\Entity\Attachment")
 * @ORM\JoinTable(name="attachment_associations",
 *      joinColumns={@ORM\JoinColumn(name="parentId", referencedColumnName="id")},
 *      inverseJoinColumns={
 *          @ORM\JoinColumn(name="attachmentId", referencedColumnName="id")
 *     }
 * )
 */
protected $attachments;

Another update

If I delete a Mail, Order or Ticket, will all corresponding attachments be deleted as well?

like image 368
Peter Avatar asked Oct 19 '15 13:10

Peter


2 Answers

One way to do this very easily is to implement a mapped super class with class table inheritance which your other entities extend from.

Though there are performance implications which you have to judge for your specific project.

Here's a simple example:


The mapped super class

<?php

namespace AcmeBundle\Model;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;


/**
 * @ORM\Entity()
 * @ORM\InheritanceType("JOINED")
 * @ORM\DiscriminatorColumn(name="type", type="string")
 */
abstract class SuperClass
{
    /**
     * @var int
     *
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer")
     */
    protected $id;

    /**
     * @var Attachment[]
     *
     * @ORM\ManyToMany(targetEntity="Attachment", mappedBy="parents")
     */
    protected $attachments = [];

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

    // put setters/getters for $attachments here
}

and the attachment manage the association.

<?php

namespace AcmeBundle\Model;

use Doctrine\ORM\Mapping as ORM;


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

    /**
     * @var SuperClass
     *
     * @ORM\ManyToMany(targetEntity="SuperClass", inversedBy="attachments")
     */
    private $parents;

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

the entity simply extends the super class

<?php

namespace AcmeBundle\Model;

use Doctrine\ORM\Mapping as ORM;


/**
 * @ORM\Entity()
 */
class Ticket extends SuperClass
{
}
like image 140
Yoshi Avatar answered Oct 04 '22 02:10

Yoshi


You can have all attachments in one table by using a unidirectional One-To-Many with a join table. In doctrine this is done with a unidirectional Many-To-Many with a unique constraint on the join column. It would mean one table with attachments but different join tables to connect to each parent.

The disadvantage of this solution is that it is unidirectional meaning that your attachment is not aware of the owning side of the relationship.

In full code this would look like this:

A AttachmentsTrait with setters and getters for attachments to prevent code duplication:

<?php

namespace Application\Entity;

use Doctrine\Common\Collections\Collection; 

/**
 * @property Collection $attachments
 */
trait AttachmentTrait
{
    /**
     * Add attachment.
     *
     * @param Attachment $attachment
     * @return self
     */
    public function addAttachment(Attachment $attachment)
    {
        $this->attachments[] = $attachment;

        return $this;
    }

    /**
     * Add attachments.
     *
     * @param Collection $attachments
     * @return self
     */
    public function addAttachments(Collection $attachments)
    {
        foreach ($attachments as $attachment) {
            $this->addAttachment($attachment);
        }
        return $this;
    }

    /**
     * Remove attachment.
     *
     * @param Attachment $attachments
     */
    public function removeAttachment(Attachment $attachment)
    {
        $this->attachments->removeElement($attachment);
    }

    /**
     * Remove attachments.
     *
     * @param Collection $attachments
     * @return self
     */
    public function removeAttachments(Collection $attachments)
    {
        foreach ($attachments as $attachment) {
            $this->removeAttachment($attachment);
        }
        return $this;
    }

    /**
     * Get attachments.
     *
     * @return Collection
     */
    public function getAttachments()
    {
        return $this->attachments;
    }
}

Your Mail entity:

<?php

namespace Application\Entity;

use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;    

class Mail
{
    use AttachmentsTrait;

    /**
     * @var integer
     * @ORM\Id
     * @ORM\Column(type="integer", nullable=false)
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

    /**
     * @ORM\ManyToMany(targetEntity="Attachment")
     * @ORM\JoinTable(name="mail_attachments",
     *     inverseJoinColumns={@ORM\JoinColumn(name="attachment_id", referencedColumnName="id")},
     *     joinColumns={@ORM\JoinColumn(name="mail_id", referencedColumnName="id", unique=true)}
     * )
     */
    $attachments;

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

Your Order entity:

<?php

namespace Application\Entity;

use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;    

class Order
{
    use AttachmentsTrait;

    /**
     * @var integer
     * @ORM\Id
     * @ORM\Column(type="integer", nullable=false)
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

    /**
     * @ORM\ManyToMany(targetEntity="Attachment")
     * @ORM\JoinTable(name="order_attachment",
     *     inverseJoinColumns={@ORM\JoinColumn(name="attachment_id", referencedColumnName="id")},
     *     joinColumns={@ORM\JoinColumn(name="order_id", referencedColumnName="id", unique=true)}
     * )
     */
    $attachments;

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

Your Ticket entity:

<?php

namespace Application\Entity;

use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;    

class Ticket
{
    use AttachmentsTrait;

    /**
     * @var integer
     * @ORM\Id
     * @ORM\Column(type="integer", nullable=false)
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    protected $id;

    /**
     * @ORM\ManyToMany(targetEntity="Attachment")
     * @ORM\JoinTable(name="ticket_attachment",
     *     inverseJoinColumns={@ORM\JoinColumn(name="attachment_id", referencedColumnName="id")},
     *     joinColumns={@ORM\JoinColumn(name="ticket_id", referencedColumnName="id", unique=true)}
     * )
     */
    $attachments;

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

EDIT:

If you really want the Attachment to be aware of the other side you could add an additional entity in between to manage this. It would mean making the join tables themselves to entities for example: MailAttachment, TicketAttachment and OrderAttachment.

like image 32
Wilt Avatar answered Oct 04 '22 04:10

Wilt