Asociaciones en Doctrine ORM

Asociaciones entre tablas de una base de datos mediante referencias a claves externas en Doctrine ORM

Las asociaciones entre tablas permiten crear relaciones entre unas entidades y otras.

Tipos de asociaciones

Las asociaciones pueden agruparse en tres grupos:

OneToOne

Una entidad está relacionada con otra entidad. Ejemplo: un usuario tiene una dirección de email.

OneToMany / ManyToOne

Una entidad está relacionada con varias entidades y viceversa. Ejemplo: un usuario tiene diferentes entradas publicadas, estas entradas pertenecen a un sólo usuario.

ManyToMany

Varias entidades están relacionadas con otras entidades. Ejemplo: un usuario puede pertenecer a varios grupos y estos grupos pueden tener varios usuarios.

Asociaciones unidireccionales y asociaciones bidireccionales

En asociación unidireccional la relación sólo fluye hacia una dirección, mientras que en una bidireccional la relación fluye en ambas direcciones. Es importante elegir adecuadamente el tipo de relación para evitar complicaciones innecesarias en el futuro.

Por ejemplo tenemos las entidades Producto y Embarque. A cada producto le corresponde un número de embarque, y éste podrá obtenerse desde la entidad producto. Si no necesitamos que desde la entidad embarque se sepa el producto en concreto, bastaría con una relación unidireccional. Si, en cambio, querríamos poder obtener el producto desde el número de embarque, necesitaríamos la relación bidireccional.

El Owning side y el Inverse side

Una asociación unidireccional sólo tiene un Owning side, y una relación bidireccional tiene un Owning side y un Inverse side. Cuando Doctrine actualiza la asociación, busca el Owning side para determinar si ha cambiado algo en las entidades.

Para relaciones bidireccionales ManyToMany, el Owning side puede estar en cualquiera de los dos lados. En las relaciones bidireccionales OneToOne, el Owning side es el lado con la clave externa. En las relaciones OneToMany y ManyToOne el lado Many ha de ser el Owning side.

Las colecciones

Las colecciones son básicamente arrays, pero son instancias de la clase ArrayCollection. Esta clase tiene métodos que permiten trabajar con los objetos del array de forma más fácil y hacer uso del lazy loading, que permite no tener los datos hasta que no se necesitan.

Las colecciones siempre se inician en los constructores de las asociaciones OneToMany y ManyToMany.

Definiciones de asociaciones

Doctrine utiliza referencias a entidades para trabajar con claves externas (foreign keys). Una referencia a una entidad representa una clave externa. Una colección representa varias claves externas apuntando a la entidad que posee la colección (Owning side).

ManyToOne, unidireccional

Es la asociación más común entre entidades. En este ejemplo es entre las entidades User y Address:

/** @Entity **/
class User
{
    // ...

    /**
     * @ManyToOne(targetEntity="Address")
     **/
    private $address;
}

/** @Entity **/
class Address
{
    // ...
}

OneToOne unidireccional

En esta relación, una entidad Producto referencia a una entidad Embarque en modo unidireccional:

/** @Entity **/
class Product
{
    // ...

    /**
     * @OneToOne(targetEntity="Shipping")
     **/
    private $shipping;

    // ...
}

/** @Entity **/
class Shipping
{
    // ...
}

OneToOne bidireccional

En este caso la relación es uno a uno entre Clientes y Cesta en modo bidireccional:

/** @Entity **/
class Customer
{
    // ...

    /**
     * @OneToOne(targetEntity="Cart", mappedBy="customer")
     **/
    private $cart;

    // ...
}

/** @Entity **/
class Cart
{
    // ...

    /**
     * @OneToOne(targetEntity="Customer", inversedBy="cart")
     **/
    private $customer;

    // ...
}

En este caso se puede ver que cuando se trata del Owner side, se añade la anotación "mappedBy", y cuando se trata del Inverse side, se añade "inversedBy".

OneToMany bidireccional

Una relación OneToMany tiene que ser bidireccional a no ser que se emplee una tabla join. Esto es así porque la clave externa ha de estar en el lado Many. Doctrine necesita la asociación ManyToOne para conocer el Owner side con la clave externa.

/** @Entity **/
class Product
{
    // ...
    /**
     * @OneToMany(targetEntity="Feature", mappedBy="product")
     **/
    private $features;
    // ...

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

/** @Entity **/
class Feature
{
    // ...
    /**
     * @ManyToOne(targetEntity="Product", inversedBy="features")
     **/
    private $product;
    // ...
}

OneToMany unidireccional con tabla Join

Una asociación unidireccional OneToMany se puede hacer con una tabla join. Desde el punto de vista de Doctrine es como una relación ManyToMany unidireccional pero con una restricción en uno de los lados:

/** @Entity **/
class User
{
    // ...

    /**
     * @ManyToMany(targetEntity="Phonenumber")
     * @JoinTable(name="users_phonenumbers",
     *      joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@JoinColumn(name="phonenumber_id", referencedColumnName="id", unique=true)}
     *      )
     **/
    private $phonenumbers;

    public function __construct()
    {
        $this->phonenumbers = new \Doctrine\Common\Collections\ArrayCollection();
    }

    // ...
}

/** @Entity **/
class Phonenumber
{
    // ...
}

ManyToMany unidireccional

El siguiente ejemplo muestra una relación unidireccional entre Usuarios y Grupos:

/** @Entity **/
class User
{
    // ...

    /**
     * @ManyToMany(targetEntity="Group")
     * @JoinTable(name="users_groups",
     *      joinColumns={@JoinColumn(name="user_id", referencedColumnName="id")},
     *      inverseJoinColumns={@JoinColumn(name="group_id", referencedColumnName="id")}
     *      )
     **/
    private $groups;

    // ...

    public function __construct() {
        $this->groups = new \Doctrine\Common\Collections\ArrayCollection();
    }
}

/** @Entity **/
class Group
{
    // ...
}

ManyToMany bidireccional

Este ejemplo es igual que el anterior pero con una relación bidireccional:

/** @Entity **/
class User
{
    // ...

    /**
     * @ManyToMany(targetEntity="Group", inversedBy="users")
     * @JoinTable(name="users_groups")
     **/
    private $groups;

    public function __construct() {
        $this->groups = new \Doctrine\Common\Collections\ArrayCollection();
    }

    // ...
}

/** @Entity **/
class Group
{
    // ...
    /**
     * @ManyToMany(targetEntity="User", mappedBy="groups")
     **/
    private $users;

    public function __construct() {
        $this->users = new \Doctrine\Common\Collections\ArrayCollection();
    }

    // ...
}

Definiciones opcionales

En algunas de las asociaciones anteriores se ha utilizado @JoinColumn y @JoinTable, que pueden personalizarse, pero normalmente se utilizan sus valores por defecto, que son:

name: "<fieldname>_id"
referencedColumnName: "id"

En el caso de ManyToMany anterior, puede utilizarse este código:

class User
{
    //...
    /** @ManyToMany(targetEntity="Group") **/
    private $groups;
    //...
}

O el siguiente:

class User
{
    //...
    /**
     * @ManyToMany(targetEntity="Group")
     * @JoinTable(name="User_Group",
     *      joinColumns={@JoinColumn(name="User_id", referencedColumnName="id")},
     *      inverseJoinColumns={@JoinColumn(name="Group_id", referencedColumnName="id")}
     *      )
     **/
    private $groups;
    //...
}

El resultado será el mismo.

Fuentes: doctrine-project.org, culttt.com, librosweb.es