Doctrine ResultSetMapping con ejemplos

Doctrine ORM proporciona al desarrollador medios convenientes para obtener datos. Este es un potente DQL para trabajar de forma orientada a objetos y un conveniente generador de consultas , simple e intuitivo de usar. Cubren la mayoría de las necesidades, pero a veces se hace necesario usar consultas SQL optimizadas o específicas para un DBMS en particular. Para trabajar con resultados de consultas en código, es importante comprender cómo funciona el mapeo en Doctrine.



Doctrine ORM se basa en el patrón Data Mapper , que aísla la representación relacional de la representación del objeto y convierte los datos entre ellos. Uno de los componentes clave de este proceso es el objeto ResultSetMapping , que describe cómo transformar los resultados de la consulta del modelo relacional al objeto uno. Doctrine siempre usa ResultSetMapping para representar los resultados de la consulta, pero generalmente este objeto se crea sobre la base de anotaciones o configuraciones yaml, xml, permanece oculto a los ojos del desarrollador, por lo tanto, no todos conocen sus capacidades.

Para obtener ejemplos de cómo trabajar con ResultSetMapping, utilizaré MySQL y la base de datos de muestra de los empleados .

Estructura DB


Describamos las entidades Departamento, Empleado, Salario, con las cuales continuaremos trabajando más.

Departamento
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Departments
 *
 * @ORM\Table(name="departments", uniqueConstraints={@ORM\UniqueConstraint(name="dept_name", columns={"dept_name"})})
 * @ORM\Entity
 */
class Department
{
    /**
     * @var string
     *
     * @ORM\Column(name="dept_no", type="string", length=4, nullable=false, options={"fixed"=true})
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $deptNo;

    /**
     * @var string
     *
     * @ORM\Column(name="dept_name", type="string", length=40, nullable=false)
     */
    private $deptName;

    /**
     * @var \Doctrine\Common\Collections\Collection
     *
     * @ORM\ManyToMany(targetEntity="Employee", mappedBy="deptNo")
     */
    private $empNo;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->empNo = new \Doctrine\Common\Collections\ArrayCollection();
    }
}



Empleado

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* Employees
*
* ORM\Table(name=«employees»)
* ORM\Entity
*/
class Employee
{
/**
* var int
*
* ORM\Column(name=«emp_no», type=«integer», nullable=false)
* ORM\Id
* ORM\GeneratedValue(strategy=«IDENTITY»)
*/
private $empNo;

/**
* var \DateTime
*
* ORM\Column(name=«birth_date», type=«date», nullable=false)
*/
private $birthDate;

/**
* var string
*
* ORM\Column(name=«first_name», type=«string», length=14, nullable=false)
*/
private $firstName;

/**
* var string
*
* ORM\Column(name=«last_name», type=«string», length=16, nullable=false)
*/
private $lastName;

/**
* var string
*
* ORM\Column(name=«gender», type=«string», length=0, nullable=false)
*/
private $gender;

/**
* var \DateTime
*
* ORM\Column(name=«hire_date», type=«date», nullable=false)
*/
private $hireDate;

/**
* var \Doctrine\Common\Collections\Collection
*
* ORM\ManyToMany(targetEntity=«Department», inversedBy=«empNo»)
* ORM\JoinTable(name=«dept_manager»,
* joinColumns={
* ORM\JoinColumn(name=«emp_no», referencedColumnName=«emp_no»)
* },
* inverseJoinColumns={
* ORM\JoinColumn(name=«dept_no», referencedColumnName=«dept_no»)
* }
* )
*/
private $deptNo;

/**
* Constructor
*/
public function __construct()
{
$this->deptNo = new \Doctrine\Common\Collections\ArrayCollection();
}

}


Salario
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* Salaries
*
* ORM\Table(name=«salaries», indexes={@ORM\Index(name=«IDX_E6EEB84BA2F57F47», columns={«emp_no»})})
* ORM\Entity
*/
class Salary
{
/**
* var \DateTime
*
* ORM\Column(name=«from_date», type=«date», nullable=false)
* ORM\Id
* ORM\GeneratedValue(strategy=«NONE»)
*/
private $fromDate;

/**
* var int
*
* ORM\Column(name=«salary», type=«integer», nullable=false)
*/
private $salary;

/**
* var \DateTime
*
* ORM\Column(name=«to_date», type=«date», nullable=false)
*/
private $toDate;

/**
* var Employee
*
* ORM\Id
* ORM\OneToOne(targetEntity=«Employee»)
* ORM\JoinColumns({
* ORM\JoinColumn(name=«emp_no», referencedColumnName=«emp_no»)
* })
*/
private $empNo;

/**
* var Employee
*
*/
private $employee;

}



