Doctrine ResultSetMapping dengan Contoh

Doctrine ORM menyediakan alat yang nyaman bagi pengembang untuk mengambil data. Ini adalah DQL yang kuat untuk bekerja dengan cara berorientasi objek, dan Query Builder yang nyaman , sederhana dan intuitif untuk digunakan. Mereka mencakup sebagian besar kebutuhan, tetapi kadang-kadang menjadi perlu untuk menggunakan query SQL yang dioptimalkan atau khusus untuk DBMS tertentu. Untuk bekerja dengan hasil kueri dalam kode, pemahaman tentang bagaimana pemetaan dalam pekerjaan Ajaran adalah penting.



Doctrine ORM didasarkan pada pola Data Mapper , yang mengisolasi representasi relasional dari representasi objek dan mengubah data di antara mereka. Salah satu komponen utama dari proses ini adalah objek ResultSetMapping , yang menjelaskan cara mengubah hasil kueri dari model relasional ke objek satu. Doctrine selalu menggunakan ResultSetMapping untuk menyajikan hasil kueri, tetapi biasanya objek ini dibuat berdasarkan anotasi atau yaml, konfigurasi xml, tetap disembunyikan dari mata pengembang, oleh karena itu tidak semua orang tahu tentang kemampuannya.

Untuk contoh bekerja dengan ResultSetMapping, saya akan menggunakan database sampel MySQL dan karyawan .

Struktur DB


Mari kita gambarkan entitas Departemen, Karyawan, Gaji, yang dengannya kami akan terus bekerja lebih jauh.

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



Karyawan

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

}


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

}



Hasil entitas


Mari kita mulai dengan yang sederhana, pilih semua departemen dari pangkalan dan proyeksikan ke Departemen:

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


Metode addEntityResult menunjukkan kelas mana sampel kami akan diproyeksikan, dan metode addFieldResult menunjukkan pemetaan antara kolom sampel dan bidang objek. Kueri SQL yang diteruskan ke metode createNativeQuery akan ditransfer ke database dalam formulir ini dan Doctrine tidak akan memodifikasinya dengan cara apa pun.

Perlu diingat bahwa Entitas tentu memiliki pengidentifikasi unik, yang harus dimasukkan dalam fieldResult.

Hasil Entitas yang Bergabung


Kami memilih departemen di mana ada karyawan yang dipekerjakan setelah awal tahun 2000, bersama dengan karyawan ini.

        $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


Seperti yang Anda lihat, bekerja langsung dengan objek ResultSetMapping agak rumit dan membuatnya sangat rinci untuk menggambarkan perbandingan pemilihan dan objek. Selain itu, Doctrine menyediakan alat yang lebih nyaman - ResultSetMappingBuilder , yang merupakan pembungkus RSM dan menambahkan metode yang lebih nyaman untuk bekerja dengan pemetaan. Misalnya, metode generateSelectClause , yang memungkinkan Anda membuat parameter untuk bagian SELECT dari kueri dengan deskripsi bidang yang diperlukan untuk pemilihan. Permintaan sebelumnya dapat ditulis ulang dalam bentuk yang lebih sederhana.

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

Perlu dicatat bahwa jika Anda tidak menentukan semua bidang objek yang dibuat, Doktrin akan mengembalikan objek parsial , yang penggunaannya dapat dibenarkan dalam beberapa kasus, tetapi perilakunya berbahaya dan tidak direkomendasikan . Sebagai gantinya, ResultSetMappingBuilder memungkinkan Anda untuk tidak menentukan setiap bidang dari kelas akhir, tetapi menggunakan entitas yang dijelaskan melalui anotasi (konfigurasi yaml, xml). Kami menulis ulang RSM kami dari permintaan sebelumnya, dengan mempertimbangkan metode ini:

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

Hasil skalar


Penggunaan entitas yang dideskripsikan secara luas tidak dibenarkan, hal itu dapat menyebabkan masalah kinerja, karena Doktrin perlu membuat banyak objek yang tidak akan digunakan sepenuhnya di masa depan. Untuk kasus-kasus seperti itu, alat pemetaan hasil skalar, addScalarResult, disediakan .

Pilih gaji rata-rata untuk setiap departemen:

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

Argumen pertama ke metode addScalarResult adalah nama kolom di set hasil, dan yang kedua adalah kunci untuk array hasil yang akan dikembalikan oleh Doktrin. Parameter ketiga adalah jenis nilai hasil. Nilai kembali akan selalu berupa array array.

Pemetaan DTO


Tetapi bekerja dengan array, terutama ketika mereka memiliki struktur yang kompleks, sangat tidak nyaman. Anda perlu mengingat nama-nama kunci, jenis bidang. Doctrine DQL memiliki kemampuan untuk menggunakan DTO sederhana dalam hasil pemilihan, misalnya:

    SELECT NEW DepartmentSalary(d.dept_no, avg_salary) FROM

Bagaimana dengan Native Query dan RSM? Doctrine tidak menyediakan kapabilitas terdokumentasi untuk membuat DTO baru , tetapi dengan menggunakan properti newObjectMappings, kita dapat menentukan objek yang ingin kita petakan hasil seleksi. Tidak seperti Entity, objek ini tidak akan dikontrol oleh UnitOfWork, dan tidak diharuskan berada dalam ruang nama yang ditentukan dalam konfigurasi.

Lengkapi RSM dari contoh sebelumnya:

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

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

Kunci dalam array bidang newObjectMappings menunjuk ke kolom yang dihasilkan, tetapi nilainya array lain yang menggambarkan objek yang sedang dibuat. Kunci className mendefinisikan nama kelas dari objek baru, argIndex - urutan argumen dalam konstruktor objek, objIndex - urutan objek, jika kita ingin mendapatkan beberapa objek dari setiap baris pilihan.

Hasil meta


Hasil meta digunakan untuk mendapatkan meta-data kolom, seperti kunci asing atau kolom diskriminator. Sayangnya, saya tidak dapat memberikan contoh berdasarkan pada database karyawan, jadi saya harus membatasi diri pada deskripsi dan contoh dari dokumentasi .

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

Dalam kasus ini, dengan metode addMetaResult, kami memberi tahu Doctine bahwa tabel pengguna memiliki kunci asing pada alamat, tetapi alih-alih memuat asosiasi ke dalam memori (eager load), kami membuat objek-proxy yang menyimpan pengenal entitas, dan ketika diakses, itu memuat dia dari database.

Database relasional klasik tidak menawarkan mekanisme pewarisan tabel, sementara pewarisan tersebar luas dalam model objek. Hasil seleksi kami dapat diproyeksikan ke hierarki kelas, menurut beberapa atribut, yang merupakan nilai dari kolom <discriminator_column> dalam pilihan yang dihasilkan. Dalam hal ini, kita dapat memberi tahu RSM kolom mana yang harus menentukan kelas Instantiated dengan menggunakan metode setDiscriminatorColumn .

Kesimpulan


Doktrin sangat kaya akan berbagai fitur, termasuk yang tidak semua pengembang ketahui. Dalam posting ini, saya mencoba untuk memperkenalkan pemahaman tentang pengoperasian salah satu komponen utama ORM - ResultSetMapping dalam kombinasi dengan Native Query. Menggunakan kueri yang benar-benar kompleks dan spesifik platform, sambil mempertahankan ketersediaan dan kelengkapan contoh, akan menjadi tugas yang sulit, karena penekanannya adalah pada memahami pekerjaan ResultSetMapping. Setelah itu, Anda bisa menggunakannya dalam permintaan yang sangat kompleks untuk basis data Anda.

All Articles