Symfony 2.1, el libro oficial

8.3. Relaciones y asociaciones de entidades

Extendiendo el ejemplo de las secciones anteriores, supón que los productos de la aplicación pertenecen a una (y sólo a una) categoría. En este caso, necesitarás un objeto de tipo Category y una manera de relacionar un objeto Product a un objeto Category.

En primer lugar, crea la nueva entidad Category. Para ello puedes utilizar el siguiente comando de Doctrine:

$ php app/console doctrine:generate:entity
      --entity="AcmeStoreBundle:Category"
      --fields="name:string(255)"

Esta tarea genera la entidad Category con un campo id, un campo name y los getters y setters correspondientes.

8.3.1. Mapeando relaciones

Para relacionar las entidades Category y Product, debes crear en primer lugar una propiedad llamada producto en la clase Category:

// src/Acme/StoreBundle/Entity/Category.php

// ...
use Doctrine\Common\Collections\ArrayCollection;

class Category
{
    // ...

    /**
     * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
     */
    protected $products;

    public function __construct()
    {
        $this->products = new ArrayCollection();
    }
}
# src/Acme/StoreBundle/Resources/config/doctrine/Category.orm.yml
Acme\StoreBundle\Entity\Category:
    type: entity
    # ...
    oneToMany:
        products:
            targetEntity: Product
            mappedBy: category
    # no olvides inicializar la colección en el método
    # __construct() de la entidad

Como un objeto Category puede estar relacionado con muchos objetos de tipo Product, se define la propiedad products de tipo array para poder almacenar todos esos objetos Product. Una vez más, esto no se hace porque lo necesite Doctrine, sino porque en la aplicación tiene sentido que cada Category almacene un array de objetos Product.

Nota El código del método __construct() es importante porque Doctrine requiere que la propiedad $products sea un objeto de tipo ArrayCollection. Este objeto se comporta casi exactamente como un array, pero añade cierta flexibilidad. Si utilizar este objeto te parece raro, imagina que es un array normal y ya está.

Truco El valor de targetEntity utilizado en el ejemplo anterior puede hacer referencia a cualquier entidad con un espacio de nombres válido, no sólo a las entidades definidas en la misma clase. Para relacionarlo con una entidad definida en una clase o bundle diferente, escribe el espacio de nombres completo.

A continuación, como cada clase Product se puede relacionar exactamente con un objeto Category(y sólo uno), puedes añadir una propiedad $category a la clase Product:

// src/Acme/StoreBundle/Entity/Product.php

// ...
class Product
{
    // ...

    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    protected $category;
}
# src/Acme/StoreBundle/Resources/config/doctrine/Product.orm.yml
Acme\StoreBundle\Entity\Product:
    type: entity
    # ...
    manyToOne:
        category:
            targetEntity: Category
            inversedBy: products
            joinColumn:
                name: category_id
                referencedColumnName: id

Por último, ahora que has añadido una nueva propiedad a ambas clases (Category y Product) puedes hacer que Doctrine añada automáticamente los getters y setters que faltan con el siguiente comando:

$ php app/console doctrine:generate:entities Acme

Por el momento no te fijes mucho en los metadatos de las entidades de Doctrine. Piensa que tienes dos clases (Category y Product) con una relación de tipo "uno a muchos". La clase Category tiene un array de objetos Product y el objeto Producto puede contener un objeto Category. En otras palabras, las clases que has creado tienen sentido desde el punto de vista de tu aplicación. El hecho de que los datos se vayan a persistir en una base de datos, siempre es algo secundario.

Ahora observa los metadatos de la propiedad $category en la clase Product. Esta información le dice a Doctrine que la clase relacionada es Category y que debe guardar el id de la categoría asociada en un campo llamado category_id de la tabla product. En otras palabras, el objeto Category relacionado se almacena en la propiedad $category, pero internamente Doctrine persiste esta relación almacenando el valor del id de la categoría en la columna category_id de la tabla product.

Cómo gestiona Doctrine las entidades relacionadas

Figura 8.3 Cómo gestiona Doctrine las entidades relacionadas

Los metadatos de la propiedad $products del objeto Category son menos importantes, y simplemente le indican a Doctrine que utilice la propiedad Product.category para resolver la relación.

Antes de continuar, asegúrate de decirle a Doctrine que cree la nueva tabla category, la nueva columna product.category_id y la nueva clave externa:

$ php app/console doctrine:schema:update --force

