Diferència entre revisions de la pàgina «A2- Framework de capes per a DDBB (20h)»

De wikiserver
Dreceres ràpides: navegació, cerca
Línia 553: Línia 553:
 
Ara, just abans de persistir l'entitat per primera vegada, Doctrine executa automàticament aquest mètode i establirà el camp created a la data actual.Consulta la [http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events documentació] sobre esdeveniments de Doctrine per obtenir molta més informació.  
 
Ara, just abans de persistir l'entitat per primera vegada, Doctrine executa automàticament aquest mètode i establirà el camp created a la data actual.Consulta la [http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#lifecycle-events documentació] sobre esdeveniments de Doctrine per obtenir molta més informació.  
  
<!--
+
 
 
== Gernerar Entitats des de una BD creada ==
 
== Gernerar Entitats des de una BD creada ==
 
El primer pas cap a construir classes d'entitat a partir d'una base de dades és pemeten que Doctrine introspecti la base de dades i generiu la corresponent metadada. Les metadades son les diferentes informacions que s'afegeixen als arxius que associen les propietats d'una classe amb el corresponent camp de la taula.
 
El primer pas cap a construir classes d'entitat a partir d'una base de dades és pemeten que Doctrine introspecti la base de dades i generiu la corresponent metadada. Les metadades son les diferentes informacions que s'afegeixen als arxius que associen les propietats d'una classe amb el corresponent camp de la taula.

Revisió del 15:03, 11 març 2015

Doctrine

Una de les tasques més comunes i desafiadores per a qualsevol aplicació implica la persistència i la lectura d'informació cap a i des d'una base de dades. Encara que el framework Symfony no integra cap ORM per defecte, l'edició estàndard de Symfony, que és la distribució més utilitzada, ve integrada amb Doctrine, una biblioteca, l'únic objectiu de la qual és donar eines poderoses per fer-ho fàcil.

La llibreria Doctrine proporciona eines per simplificar l'accés i maneig de la informació de la base de dades.

La millor manera per explicar el framework doctrine és mitjançant exmples. Per aixó, es configura l'accés a la base de dades amb doctrine i s'exemplificarà amb la creació d'un objecte anomenat Product.

Configuracio de Doctrine i la Base de Dades

Ès necessari configurar la informació per accedir a la base de dades. Per convenció, aquesta informació es configura en l'arxiu app/config/parameters.yml:

# app/config/parameters.yml
parameters:
    database_driver:   pdo_mysql
    database_host:     localhost
    database_name:     test_project
    database_user:     root
    database_password: password

Ara que doctrine ja coneix l'usuari, la contrasenya i la Base de Dades a utilitzar, pots crear-la amb la següent comanda:

$ php app/console doctrine:database:create

Creant una classe Entity

Imagina que estàs desenvolupant una aplicació en la qual vas a mostrar productes. Oblidant-te de Doctrine i de les bases de dades, segurament estàs pensant a utilitzar un objecte Product per representar als productes. Crea aquesta classe dins del directori Entity del bundle AcmeStoreBundle:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
 
class Product
{
    protected $name;
 
    protected $price;
 
    protected $description;
}

És una classe molt senzilla que només s'utilitza per emmagatzemar dades. Encara que es tracta d'una classe molt bàsica, compleix el seu objectiu de representar als productes de la teva aplicació. No obstant això, aquesta classe no es pot guardar en una base de dades — és només una classe PHP simple.

Podràs generar les classes de tipus entitat més fàcilment amb la següent comanda. Una vegada executat, Doctrine et farà diverses preguntes per generar l'entitat de forma interactiva:

$ php app/console doctrine:generate:entity

Mapeig d'objectes PHP a tables de BD

En comptes de treballar amb files i taules, Doctrine et permet guardar i obtenir objectes sencers a partir de la informació de la base de dades. El truc perquè això funcioni consisteix en mapear una classe PHP a una taula de la base de dades i després, mapear les propietats de la classe PHP a les columnes d'aquesta taula:

Php-bd.png

Només has d'afegir algunes metadades a la classe PHP per configurar com es mapean la classe Product i les seves propietats. Aquestes metadades es poden configurar en arxius YAML, XML o directament mitjançant anotacions a la pròpia classe PHP:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
 
/**
 * @ORM\Entity
 * @ORM\Table(name="product")
 */