Resultado de la entidad


Comencemos con uno simple, seleccione todos los departamentos de la base y proyecte en el Departamento:

        $rsm = new Query\ResultSetMapping();
        $rsm->addEntityResult(Department::class, 'd');
        $rsm->addFieldResult('d', 'dept_no', 'deptNo');
        $rsm->addFieldResult('d', 'dept_name', 'deptName');

        $sql = 'SELECT * FROM departments';

        $query = $this->entityManager->createNativeQuery($sql, $rsm);

        $result = $query->getResult();


El método addEntityResult indica en qué clase se proyectará nuestra muestra, y los métodos addFieldResult indican una asignación entre columnas de muestra y campos de objeto. La consulta SQL pasada al método createNativeQuery se transferirá a la base de datos en este formulario y Doctrine no la modificará de ninguna manera.

Vale la pena recordar que Entity necesariamente tiene un identificador único, que debe incluirse en fieldResult.

Resultado de la entidad unida


Seleccionamos los departamentos en los que hay empleados contratados después de principios de 2000, junto con estos empleados.

        $rsm = new Query\ResultSetMapping();
        $rsm->addEntityResult(Department::class, 'd');
        $rsm->addFieldResult('d', 'dept_no', 'deptNo');
        $rsm->addFieldResult('d', 'dept_name', 'deptName');

        $rsm->addJoinedEntityResult(Employee::class, 'e', 'd', 'empNo');
        $rsm->addFieldResult('e', 'first_name', 'firstName');
        $rsm->addFieldResult('e', 'last_name', 'lastName');
        $rsm->addFieldResult('e', 'birth_date', 'birthDate');
        $rsm->addFieldResult('e', 'gender', 'gender');
        $rsm->addFieldResult('e', 'hire_date', 'hireDate');

        $sql = "
        SELECT *
            FROM departments d
            JOIN dept_emp ON d.dept_no = dept_emp.dept_no
            JOIN employees e on dept_emp.emp_no = e.emp_no
        WHERE e.hire_date > DATE ('1999-12-31')
       ";

        $query = $this->entityManager->createNativeQuery($sql, $rsm);

        $result = $query->getResult();

ResultSetMappingBuilder


Como puede ver, trabajar directamente con el objeto ResultSetMapping es algo complicado y lo hace extremadamente detallado para describir la comparación de la selección y los objetos. Además, Doctrine proporciona una herramienta más conveniente: ResultSetMappingBuilder , que es un contenedor en RSM y agrega métodos más convenientes para trabajar con el mapeo. Por ejemplo, el método generateSelectClause , que le permite crear un parámetro para la parte SELECT de la consulta con una descripción de los campos necesarios para la selección. La solicitud anterior puede reescribirse en una forma más simple.

        $sql = "
        SELECT {$rsm->generateSelectClause()}
            FROM departments d
            JOIN dept_emp ON d.dept_no = dept_emp.dept_no
            JOIN employees e on dept_emp.emp_no = e.emp_no
        WHERE e.hire_date > DATE ('1999-12-31')
       ";

Vale la pena señalar que si no especifica todos los campos del objeto creado, Doctrine devolverá un objeto parcial , cuyo uso puede estar justificado en algunos casos, pero su comportamiento es peligroso y no se recomienda . En cambio, ResultSetMappingBuilder le permite no especificar cada campo de la clase final, sino utilizar entidades descritas a través de anotaciones (configuraciones yaml, xml). Reescribimos nuestro RSM de la solicitud anterior, teniendo en cuenta estos métodos:

        $rsm = new Query\ResultSetMappingBuilder($this->entityManager);
        $rsm->addRootEntityFromClassMetadata(Department::class, 'd');
        $rsm->addJoinedEntityFromClassMetadata(Employee::class, 'e', 'd', 'empNo');

Resultado escalar


El uso generalizado de la entidad descrita no está justificado, puede conducir a problemas de rendimiento, ya que Doctrine necesita crear muchos objetos que no se utilizarán por completo en el futuro. Para tales casos, se proporciona una herramienta de mapeo de resultados escalares, addScalarResult .

Elija el salario promedio para cada departamento:

        $rsm = new ResultSetMappingBuilder($this->entityManager);

        $rsm->addScalarResult('dept_name', 'department', 'string');
        $rsm->addScalarResult('avg_salary', 'salary', 'integer');

        $sql = "
            SELECT d.dept_name, AVG(s.salary) AS avg_salary
            FROM departments d
            JOIN dept_emp de on d.dept_no = de.dept_no
            JOIN employees e on de.emp_no = e.emp_no
            JOIN salaries s on e.emp_no = s.emp_no
            GROUP BY d.dept_name
        ";

        $query = $this->entityManager->createNativeQuery($sql, $rsm);

        $result = $query->getResult();

El primer argumento para el método addScalarResult es el nombre de la columna en el conjunto de resultados, y el segundo es la clave de la matriz de resultados que Doctrine devolverá. El tercer parámetro es el tipo del valor del resultado. El valor de retorno siempre será una matriz de matrices.

Mapeo DTO


Pero trabajar con matrices, especialmente cuando tienen una estructura compleja, no es muy conveniente. Debe tener en cuenta los nombres de las claves, los tipos de campo. Doctrine DQL tiene la capacidad de usar DTO simples en los resultados de la selección, por ejemplo:

    SELECT NEW DepartmentSalary(d.dept_no, avg_salary) FROM

¿Qué pasa con Native Query y RSM? Doctrine no proporciona capacidades documentadas para crear nuevos DTO , pero al usar la propiedad newObjectMappings, podemos especificar los objetos a los que queremos asignar los resultados de la selección. A diferencia de Entity, UnitOfWork no controlará estos objetos y no es necesario que estén en los espacios de nombres especificados en la configuración.

Complemente RSM del ejemplo anterior:

        $rsm->newObjectMappings['dept_name'] = [
            'className' => DepartmentSalary::class,
            'argIndex' => 0,
            'objIndex' => 0,
        ];

        $rsm->newObjectMappings['avg_salary'] = [
            'className' => DepartmentSalary::class,
            'argIndex' => 1,
            'objIndex' => 0,
        ];

La clave en la matriz del campo newObjectMappings apunta a la columna resultante, pero su valor es otra matriz que describe el objeto que se está creando. La clave className define el nombre de clase del nuevo objeto, argIndex , el orden del argumento en el constructor del objeto, objIndex , el orden del objeto, si queremos obtener varios objetos de cada fila de la selección.

Meta resultado


El resultado meta se usa para obtener metadatos de columnas, como claves foráneas o columnas discriminadoras. Desafortunadamente, no pude encontrar un ejemplo basado en la base de datos de empleados, así que tengo que limitarme a una descripción y ejemplos de la documentación .

        $rsm = new ResultSetMapping;
        $rsm->addEntityResult('User', 'u');
        $rsm->addFieldResult('u', 'id', 'id');
        $rsm->addFieldResult('u', 'name', 'name');
        $rsm->addMetaResult('u', 'address_id', 'address_id');

        $query = $this->_em->createNativeQuery('SELECT id, name, address_id FROM users WHERE name = ?', $rsm);

En este caso, utilizando el método addMetaResult, le decimos a Doctine que la tabla de usuarios tiene una clave externa en las direcciones, pero en lugar de cargar la asociación en la memoria (carga ansiosa), creamos un objeto proxy que almacena el identificador de la entidad y, cuando se accede, carga ella de la base de datos.

Las bases de datos relacionales clásicas no ofrecen mecanismos de herencia de tablas, mientras que la herencia está muy extendida en el modelo de objetos. El resultado de nuestra selección se puede proyectar en la jerarquía de clases, de acuerdo con algún atributo, que es el valor de la columna <discriminator_column> en la selección resultante. En este caso, podemos decirle a RSM qué columna Doctrine debería determinar la clase instanciada mediante el método setDiscriminatorColumn .

Conclusión


Doctrine es muy rico en varias características, incluidas aquellas que no todos los desarrolladores conocen. En esta publicación, intenté presentar una comprensión de la operación de uno de los componentes clave de ORM: ResultSetMapping en combinación con Native Query. Usar consultas verdaderamente complejas y específicas de la plataforma mientras se mantiene la disponibilidad y la comprensión de los ejemplos sería una tarea difícil, porque se hace hincapié en comprender el trabajo de ResultSetMapping. Después de eso, puede usarlo en consultas realmente complejas para sus bases de datos.

All Articles