Doctrine ResultSetMapping com exemplos

O Doctrine ORM fornece ao desenvolvedor meios convenientes de buscar dados. Este é um DQL poderoso para trabalhar de maneira orientada a objetos e um conveniente Query Builder , simples e intuitivo de usar. Eles cobrem a maioria das necessidades, mas às vezes se torna necessário o uso de consultas SQL otimizadas ou específicas para um DBMS específico. Para trabalhar com os resultados da consulta no código, é importante entender como o mapeamento no Doctrine funciona.



O Doctrine ORM é baseado no padrão Data Mapper , que isola a representação relacional do objeto e converte os dados entre eles. Um dos principais componentes desse processo é o objeto ResultSetMapping , que descreve como transformar os resultados da consulta do modelo relacional no objeto. A doutrina sempre usa ResultSetMapping para apresentar resultados da consulta, mas geralmente esse objeto é criado com base em anotações ou yaml, xml configs, permanece oculto aos olhos do desenvolvedor, portanto, nem todo mundo sabe sobre seus recursos.

Para exemplos de trabalho com ResultSetMapping, usarei o banco de dados de exemplo MySQL e funcionários .

Estrutura de banco de dados


Vamos descrever as entidades Departamento, Funcionário, Salário, com as quais continuaremos a trabalhar mais.

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();
    }
}



Empregado

<?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();
}

}


Salário
<?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 da entidade


Vamos começar com um simples, selecionar todos os departamentos da base e projetá-los no 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();


O método addEntityResult indica em qual classe nossa amostra será projetada e os métodos addFieldResult indicam um mapeamento entre as colunas da amostra e os campos do objeto. A consulta SQL passada para o método createNativeQuery será transferida para o banco de dados neste formulário e o Doctrine não a modificará de forma alguma.

Vale lembrar que a Entidade necessariamente possui um identificador exclusivo, que deve ser incluído no fieldResult.

Resultado da Entidade Associada


Selecionamos os departamentos em que existem funcionários contratados após o início de 2000, juntamente com esses funcionários.

        $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 você pode ver, trabalhar diretamente com o objeto ResultSetMapping é um pouco complicado e torna extremamente detalhado descrever a comparação da seleção e dos objetos. Além disso, o Doctrine fornece uma ferramenta mais conveniente - ResultSetMappingBuilder , que é um wrapper no RSM e adiciona métodos mais convenientes para trabalhar com o mapeamento. Por exemplo, o método generateSelectClause , que permite criar um parâmetro para a parte SELECT da consulta com uma descrição dos campos necessários para a seleção. A solicitação anterior pode ser reescrita de uma forma mais simples.

        $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 ressaltar que, se você não especificar todos os campos do objeto criado, o Doctrine retornará um objeto parcial , cuja utilização pode ser justificada em alguns casos, mas o comportamento deles é perigoso e não é recomendado . Em vez disso, ResultSetMappingBuilder permite que você não especifique cada campo da classe final, mas use entidades descritas por meio de anotações (configurações yaml, xml). Reescrevemos nosso RSM da solicitação anterior, levando em consideração os seguintes métodos:

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

Resultado escalar


O uso generalizado da entidade descrita não é justificado, pois pode levar a problemas de desempenho, pois o Doctrine precisa criar muitos objetos que não serão usados ​​totalmente no futuro. Para esses casos , é fornecida uma ferramenta de mapeamento de resultados escalar, addScalarResult .

Escolha o salário médio 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();

O primeiro argumento para o método addScalarResult é o nome da coluna no conjunto de resultados e o segundo é a chave para a matriz de resultados que o Doctrine retornará. O terceiro parâmetro é o tipo do valor do resultado. O valor de retorno sempre será uma matriz de matrizes.

Mapeamento de DTO


Mas trabalhar com matrizes, especialmente quando elas têm uma estrutura complexa, não é muito conveniente. Você precisa ter em mente os nomes das chaves, os tipos de campo. O Doctrine DQL tem a capacidade de usar DTOs simples nos resultados da seleção, por exemplo:

    SELECT NEW DepartmentSalary(d.dept_no, avg_salary) FROM

E quanto à consulta nativa e ao RSM? O Doctrine não fornece recursos documentados para criar novos DTOs , mas usando a propriedade newObjectMappings, podemos especificar os objetos nos quais queremos mapear os resultados da seleção. Diferentemente da Entidade, esses objetos não serão controlados pelo UnitOfWork e não precisam estar nos espaços para nome especificados na configuração.

Complemente o RSM do exemplo anterior:

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

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

A chave na matriz do campo newObjectMappings aponta para a coluna resultante, mas seu valor é outra matriz que descreve o objeto que está sendo criado. A chave className determina o nome da classe do novo objeto, argIndex - a ordem do argumento no construtor do objeto, objIndex - a ordem do objeto, se queremos obter vários objetos de cada linha da seleção.

Meta resultado


O meta resultado é usado para obter metadados de colunas, como chaves estrangeiras ou colunas discriminadoras. Infelizmente, não consegui encontrar um exemplo com base no banco de dados de funcionários, por isso tenho que me limitar a uma descrição e exemplos da documentação .

        $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);

Nesse caso, com o método addMetaResult, informamos ao Doctine que a tabela de usuários possui uma chave estrangeira nos endereços, mas, em vez de carregar a associação na memória (carregamento rápido), criamos um objeto proxy que armazena o identificador da entidade e, quando é acessado, ele carrega ela do banco de dados.

Os bancos de dados relacionais clássicos não oferecem mecanismos de herança de tabela, enquanto a herança é generalizada no modelo de objeto. O resultado da nossa seleção pode ser projetado na hierarquia de classes, de acordo com algum atributo, que é o valor da coluna <discriminator_column> na seleção resultante. Nesse caso, podemos dizer ao RSM qual coluna o Doctrine deve determinar a classe instanciada usando o método setDiscriminatorColumn .

Conclusão


A doutrina é muito rica em vários recursos, incluindo aqueles que nem todos os desenvolvedores conhecem. Neste post, tentei apresentar um entendimento da operação de um dos principais componentes do ORM - ResultSetMapping em combinação com a Consulta nativa. Usar consultas verdadeiramente complexas e específicas da plataforma, mantendo a disponibilidade e a compreensibilidade dos exemplos, seria uma tarefa difícil, porque a ênfase é colocada no entendimento do trabalho do ResultSetMapping. Depois disso, você pode usá-lo em consultas realmente complexas para seus bancos de dados.

All Articles