class Product
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $id;
 
    /**
     * @ORM\Column(type="string", length=100)
     */
    protected $name;
 
    /**
     * @ORM\Column(type="decimal", scale=2)
     */
    protected $price;
 
    /**
     * @ORM\Column(type="text")
     */
    protected $description;
}

El nom de la taula és opcional i si ho omets, es genera automàticament en funció del nom de la classe PHP.

Pots consultar la documentació oficial de Doctrine sobre el mapeig. Tingues en compte que en la documentació de Doctrine no s'explica que si utilitzes anotacions, has de prefixar-les totes amb la cadena ORM\ (per exemple, ORM\Column(...)). Igualment, no t'oblidis d'afegir la declaració use Doctrine\ORM\Mapping as ORM; al principi de les teves classes per importar el prefix ORM\.

Consulta la secció Quoting reserved words de la documentació de Doctrine per conèixer la llista completa de paraules reservades.

Generant getters i setters

Recordem la classe Product que havíem creat:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
 
class Product
{
    protected $name;
 
    protected $price;
 
    protected $description;
}

Doctrine ja sap com persistir els objectes de tipus Product en la base de dades, però aquesta classe no és molt útil de moment. Com Product és una classe PHP normal i corrent, és necessari crear mètodes getters i setters' (getName(), setName(), etc.) per poder accedir a les seves propietats (perquè són de tipus protected). Com això és bastant habitual, existeix un comando perquè Doctrine anyada aquests mètodes automàticament:

$ php app/console doctrine:generate:entities Acme/StoreBundle/Entity/Product

Executa aquestes comandes quan estiguis desenvolupant l'aplicació. En el servidor de producció has d'utilitzar les migracions que proporciona el bundle DoctrineMigrationsBundle.

Creant les taules de la base de dades (el esquema)

Encara que tens una classe Product utilitzable amb informació de mapatge perquè Doctrine sàpiga persistir-la, encara no tens la seva corresponent taula product en la base de dades. Afortunadament, Doctrine pot crear automàticament totes les taules necessàries en la base de dades (una per a cada entitat coneguda de la teva aplicació). Per a això, executa la següent comanda:

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

Internament compara l'estructura que hauria de tenir la teva base de dades (segons la informació de mapatge de les teves entitats) amb l'estructura que realment té i genera les sentències SQL necessàries per actualitzar l'estructura de la base de dades.

En altres paraules, si afegeixes una nova propietat a la classe Product i executes aquest comando una altra vegada, es genera una sentència de tipus ALTER TABLE per afegir la nova columna a la taula product existent.

Persistint objectes a la base de dades

Ara que tens mapeada una entitat Product i la seva taula product corresponent, ja pots persistir la informació en la base de dades. De fet, persistir informació dins d'un controlador és bastant senzill. Afegeix el següent mètode al controlador DefaultController del bundle:

// src/Acme/StoreBundle/Controller/DefaultController.php
 
// ...
use Acme\StoreBundle\Entity\Product;
use Symfony\Component\HttpFoundation\Response;
 
public function createAction()
{
    $product = new Product();
    $product->setName('A Foo Bar');
    $product->setPrice('19.99');
    $product->setDescription('Lorem ipsum dolor');
 
    $em = $this->getDoctrine()->getManager();
    $em->persist($product);
    $em->flush();
 
    return new Response('Created product id '.$product->getId());
}

Explicació de les parts més importants del codi anterior:

  • Línies 9-12 En aquesta secció, creguis una instància i treballes amb l'objecte $product com faries amb qualsevol altre objecte PHP normal.
  • Línia 14 Aquesta línia obté el entity manager o gestor d'entitats de Doctrine, que s'utilitza per persistir i recuperar objectes cap a i des de la base de dades.
  • Línia 15 El mètode persist() li diu a Doctrine que ha de persistir l'objecte $product, però encara no es genera (i per tant, tampoc s'executa) la sentència SQL corresponent.
  • Línia 16 Quan es diu al mètode flush(), Doctrine examina tots els objectes que està gestionant per veure si és necessari persistir-los en la base de dades. En aquest exemple, l'objecte $product encara no s'ha persistit, per la qual cosa el gestor de l'entitat executa una consulta de tipus INSERT i crea una fila en la taula product.

Buscant objectes a la base de dades

Buscar informació de la base de dades i recuperar en forma d'objecte és encara més fàcil. Imagina que has configurat una ruta de l'aplicació per mostrar la informació d'un producte a partir del valor del seu id. El codi del controlador corresponent podria ser el següent:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->find($id);
 
    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }
 
    // ... (pasar el objeto $product a una plantilla)
}

Doctrine sempre utilitza el que es coneix com a "repositori. Aquests repositoris són com a classes PHP el treball de les quals consisteix a ajudar-te a buscar les entitats d'una determinada classe. Pots accedir al repositori de l'entitat d'una classe mitjançant el codi:

$repository = $this->getDoctrine()
    ->getRepository('AcmeStoreBundle:Product');

Una vegada que obtens el repositori, tens accés a tot tipus de mètodes útils:

// consulta por la clave principal (generalmente 'id')
$product = $repository->find($id);
 
// métodos con nombres dinámicos para buscar un valor en función de alguna columna
$product = $repository->findOneById($id);
$product = $repository->findOneByName('foo');
 
// obtiene todos los productos
$products = $repository->findAll();
 
// busca productos basándose en el valor de una columna
$products = $repository->findByPrice(19.99);

També pots utilitzar els mètodes findBy i findOneBy per obtenir objectes en funció de vàries condicions:

// busca un producto con ese nombre y ese precio
$product = $repository->findOneBy(array(
    'name'  => 'foo', 'price' => 19.99
));
 
// obtiene todos los productos con un nombre determinado
// y ordena los resultados por precio
$product = $repository->findBy(
    array('name'  => 'foo'),
    array('price' => 'ASC')
);

Actualitzant un objecte

Una vegada que hagis obtingut un objecte de Doctrine, actualitzar-ho és relativament fàcil. Suposem que l'aplicació disposa d'una ruta que actualitza la informació del producte amb identificador id:

public function updateAction($id)
{
    $em = $this->getDoctrine()->getManager();
    $product = $em->getRepository('AcmeStoreBundle:Product')->find($id);
 
    if (!$product) {
        throw $this->createNotFoundException(
            'No product found for id '.$id
        );
    }
 
    $product->setName('New product name!');
    $em->flush();
 
    return $this->redirect($this->generateUrl('homepage'));
}

Actualitzar un objecte requereix de tres passos:

  1. Obtenir l'objecte utilitzant Doctrine.
  2. Modificar l'objecte.
  3. Invocar al mètode flush() del entity manager.

Observa que no fa falta cridar al mètode $em->persist($product). Aquest mètode sive per avisar a Doctrine que vas a manipular un determinat objecte. En aquest cas, com l'objecte $product ho has obtingut mitjançant una consulta a Doctrine, aquest ja sap que ha d'estar atent als possibles canvis de l'objecte.

Eliminant un objecte

Eliminar objectes és un procés similar, però requereix invocar el mètode remove() del entity manager:

$em->remove($product);
$em->flush();

Com pot ser que imaginis, el mètode remove() avisa a Doctrine que vols eliminar aquesta entitat de la base de dades, però no l'esborra realment. La consulta DELETE corresponent no es genera ni s'executa fins que no s'invoca el mètode flush().

Exercici

Actualitza l'exercici CRUD realitzat a la UF anterior per a que utilitzi BD amb Doctrine.

Buscant Objectes amb el generador de consultes de Doctrine

Imagina que vols buscar tots aquells productes el preu dels quals sigui superior a 19.99 i retornar els resultats ordenats del més barat al més car. Aquesta cerca es pot realitzar de la següent manera amb el QueryBuilder de Doctrine:

$repository = $this->getDoctrine()
    ->getRepository('AcmeStoreBundle:Product');
 
$query = $repository->createQueryBuilder('p')
    ->where('p.price > :price')
    ->setParameter('price', '19.99')
    ->orderBy('p.price', 'ASC')
    ->getQuery();
 
$products = $query->getResult();

L'objecte QueryBuilder conté tots els mètodes necessaris per construir la consulta. En cridar al mètode getQuery(), el query builder retorna l'objecte de tipus Query amb el qual realment s'executa la consulta. El mètode getResult() retorna un array de resultats. Per obtenir solament un resultat, utilitza getSingleResult() (que llança una excepció quan no hi ha cap resultat) o getOneOrNullResult():

