Doctrine ResultSetMapping avec des exemples

Doctrine ORM fournit au développeur des moyens pratiques de récupérer des données. Il s'agit d'un DQL puissant pour travailler de manière orientée objet, et d'un générateur de requêtes pratique , simple et intuitif à utiliser. Ils couvrent la plupart des besoins, mais il devient parfois nécessaire d'utiliser des requêtes SQL optimisées ou spécifiques à un SGBD particulier. Pour travailler avec des résultats de requête dans du code, il est important de comprendre le fonctionnement du mappage dans Doctrine.



L'ORM Doctrine est basé sur le modèle Data Mapper , qui isole la représentation relationnelle de l'objet et convertit les données entre eux. L'un des composants clés de ce processus est l'objet ResultSetMapping , qui décrit comment transformer les résultats de requête du modèle relationnel en objet. La doctrine utilise toujours ResultSetMapping pour représenter les résultats de la requête, mais généralement cet objet est créé sur la base d'annotations ou de configurations yaml, xml, il reste caché aux yeux du développeur, donc tout le monde ne connaît pas ses capacités.

Pour des exemples de travail avec ResultSetMapping, j'utiliserai MySQL et la base de données exemple des employés .

Structure DB


Décrivons les entités Département, Employé, Salaire, avec lesquelles nous continuerons à travailler davantage.

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



Employé

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

}


Un salaire
<?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;

}



Résultat de l'entité


Commençons par un simple, sélectionnons tous les départements de la base et projetons-les sur le département:

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


La méthode addEntityResult indique sur quelle classe notre échantillon sera projeté et les méthodes addFieldResult indiquent un mappage entre les exemples de colonnes et les champs d'objet. La requête SQL passée à la méthode createNativeQuery sera transférée dans la base de données sous cette forme et Doctrine ne la modifiera en aucune façon.

Il convient de rappeler que l'entité a nécessairement un identifiant unique, qui doit être inclus dans fieldResult.

Résultat d'entité jointe


Nous sélectionnons les services dans lesquels il y a des employés embauchés après le début de 2000, avec ces employés.

        $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


Comme vous pouvez le voir, travailler directement avec l'objet ResultSetMapping est quelque peu compliqué et rend extrêmement détaillée la description de la comparaison de la sélection et des objets. En partie, Doctrine fournit un outil plus pratique - ResultSetMappingBuilder , qui est un wrapper sur RSM et ajoute des méthodes plus pratiques pour travailler avec la cartographie. Par exemple, la méthode generateSelectClause , qui vous permet de créer un paramètre pour la partie SELECT de la requête avec une description des champs requis pour la sélection. La demande précédente peut être réécrite sous une forme plus 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')
       ";

Il est à noter que si vous ne spécifiez pas tous les champs de l'objet créé, Doctrine renverra un objet partiel , dont l'utilisation peut être justifiée dans certains cas, mais leur comportement est dangereux et déconseillé . Au lieu de cela, ResultSetMappingBuilder vous permet de ne pas spécifier chaque champ de la classe finale, mais d'utiliser des entités décrites via des annotations (configurations yaml, xml). Nous réécrivons notre RSM à partir de la demande précédente, en tenant compte de ces méthodes:

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

Résultat scalaire


Une utilisation généralisée de l'entité décrite n'est pas justifiée, elle peut entraîner des problèmes de performances, car Doctrine doit créer de nombreux objets qui ne seront pas pleinement utilisés à l'avenir. Dans de tels cas, un outil de mappage de résultats scalaires, addScalarResult, est fourni .

Choisissez le salaire moyen pour chaque département:

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

Le premier argument de la méthode addScalarResult est le nom de la colonne dans le jeu de résultats et le second est la clé du tableau de résultats que Doctrine renverra. Le troisième paramètre est le type de la valeur de résultat. La valeur de retour sera toujours un tableau de tableaux.

Cartographie DTO


Mais travailler avec des tableaux, en particulier lorsqu'ils ont une structure complexe, n'est pas très pratique. Vous devez garder à l'esprit les noms des clés, les types de champs. Doctrine DQL a la possibilité d'utiliser des DTO simples dans les résultats de sélection, par exemple:

    SELECT NEW DepartmentSalary(d.dept_no, avg_salary) FROM

Qu'en est-il de Native Query et RSM? Doctrine ne fournit pas de capacités documentées pour créer de nouveaux DTO , mais en utilisant la propriété newObjectMappings, nous pouvons spécifier les objets dans lesquels nous voulons mapper les résultats de la sélection. Contrairement à Entity, ces objets ne seront pas contrôlés par UnitOfWork et ne doivent pas nécessairement se trouver dans les espaces de noms spécifiés dans la configuration.

Complétez RSM de l'exemple précédent:

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

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

La clé dans le tableau du champ newObjectMappings pointe vers la colonne résultante, mais sa valeur est un autre tableau qui décrit l'objet en cours de création. La clé className définit le nom de classe du nouvel objet, argIndex - l'ordre de l'argument dans le constructeur de l'objet, objIndex - l'ordre de l'objet, si nous voulons obtenir plusieurs objets de chaque ligne de la sélection.

Résultat méta


Le méta-résultat est utilisé pour obtenir des métadonnées de colonnes, telles que des clés étrangères ou des colonnes discriminantes. Malheureusement, je n'ai pas pu trouver d'exemple basé sur la base de données des employés, je dois donc me limiter à une description et des exemples de la documentation .

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

Dans ce cas, avec la méthode addMetaResult, nous disons à Doctine que la table des utilisateurs a une clé étrangère sur les adresses, mais au lieu de charger l'association en mémoire (chargement ardent), nous créons un proxy-object qui stocke l'identifiant d'entité, et lorsqu'il est accédé, il se charge elle de la base de données.

Les bases de données relationnelles classiques n'offrent pas de mécanismes d'héritage de table, tandis que l'héritage est répandu dans le modèle objet. Le résultat de notre sélection peut être projeté sur la hiérarchie des classes, selon un attribut, qui est la valeur de la colonne <discriminator_column> dans la sélection résultante. Dans ce cas, nous pouvons dire à RSM quelle colonne Doctrine doit déterminer la classe instanciée en utilisant la méthode setDiscriminatorColumn .

Conclusion


La doctrine est très riche en diverses fonctionnalités, y compris celles que tous les développeurs ne connaissent pas. Dans cet article, j'ai essayé d'introduire une compréhension du fonctionnement de l'un des composants clés d'ORM - ResultSetMapping en combinaison avec Native Query. L'utilisation de requêtes vraiment complexes et spécifiques à la plate-forme, tout en maintenant la disponibilité et l'intelligibilité des exemples, serait une tâche difficile, car l'accent est mis sur la compréhension du travail de ResultSetMapping. Après cela, vous pouvez l'utiliser dans des requêtes vraiment complexes pour vos bases de données.

All Articles