Doctrine ResultSetMapping mit Beispielen

Doctrine ORM bietet dem Entwickler eine bequeme Möglichkeit, Daten abzurufen. Dies ist eine leistungsstarke DQL für die objektorientierte Arbeit und ein praktischer Query Builder , der einfach und intuitiv zu bedienen ist. Sie decken die meisten Anforderungen ab, aber manchmal müssen SQL-Abfragen verwendet werden, die für ein bestimmtes DBMS optimiert oder spezifisch sind. Um mit Abfrageergebnissen in Code arbeiten zu können, ist es wichtig zu verstehen, wie die Zuordnung in Doctrine funktioniert.



Das Doctrine ORM basiert auf dem Data Mapper- Muster , das die relationale Darstellung vom Objekt isoliert und die Daten zwischen ihnen konvertiert. Eine der Schlüsselkomponenten dieses Prozesses ist das ResultSetMapping- Objekt , das beschreibt, wie Abfrageergebnisse aus dem relationalen Modell in das Objektmodell umgewandelt werden. Doctrine verwendet immer ResultSetMapping, um die Abfrageergebnisse darzustellen. In der Regel wird dieses Objekt jedoch auf der Grundlage von Anmerkungen oder yaml- und xml-Konfigurationen erstellt. Es bleibt vor den Augen des Entwicklers verborgen, sodass nicht jeder über seine Funktionen Bescheid weiß.

Für Beispiele für die Arbeit mit ResultSetMapping verwende ich die Beispieldatenbank von MySQL und Mitarbeitern .

DB-Struktur


Beschreiben wir die Entitäten Abteilung, Mitarbeiter, Gehalt, mit denen wir weiter arbeiten werden.

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



Mitarbeiter

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

}


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

}



Entitätsergebnis


Beginnen wir mit einer einfachen, wählen Sie alle Abteilungen aus der Basis aus und projizieren Sie sie auf die Abteilung:

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


Die addEntityResult- Methode gibt an, auf welche Klasse unser Beispiel projiziert wird, und die addFieldResult- Methoden geben eine Zuordnung zwischen Beispielspalten und Objektfeldern an. Die an die Methode createNativeQuery übergebene SQL-Abfrage wird in dieser Form an die Datenbank übertragen, und Doctrine ändert sie in keiner Weise.

Es ist zu beachten, dass Entity notwendigerweise eine eindeutige Kennung hat, die in fieldResult enthalten sein muss.

Ergebnis der verbundenen Entität


Wir wählen zusammen mit diesen Mitarbeitern die Abteilungen aus, in denen nach Anfang 2000 Mitarbeiter eingestellt werden.

        $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


Wie Sie sehen können, ist die direkte Arbeit mit dem ResultSetMapping-Objekt etwas kompliziert und macht es äußerst detailliert, den Vergleich der Auswahl und der Objekte zu beschreiben. Darüber hinaus bietet Doctrine ein komfortableres Tool - ResultSetMappingBuilder , ein Wrapper für RSM, der bequemere Methoden für die Arbeit mit Mapping hinzufügt. Zum Beispiel die generateSelectClause- Methode , mit der Sie einen Parameter für den SELECT-Teil der Abfrage mit einer Beschreibung der für die Auswahl erforderlichen Felder erstellen können. Die vorherige Anfrage kann in einer einfacheren Form umgeschrieben werden.

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

Wenn Sie nicht alle Felder des erstellten Objekts angeben, gibt Doctrine ein Teilobjekt zurück , dessen Verwendung in einigen Fällen gerechtfertigt sein kann, dessen Verhalten jedoch gefährlich ist und nicht empfohlen wird . Stattdessen können Sie mit ResultSetMappingBuilder nicht jedes Feld der endgültigen Klasse angeben, sondern Entitäten verwenden, die durch Anmerkungen (yaml-, xml-Konfigurationen) beschrieben werden. Wir schreiben unser RSM aus der vorherigen Anfrage unter Berücksichtigung der folgenden Methoden neu:

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

Skalares Ergebnis


