教义ResultSetMapping与示例

Doctrine ORM为开发人员提供了获取数据的便捷方法。这是一个功能强大的DQL,可以以面向对象的方式工作,它是一个方便的Query Builder,使用起来简单直观。它们满足了大多数需求,但是有时有必要使用针对特定DBMS进行了优化或特定的SQL查询。要使用代码中的查询结果,了解Doctrine中的映射如何工作非常重要。



原则ORM基于数据映射器模式,该模式将关系表示与对象隔离,并在对象之间转换数据。此过程的关键组成部分之一是ResultSetMapping对象,该对象描述了如何将查询结果从关系模型转换为对象。Doctrine始终使用ResultSetMapping表示查询结果,但是通常该对象是基于注释或yaml,xml配置创建的,它对开发人员而言是隐藏的,因此并不是每个人都知道其功能。

对于使用ResultSetMapping的示例,我将使用MySQL和employee示例数据库

数据库结构


让我们描述实体部门,雇员,薪水,我们将继续与之合作。

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



雇员

<?php

名称空间App \ Entity;

使用Doctrine \ ORM \ Mapping作为ORM;

/ **
*员工
*
*ORM\表(名称=“员工”)
*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();
}

}


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

}



实体结果


让我们从一个简单的开始,从基础中选择所有部门并将它们投影到部门上:

        $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方法指示样本列与对象字段之间的映射。传递给createNativeQuery方法的SQL查询将以这种形式传输到数据库,Doctrine不会以任何方式对其进行修改。

值得记住的是,Entity必须具有唯一的标识符,该标识符必须包含在fieldResult中。

合并实体结果


我们选择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将返回部分对象,在某些情况下可以合理使用它,但是它们的行为很危险,因此不建议这样做相反,使用ResultSetMappingBuilder,您无需指定最终类的每个字段,而可以使用通过注释(yaml,xml配置)描述的实体。考虑到以下方法,我们从上一个请求重写了我们的RSM:

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

标量结果


所描述的实体的广泛使用是不合理的,这可能会导致性能问题,因为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映射


但是使用数组,特别是当它们具有复杂的结构时,并不是很方便。您需要牢记键的名称,字段类型。教义DQL可以在选择结果中使用简单的DTO,例如:

    SELECT NEW DepartmentSalary(d.dept_no, avg_salary) FROM

原生查询和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-对象的顺序。

元结果


元结果用于获取列的元数据,例如外键或鉴别符列。不幸的是,我无法基于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用户表在地址上有一个外键,但是我们创建了一个代理对象来存储实体标识符,并在访问时加载,而不是将关联加载到内存中(急于加载)。她从数据库中。

经典的关系数据库不提供表继承机制,而继承在对象模型中却很普遍。根据某些属性,我们的选择结果可以投影到类的层次结构上,该属性是结果选择中列<discriminator_column>的值。在这种情况下,我们可以使用setDiscriminatorColumn方法告诉RSM Doctrine哪个列应​​确定实例化的类

结论


Doctrine具有非常丰富的各种功能,包括并非所有开发人员都知道的功能。在这篇文章中,我试图介绍对ORM关键组件之一的操作-ResultSetMapping与本机查询的结合的理解。在保持示例的可用性和可理解性的同时,使用真正复杂且特定于平台的查询将是一项艰巨的任务,因为重点在于理解ResultSetMapping的工作。之后,您可以在非常复杂的数据库查询中使用它。

All Articles