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
<!-- src/Acme/StoreBundle/Resources/config/doctrine/Category.orm.xml -->
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Acme\StoreBundle\Entity\Category">
<!-- ... -->
<one-to-many field="products"
target-entity="product"
mapped-by="category"
/>
<!--
no olvides inicializar la colección
en el método __construct() de la entidad
-->
</entity>
</doctrine-mapping>
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
.
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
.
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.