Doctrine ResultSetMapping на примерах

от автора

Doctrine ORM предоставляет разработчику удобные средства выборки данных. Это и мощный DQL для работы в объектно-ориентированном ключе, и удобный Query Builder, простой и понятный в использовании. Они покрывают большую часть потребностей, но иногда возникает необходимость использовать SQL запросы, оптимизированные или специфичные для конкретной СУБД. Для работы с результатами запросов в коде важно понимание того, как работает маппинг в Doctrine.



В основе Doctrine ORM лежит паттерн Data Mapper, изолирующий реляционное представление от объектного, и конвертирующий данные между ними. Одним из ключевых компонентов этого процесса является объект ResultSetMapping, с помощью которого описывается, как именно преобразовывать результаты запроса из реляционной модели в объектную. Doctrine всегда использует ResultSetMapping для представления результатов запроса, но обычно этот объект создается на основе аннотаций или yaml, xml конфигов, остается скрыт от глаз разработчика, потому о его возможностях знают далеко не все.

Для примеров работы с ResultSetMapping я буду использовать MySQL и employees sample database.

Структура БД

Опишем сущности Department, Employee, Salary, c которыми и будем продолжать работу далее.

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

Employee

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

}

Salary

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

}

Entity result

Начнем с простого, выберем все департаменты из базы и спроецируем их на Department:

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

Метод addEntityResult указывает, на какой класс будет проецироваться наша выборка, а методы addFieldResult указывают на сопоставление столбцов выборки и полей объектов. SQL запрос, передаваемый в метод createNativeQuery будет передан в БД именно в таком виде и Doctrine не будет никаким образом его изменять.

Стоит помнить, что Entity обязательно обладает уникальным идентификатором, который должен быть включен в fieldResult.

Joined Entity result

Выберем департаменты, в которых присутствуют сотрудники, нанятые после начала 2000 года, вместе с этими сотрудниками.

        $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

Как видно, работа с непосредственно объектом ResultSetMapping несколько сложна и заставляет крайне детально описывать сопоставление выборки и объектов. К частью, Doctrine предоставляет более удобный инструмент — ResultSetMappingBuilder, являющийся оберткой над RSM и добавляющий больше удобных методов для работы с маппингом. Например, метод generateSelectClause, позволяющий создать параметр для SELECT части запроса с описанием необходимых для выборки полей. Предыдущий запрос можно переписать в более простом виде.

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

Стоит обратить внимание, что если не указать все поля создаваемого объекта, то Doctrine вернет partial object, использование которых может быть оправдано в некоторых случаях, но их поведение опасно и не рекомендуется. Вместо этого, ResultSetMappingBuilder позволяет не указывать каждое поле итогового класса, а использовать описанные через аннотации (yaml, xml конфигурации) сущности. Перепишем наш RSM из предыдущего запроса с учетом этих методов:

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

Scalar result

Повсеместное использование описанных entity не оправдано, может привести к проблемам с производительностью, так как Doctrine требуется создать множество объектов, которые в дальнейшем не будут использоваться полноценно. Для таких случаев предоставляется инструмент маппинга скалярных результатов — addScalarResult.

Выберем среднюю зарплату по каждому департаменту:

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

Первым аргументом метода addScalarResult служит имя колонки в результирующей выборке, вторым — ключ массива результатов, которые вернет Doctrine. Третий параметр — тип значения результата. Возвращаемым значением всегда будет массив массивов.

Маппинг на DTO

Но работать с массивами, особенно когда те имеют сложную структуру не очень удобно. Нужно держать в голове названия ключей, типы полей. В Doctrine DQL есть возможность использовать в результатах выборки простые DTO, например:

    SELECT NEW DepartmentSalary(d.dept_no, avg_salary) FROM …

А что же в случае Native Query и RSM? Doctrine не предоставляет документированных возможностей для создания новых DTO, но путем использования свойства newObjectMappings мы можем указать объекты, в которые хотим маппить результаты выборки. В отличие от Entity, эти объекты не будут контролироваться UnitOfWork’ом, и не обязаны находиться в неймспейсах, указанных в конфигурации.

Дополним RSM из предыдущего примера:

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

Ключ в массиве поля newObjectMappings указывает на столбец результирующий выборки, значением же его является другой массив, в котором описывается создаваемый объект. Ключ className определяет имя класса нового объекта, argIndex — порядок аргумента в конструкторе объекта, objIndex — порядок объекта, если мы хотим получить несколько объектов из каждой строки выборки.

Meta result

Meta result используется для получения мета-данных колонок, таких как foreign keys или discriminator columns. К сожалению, я не смог придумать пример на основе employees базы данных, потому придется ограничиться описанием и примерами из документации.

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

В данном случае, методом addMetaResult мы указываем Doctine, что таблица users имеет внешний ключ на addresses, но вместо того, чтобы загружать ассоциацию в память (eager load), мы создаем proxy-object, который хранит идентификатор сущности, и при обращении к нему загрузит ее из БД.

Классические реляционные базы данных не предлагают механизмов наследования таблиц, в то же время в объектной модели наследование широко распространено. Результат нашей выборки может проецироваться на иерархию классов, по некоему признаку, являющемуся значением колонки <discriminator_column> в результирующей выборке. В этом случае, мы можем указать RSM, по какой колонке Doctrine должна определять инстанцируемый класс посредством метода setDiscriminatorColumn.

Заключение

Doctrine весьма богата различными возможностями, в том числе и такими, о которых знают не все разработчики. В данном посте я постарался ознакомить с пониманием работы одного из ключевых компонентов ORM — ResultSetMapping в комбинации с Native Query. Использовать действительно сложные и платформо-зависимые запросы, сохранив доступность и понятность примеров было бы сложной задачей, потому акцент сделан именно на понимание работы ResultSetMapping. После этого вы сможете использовать его в действительно сложных запросах к вашим базам данных.

ссылка на оригинал статьи https://habr.com/ru/post/496166/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *