Doctrine Migrations Bundle o actualizaciones de datos incrementales

Uno de los problemas que surgen cuando usamos DataFixtures o datos de pruebas es que creamos muchas fixtures para que la base de datos tenga la informacion necesaria antes de lanzar la aplicacion a producción y que pueda funcionar todo a la perfección para aquellas tablas  que solo contienen informacion acotada para campos <select> por ejemplo. Hasta aquí todo bien, es el procedimiento normal.

Ahora bien, imagínate que al cabo del tiempo y una vez has desplegado tu aplicacion en producción, necesitas modificar un valor de un campo y no puedes borrar el esquema de la base de datos porque obviamente ya existen datos que no podemos perder de nuestros usuarios etc.

Aquí entra en juego el bundle de Doctrine para hacer “migraciones” incrementales y os voy a explicar brevemente como usarlo.

-Instalar bundle

>>php composer.phar require doctrine/doctrine-migrations-bundle

-Inicializar bundle(AppKernel.php)

new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),

-Crear migraciones

>> php app/console doctrine:migrations:generate

este comando lo que hace basicamente es crear una clase PHP dentro del directorio por defecto app/DoctrineMigrations/ cuyo nombre empieza por Version y añade la fecha y hora del momento de creacion. Ademas, la clase incluye 2 metodos up() y down() donde implementaremos el cambio(up) y la forma de deshacerlo(down) por si quisieramos volver atras. Podemos implementar la interfaz para hacer uso del contenedor de dependencias.

-Ejemplo:

class Version20160523164637 extends AbstractMigration implements ContainerAwareInterface
{
    /** @var ContainerInterface $container */
    private $container;

    /**
     * @param ContainerInterface $container
     */
    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    /**
     * @param Schema $schema
     */
    public function up(Schema $schema)
    {
        // this up() migration is auto-generated, please modify it to your needs
        $documentationTypeManager = $this->container->get('documentation_type_manager');
        $docTypeDni = $documentationTypeManager->findOneByName('DNI');

        if($docTypeDni){
            $docTypeDni->setName('DNI/NIF');
            $documentationTypeManager->persistAndFlush($docTypeDni);
        }
    }

    /**
     * @param Schema $schema
     */
    public function down(Schema $schema)
    {
        // this down() migration is auto-generated, please modify it to your needs
        $documentationTypeManager = $this->container->get('documentation_type_manager');
        $docTypeDni = $documentationTypeManager->findOneByName('DNI/NIF');

        if($docTypeDni){
            $docTypeDni->setName('DNI');
            $documentationTypeManager->persistAndFlush($docTypeDni);
        }
    }
}

Aqui podemos hacer cambios de 3 formas distintas:

  • Usando servicios que trabajan con objetos directamente(Managers)
  • Usando el QueryBuilder del EntityManager
  • Usando el metodo $this->addSql(‘UPDATE table SET x=1’); . Esto vendra bien si tenemos procedimientos almacenados en nuestra BD y queremos llamarlos.

-Ejecutar una migración

>> php app/console doctrine:migrations:migrate

Si todo es correcto, a continuación nos aparecerá lo siguiente:

Ejecucion de una migracion

Si investigamos en la base de datos, nos habrá creado una nueva tabla donde aparecen las versiones que tenemos y que hemos aplicado en nuestra aplicación.

-Ver el estado de las migraciones

>> php app/console doctrine:migrations:status

Con este comando podemos ver la información de las migraciones en nuestra aplicación así:

estado

-Desahacer una migración

>> php app/console doctrine:migrations:execute YYYYMMDDHHIISS –down

De esta forma deshacemos una migración cambiando YYYYMMDDHHIISS por el valor que pone en CurrentVersion del comando status. Por eso es muy importante implementar el método down() de la clase, para que sepa como deshacer una migración y que cambios tendría que aplicar para que este como estaba antes de hacer la migración.

Resumen

Con esta herramienta tendremos un histórico de aquellos cambios en los datos de nuestra esquema que han sufrido cambios a posteriori de instalar nuestros fixtures al desplegar nuestra aplicacion. También sabremos quien a cambiado que, ya que al crearse una nueva clase en nuestro proyecto, sabremos quien la ha creado y el motivo.

Documentación

Si os ha gustado, no dudéis en compartir este post.

Saludos SymfoyDevs ¡

Uso del operador IN en consultas DQL de Doctrine y otros operadores SQL

A menudo tengo la necesidad de usar el operador IN dentro del WHERE de una consulta DQL con Doctrine y casi siempre se me olvida como usarlo.

Para ello voy a explicar como usarlo a continuación con un ejemplo:

– Imaginamos que tenemos un array con los posibles valores que queremos seleccionar en la parte del where de la consulta tal que asi:

$filters = array(1, 2, 3, 4, 10, 15, 25);

-Ahora, hacemos nuestra consulta de la siguiente forma:

$users = $qb
    ->addSelect('user')
    ->from('UsuarioBundle:User', 'user')
    ->join('user.userGroup', 'userGroup')
    ->where($qb->expr()->in('userGroup.id', $filters))
    ->getQuery()
    ->getResult();

 De esta forma, usando el expr() del QueryBuilder podemos usar los operadores IN, NOT IN, BETWEEN, MAX, MIN, etc. Podeis ver toda la documentacion y los operadores disponibles en la documentacion oficial de Doctrine sobre el querybuilder:

-http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/query-builder.html#high-level-api-methods

Ale, espero que os sirva de ayuda.

Saludos Devs¡

Herencia entre entidades y consultas DQL relacionadas

herencia entre entidades y consultas DQL
Herencia entre entidades

Una de las características que tiene Doctrine es que se puede usar la herencia de clases para representar aquellas tablas que heredan de una tabla “base”.

Pongamos un ejemplo:

  • Tenemos una tabla Rol (“padre”) y de ella heredan 3 tipos de roles distintos(RolPlataforma, RolOrganizacion, RolEdicion), pero que comparten los mismos atributos que la tabla Rol “padre”, excepto que se cada “entidad” hija se relacionan con entidades diferentes. Aqui el diagrama UML que representa el caso:
  • umlAhora la parte de las entidades:
    • Entity Padre Role
  • /**
     * @ORM\Table(name="role")
     * @ORM\InheritanceType("JOINED")
     * @ORM\DiscriminatorColumn(name="discr", type="string")
     * @ORM\DiscriminatorMap({
     *     "role" = "Role",
     *     "role_edition" = "RoleEdition",
     *     "role_org" = "RoleOrganization",
     *     "role_plat" = "RolePlatform"
     * })
     */
    class Role 
    {
     const ROLE_EDITION = 'RoleEdition';
     const ROLE_ORGANIZATION = 'RoleOrganization';
     const ROLE_PLATFORM = 'RolePlatform';
     const ROLE = 'Role';
    
     /**
     * @ORM\Id
     * @ORM\Column(type="integer", nullable=false)
     * @ORM\GeneratedValue()
     */
     protected $id;
    
     /**
     * @ORM\Column(name="rol_borrado", type="boolean", nullable=false)
     */
     private $deleted = false;
    
     /**
     * @ORM\ManyToMany(targetEntity="efor\UsuarioBundle\Entity\User", mappedBy="roleUser")
     */
     private $user;
    
    //...
    
    //OTROS CAMPOS NO RELEVANTES PARA EL EJEMPLO
    //SETTERS Y GETTERS Y OTROS METODOS.
  • Con la anotacion estamos indicando que se va a hacer herencia de tablas y que los tipos de discriminador van a ser los que pone en el “DiscriminatorMap”. De esta entidad nunca se van a instanciar objetos, siempre se instanciaran objetos de las entidades hijas, por lo que podriamos definirla como Abstracta.
  • Entity descendiente RolePlatform
    • /**
       * @ORM\Table(name="rol_plataforma")
       * @ORM\Entity
       */
      class RolePlatform extends Role
      {
          protected $discr = self::ROLE_PLATFORM;
      
          /**
           * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Platform", inversedBy="rolePlatform")
           */
          private $platform;
      
          //otros campos
      }
  • Entity descendiente RoleOrganization
  • /**
     * @ORM\Table(name="rol_organizacion")
     * @ORM\Entity
     */
    class RoleOrganization extends Role
    {
        protected $discr = self::ROLE_ORGANIZATION;
    
        /**
         * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Organization", inversedBy="roleOrganization", cascade={"persist"})
         */
        private $organization;
    
        //otros campos
    }
  • Entity descendiente RoleEdition
  • /**
     * @ORM\Table(name="rol_edicion")
     * @ORM\Entity
     */
    class RoleEdition extends Role
    {
        protected $discr = self::ROLE_EDITION;
    
        /**
         * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Edition", inversedBy="roleEdition")
         */
        private $edition;
    
        //otros campos
    }

Si os fijas cada entidad hija tiene una relacion distinta a otra entidad, por eso necesitabamos usar la herencia entre entidades, porque podemos crear objetos que se relacionan con entidades distintas en funcion del tipo de relacion que queramos tener.

En la documentación de Doctrine explica los tipos de herencia entre entidades y algunas consideraciones a tener en cuenta cuando se usa la herencia. Os dejo aqui la documentacion:

-http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#class-table-inheritance

-http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#single-table-inheritance

-http://stackoverflow.com/questions/7504680/doctrine-2-how-to-write-a-dql-select-statement-to-search-some-but-not-all-the

Consultas DQL filtradas por el campo discriminador:

Una de las dudas que surgen al trabajar con la herencia en Doctrine es saber como puedes filtrar las consultas por el campo discriminatorio. En mi caso, necesitaba filtrar los resultados por aquellos objetos que eran de una determinada clase del discriminador(RoleOrganization) y despues de buscar, encontre que se puede hacer algo asi:

public function findUsersFilteredByRoleOrganization(User $loggedUser, Role $selectedRole)
{
    $qb = $this->getEntityManager()->createQueryBuilder();
    $usersResult = $qb
    ->select('user')
    ->from('UsuarioBundle:User', 'user')
    ->join('user.roleUser', 'roleUser')
    ->where('roleUser INSTANCE OF AppBundle:RoleOrganization')
    ->getQuery();

    return $usersResult->getResult();
}

Si os dais cuenta, hemos hecho uso del INSTANCE OF dentro del Where de la consulta DQL, la cual se traduce por esto en SQL:

SELECT t0_.*
INNER JOIN rol_usuario t2_ ON t0_.id = t2_.user_id
INNER JOIN rol t1_ ON t1_.id = t2_.role_id
LEFT JOIN rol_edition t3_ ON t1_.id = t3_.id
LEFT JOIN rol_org t4_ ON t1_.id = t4_.id
LEFT JOIN rol_plataforma t5_ ON t1_.id = t5_.id
WHERE t1_.discr IN (‘role_org’)

De esta forma solo obtenemos los objetos del tipo “RoleOrganization” que queremos.

Espero que os sirve de ayuda, ya que la documentacion de Doctrine no es muy explicita en este aspecto y no lo explica muy bien que digamos.

Recordad compartir este articulo para que llegue al maximo de amigos de Symfony.

Saludos ¡¡

 

 

Mapeo de entidades con distintas bases de datos

En un proyecto que estoy trabajando, ocurre que existen entidades que representan tablas de distintas fuentes de datos (mySql, PostgreSQL, etc.) y si no se configuran distintas entities ocurre que al actualizar el schema de la BD mete nuevas tablas donde no toca y otras cosas relacionadas.

Esto se puede solucionar en Symfony, para que cada conjunto de entidades que pertenecen a distintas bases de datos se mapeen como toca de forma separada por bundles.

Antes de nada, debemos tener en cuenta de crear un bundle con las entidades de cada origen de datos por separado. Por ejemplo, AppBundle tendra las entidades de la BD de MySQL, y CountryBundle tendra las entidades de la BD de PostgreSQL, por ejemplo.

Para ello necesitamos configurar primero las conexiones de las BD en el config.yml:

doctrine:
    dbal:
        default_connection: default
        connections:
            default:
                driver:   "%database_driver%"
                host:     "%database_host%"
                port:     "%database_port%"
                dbname:   "%database_name%"
                user:     "%database_user%"
                password: "%database_password%"
                charset:  UTF8
            my_sql:
                driver:   "%database_driver_mysql%"
                host:     "%database_host_mysql%"
                port:     "%database_port_mysql%"
                dbname:   "%database_name_mysql%"
                user:     "%database_user_mysql%"
                password: "%database_password_mysql%"
                charset:  UTF8
                password: "%database_password_moodle%"
                charset:  UTF8

Despues mas abajo necesitamos especificar los entity managers a que bundle pueden atacar:

orm:
    default_entity_manager: default
    entity_managers:
        default:
            connection: default
            mappings:
                UserBundle:  ~
                AppBundle:  ~
                CoreBundle:  ~
        country:
            connection: my_sql
            mappings:
                CountryBundle:  ~

De esta forma, al usar los comandos de doctrine para el schema o cualquier uso con el entity manager podremos especificar que conexion queremos usar, por ejemplo:

> php app/console doctrine:schema:update –force (por defecto usa la default)

php app/console doctrine:schema:update –force –em=my_sql (usará la conexion de MySQL)

Si neccesitamos usarlo en un controlador o servicio podremos usarlo asi:

$this->em = $this->container->get('doctrine')->getManager('my_sql');
$this->repository = $this->em->getRepository('CountryBundle:Country');

De esa forma solo usará la base de datos o entity manager que le digamos.

Eso es todo.