$product = $query->getOneOrNullResult();

Consulta la documentació de QueryBuilder per obtenir més informació.

Buscant objetes amb DQL

A més del QueryBuilder, Doctrine també et permet realitzar consultes directament amb el seu llenguatge DQL:

$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
    'SELECT p
       FROM AcmeStoreBundle:Product p
      WHERE p.price > :price
   ORDER BY p.price ASC'
)->setParameter('price', '19.99');
 
$products = $query->getResult();

La sintaxi DQL és increïblement poderosa, permetent-te unir fàcilment diferents entitats (el tema de les relacions s'explica més endavant), realitzar agrupacions, etc. Per a més informació, consulta la documentació oficial de Doctrine Query Language.

Relacions i associacions de entitats (ONE to MANY)

Suposa que els productes de l'aplicació pertanyen a una (i només a una) categoria. En aquest cas, necessitaràs un objecte de tipus Category i una manera de relacionar un objecte Product a un objecte Category. Per crear entitats, pots fer-lo com s'ha vist en els apartats anteriors o pots utilitzar la següent comanda:

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

La comanda anterior genera la entitad Category amb un id, amb un camp name i els getters i setters corresponents.

Mapeando relaciones

Per relacionar les entitats Category i Product, has de crear en primer lloc una propietat anomenada producte a la classe 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();
    }
}

El codi del mètode __construct() és important perquè Doctrine requereix que la propietat $products sigui un objecte de tipus ArrayCollection. Aquest objecte es comporta gairebé exactament com un array, però afegeix certa flexibilitat. Si utilitzar aquest objecte et sembla rar, imagina que és un array normal i ja està.

A continuació, com cada classe Product es pot relacionar exactament amb un objecte Category(i només un), pots afegir una propietat $category a la classe Product:

// src/Acme/StoreBundle/Entity/Product.php
 
// ...
class Product
{
    // ...
 
    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    protected $category;
}

Les metadades de la propietat $category a la classe Product li diu a Doctrine que la classe relacionada és Category i que ha de guardar l'id de la categoria associada en un camp anomenat category_id de la taula product. En altres paraules, l'objecte Category relacionat s'emmagatzema en la propietat $category', però internament Doctrine persisteix aquesta relació emmagatzemant el valor de l'id la categoria en la columna category_id de la taula product.

Category.png

Emmagatzemant les entitats relacionades

El següent codi mostra un exemple de com usar les entitats relacionades dins d'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()
        );
    }
}

Després d'executar aquest codi, s'afegeix una fila en les taules category i product. La columna product.category_id per al nou producte s'estableix al valor de l'id de la nova categoria. Doctrine s'encarrega de gestionar aquestes relacions automàticament.

Obtenint els objectes relacionats

Quan vulguis obtenir els objectes associats, la forma de treballar és molt similar. Primer busques un objecte $product i després accedeixes al seu objecte Category associat:

public function showAction($id)
{
    $product = $this->getDoctrine()
        ->getRepository('AcmeStoreBundle:Product')
        ->find($id);
 
    $categoryName = $product->getCategory()->getName();
 
    // ...
}

En aquest exemple, primer busques un objecte Product en funció del valor del seu id. Això fa que s'executi una sola sentència SQL per obtenir les dades de l'objecte $product. Després, quan es realitza la trucada $product->getCategory()->getName(), Doctrine realitza automàticament una altra consulta SQL per obtenir les dades de l'objecte Category relacionat amb aquest Product.

Relacions1.png

La clau és que pots accedir fàcilment a les dades de la categoria relacionada amb el producte, però no tens les seves dades fins que realment els necessitis (això és el que es diu lazy loading o càrrega diferida d'informació).

També pots realitzar cerques en el sentit contrari de la relació:

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

En aquest cas, ocorre el mateix: primer busques un únic objecte Category, i després Doctrine fa una segona consulta per recuperar els objectes Product relacionats, però només si tractes d'accedir a la seva informació (és a dir, només quan invoquis a ->getProducts()). La variable $products és un array de tots els objectes Product relacionats amb l'objecte Category indicat (i relacionat a través del valor category_id dels productes).

Uniendo registros relacionados

En els exemples anteriors, es realitzen dues consultes: la primera per a l'objecte original (Category per exemple) i la segona per el/els objectes relacionats (un array de Product per exemple).

Si saps per endavant que vas a necessitar les dades de tots els objectes, pots estalviar-te una consulta fent una unió o "join" en la primera consulta. Es pot realitzar utilitzant repositoris propis i afegint els mètodes que necessitem.

// 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;
    }
}

Pots utilitzar aquest mètode en el controlador per obtenir un objecte Product i el seu corresponent Category amb una sola consulta:

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

Repositori de classes personalitzat

Per desacoblar el codi, per poder crear tests fàcilment i per reutilitzar les consultes, és millor crear una classe pròpia de tipus repositori i incloure en ella tots els mètodes que necessitis per realitzar les consultes.

Per a això, afegeix en la informació de mapatge de l'entitat la ruta de la nova classe del teu repositori:

// src/Acme/StoreBundle/Entity/Product.php
namespace Acme\StoreBundle\Entity;
 
use Doctrine\ORM\Mapping as ORM;
 
/**
 * @ORM\Entity(repositoryClass="Acme\StoreBundle\Entity\ProductRepository")
 */
class Product
{
    //...
}

Doctrine pot generar la classe de repositori buida executant el mateix comando que vas utilitzar anteriorment per generar els getters i els setters:

$ php app/console doctrine:generate:entities Acme

Ara pots afegir un nou mètode anomenat findAllOrderedByName() a la classe del repositori recentment generat. Aquest mètode busca totes les entitats de tipus Product ordenades alfabèticament.

// src/Acme/StoreBundle/Entity/ProductRepository.php
namespace Acme\StoreBundle\Entity;
 
use Doctrine\ORM\EntityRepository;
 
class ProductRepository extends EntityRepository
{
    public function findAllOrderedByName()
    {
        return $this->getEntityManager()
            ->createQuery(
                'SELECT p FROM AcmeStoreBundle:Product p ORDER BY p.name ASC'
            )
            ->getResult();
    }
}

Ja pots utilitzar aquest nou mètode per realitzar la consulta dins d'un controlador de Symfony:

$em = $this->getDoctrine()->getManager();
$products = $em->getRepository('AcmeStoreBundle:Product')
               ->findAllOrderedByName();

Encara que utilitzis una classe repositori pròpia, encara pots fer ús dels mètodes de cerca predeterminats com find() i findAll().

Altres tipus de relacions

Per saber com es configuren i s'utilitzen altres tipus de relacions (ONE to ONE, MANY TO MANY) pots veure la documentació oficial de doctrine: Mapeig d'associacions.

Callbacks

En ocasions, és necessari realitzar una acció just abans o després d'inserir, actualitzar o eliminar una entitat. Aquest tipus d'accions es coneixen com lifecycle callbacks, ja que són mètodes (o callbacks) que s'executen en alguna de les diferents etapes per les quals passa una entitat (lifecycle)

/**
 * @ORM\Entity()
 * @ORM\HasLifecycleCallbacks()
 */
class Product
{
    // ...
}

Ara ja pots dir a Doctrine que executi un mètode en qualsevol dels esdeveniments disponibles durant el cicle de vida de l'entitat.

/**
 * @ORM\PrePersist
 */
public function setCreatedValue()
{
    $this->created = new \DateTime();
}

Ara, just abans de persistir l'entitat per primera vegada, Doctrine executa automàticament aquest mètode i establirà el camp created a la data actual.Consulta la documentació sobre esdeveniments de Doctrine per obtenir molta més informació.


Gernerar Entitats des de una BD creada

El primer pas cap a construir classes d'entitat a partir d'una base de dades és pemeten que Doctrine introspecti la base de dades i generiu la corresponent metadada. Les metadades son les diferentes informacions que s'afegeixen als arxius que associen les propietats d'una classe amb el corresponent camp de la taula.

$ php app/console doctrine:mapping:import --force AcmeBlogBundle xml

Aquesta comanda permet a Doctrine fer una introspecció de la base de dades i generar les metadades en format XML. Una vegada es generen les metadades, pots demanar a Doctrine que crei les classes (Entity) associades executant les següents comandes:

$ php app/console doctrine:mapping:convert annotation ./src
$ php app/console doctrine:generate:entities AcmeBlogBundle