Nota Ejecuta este comando solamente cuando estés desarrollando la aplicación. En el servidor de producción tienes que utilizar las migraciones que proporciona el bundle DoctrineMigrationsBundle.

8.3.2. Guardando las entidades relacionadas

El siguiente código muestra un ejemplo de cómo usar las entidades relacionadas dentro de un controlador de Symfony:

// ...

use Acme\StoreBundle\Entity\Category;
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function createProductAction()
    {
        $category = new Category();
        $category->setName('Main Products');

        $product = new Product();
        $product->setName('Foo');
        $product->setPrice(19.99);
        // relaciona este producto con una categoría
        $product->setCategory($category);

        $em = $this->getDoctrine()->getManager();
        $em->persist($category);
        $em->persist($product);
        $em->flush();

        return new Response(
            'Created product id: '.$product->getId().' and category id: '.$category->getId()
        );
    }
}

Después de ejecutar este código, se añade una fila en las tablas category y product. La columna product.category_id para el nuevo producto se establece al valor del id de la nueva categoría. Doctrine se encarga de gestionar estas relaciones automáticamente.

8.3.3. Obteniendo los objetos relacionados

Cuando quieras obtener los objetos asociados, la forma de trabajar es muy similar. Primero buscas un objeto $product y luego accedes a su objeto Category asociado:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->find($id);

    $categoryName = $product->getCategory()->getName();

    // ...
}

En este ejemplo, primero buscas un objeto Product en función del valor de su id. Esto hace que se ejecute una sola sentencia SQL para obtener los datos del objeto $product. Después, cuando se realiza la llamada $product->getCategory()->getName(), Doctrine realiza automáticamente otra consulta SQL para obtener los datos del objeto Category relacionado con este Product.

Cómo funciona el lazy loading de Doctrine

Figura 8.4 Cómo funciona el lazy loading de Doctrine

La clave es que puedes acceder fácilmente a los datos de la categoría relacionada con el producto, pero no tienes sus datos hasta que realmente los necesites (esto es lo que se llama "lazy loading" o carga diferida de información).

También puedes realizar búsquedas en el sentido contrario de la relación:

public function showProductAction($id)
{
    $category = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Category')
        ->find($id);

    $products = $category->getProducts();

    // ...
}

En este caso, ocurre lo mismo: primero buscas un único objeto Category, y luego Doctrine hace una segunda consulta para recuperar los objetos Product relacionados, pero sólo si tratas de acceder a su información (es decir, sólo cuando invoques a ->getProducts()). La variable $products es un array de todos los objetos Product relacionados con el objeto Category indicado (y relacionado a través del valor category_id de los productos).

8.3.4.  Uniendo registros relacionados

En los ejemplos anteriores, se realizan dos consultas: la primera para el objeto original (Category por ejemplo) y la segunda para el/los objetos relacionados (un array de Product por ejemplo).

Truco Recuerda que puedes ver todas las consultas SQL realizadas durante una petición a través de la barra de herramientas de depuración web de Symfony2.

Si sabes de antemano que vas a necesitar los datos de todos los objetos, puedes ahorrarte una consulta haciendo una unión o "join" en la primera consulta. Añade el siguiente método a la clase ProductRepository:

// src/Acme/StoreBundle/Entity/ProductRepository.php
public function findOneByIdJoinedToCategory($id)
{
    $query = $this->getEntityManager()
        ->createQuery('
            SELECT p, c FROM AcmeStoreBundle:Product p
            JOIN p.category c
            WHERE p.id = :id'
        )->setParameter('id', $id);

    try {
        return $query->getSingleResult();
    } catch (\Doctrine\ORM\NoResultException $e) {
        return null;
    }
}

Ahora ya puedes utilizar este método en el controlador para obtener un objeto Product y su correspondiente Category con una sola consulta:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->findOneByIdJoinedToCategory($id);

    $category = $product->getCategory();

    // ...
}

8.3.5. Más información sobre las asociaciones

Esta sección sólo ha sido una introducción a un tipo de relación entre entidades muy común: la relación uno a muchos. Para obtener detalles más avanzados y ejemplos de cómo utilizar otros tipos de relaciones (por ejemplo, uno a uno, muchos a muchos), consulta la sección Association mapping de la documentación de Doctrine.

Nota Si utilizas anotaciones, tienes que prefijar todas ellas con ORM\ (por ejemplo, ORM\OneToMany), algo que no se explica en la documentación de Doctrine. También tendrás que incluir la declaración use Doctrine\ORM\Mapping as ORM; para importar el prefijo ORM de las anotaciones.