Eine weit verbreitete Verwendung der beschriebenen Entität ist nicht gerechtfertigt, sie kann zu Leistungsproblemen führen, da Doctrine viele Objekte erstellen muss, die in Zukunft nicht vollständig verwendet werden. In solchen Fällen wird das skalare Ergebniszuordnungstool addScalarResult bereitgestellt .

Wählen Sie das Durchschnittsgehalt für jede Abteilung:

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

Das erste Argument für die Methode addScalarResult ist der Name der Spalte in der Ergebnismenge, und das zweite Argument ist der Schlüssel für das Ergebnisarray, das Doctrine zurückgibt. Der dritte Parameter ist der Typ des Ergebniswerts. Der Rückgabewert ist immer ein Array von Arrays.

DTO-Zuordnung


Das Arbeiten mit Arrays, insbesondere wenn sie eine komplexe Struktur aufweisen, ist jedoch nicht sehr praktisch. Sie müssen die Namen der Schlüssel und Feldtypen berücksichtigen. Doctrine DQL kann einfache DTOs in den Auswahlergebnissen verwenden, zum Beispiel:

    SELECT NEW DepartmentSalary(d.dept_no, avg_salary) FROM

Was ist mit Native Query und RSM? Doctrine bietet keine dokumentierten Funktionen zum Erstellen neuer DTOs . Mithilfe der Eigenschaft newObjectMappings können wir jedoch die Objekte angeben, denen die Ergebnisse der Auswahl zugeordnet werden sollen. Im Gegensatz zu Entity werden diese Objekte nicht von UnitOfWork gesteuert und müssen sich nicht in den in der Konfiguration angegebenen Namespaces befinden.

Ergänzen Sie RSM aus dem vorherigen Beispiel:

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

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

Der Schlüssel im Array des Felds newObjectMappings zeigt auf die resultierende Spalte, sein Wert ist jedoch ein anderes Array, das das zu erstellende Objekt beschreibt. Der Schlüssel className definiert den Klassennamen des neuen Objekts, argIndex - die Reihenfolge des Arguments im Konstruktor des Objekts, objIndex - die Reihenfolge des Objekts, wenn mehrere Objekte aus jeder Zeile der Auswahl abgerufen werden sollen .

Meta-Ergebnis


Das Metaergebnis wird verwendet, um Metadaten von Spalten wie Fremdschlüsseln oder Diskriminatorspalten abzurufen. Leider konnte ich kein Beispiel basierend auf der Mitarbeiterdatenbank finden, daher muss ich mich auf die Beschreibung und Beispiele aus der Dokumentation beschränken .

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

In diesem Fall teilen wir Doctine mit der Methode addMetaResult mit, dass die Benutzertabelle einen Fremdschlüssel für Adressen enthält. Anstatt die Zuordnung in den Speicher zu laden (eifriges Laden), erstellen wir ein Proxy-Objekt, in dem die Entitätskennung gespeichert und beim Zugriff geladen wird sie aus der Datenbank.

Klassische relationale Datenbanken bieten keine Mechanismen zur Vererbung von Tabellen, während die Vererbung im Objektmodell weit verbreitet ist. Das Ergebnis unserer Auswahl kann gemäß einem Attribut, das der Wert der Spalte <discriminator_column> in der resultierenden Auswahl ist, auf die Klassenhierarchie projiziert werden. In diesem Fall können wir RSM mithilfe der setDiscriminatorColumn- Methode mitteilen, welche Spalte Doctrine die instanziierte Klasse bestimmen soll .

Fazit


Doctrine ist sehr reich an verschiedenen Funktionen, einschließlich derer, die nicht alle Entwickler kennen. In diesem Beitrag habe ich versucht, ein Verständnis für die Funktionsweise einer der Schlüsselkomponenten von ORM - ResultSetMapping in Kombination mit Native Query - einzuführen. Die Verwendung wirklich komplexer und plattformspezifischer Abfragen unter Beibehaltung der Verfügbarkeit und Verständlichkeit von Beispielen wäre eine schwierige Aufgabe, da der Schwerpunkt auf dem Verständnis der Arbeit von ResultSetMapping liegt. Danach können Sie es in wirklich komplexen Abfragen für Ihre Datenbanken verwenden.

All Articles