Desain Pola Repositori yang Tepat di PHP?


291

Pendahuluan: Saya mencoba menggunakan pola repositori dalam arsitektur MVC dengan basis data relasional.

Saya baru-baru ini mulai mempelajari TDD dalam PHP, dan saya menyadari bahwa basis data saya terlalu dekat dengan aplikasi saya yang lain. Saya sudah membaca tentang repositori dan menggunakan wadah IoC untuk "menyuntikkan" ke dalam pengontrol saya. Hal yang sangat keren. Tetapi sekarang ada beberapa pertanyaan praktis tentang desain repositori. Perhatikan contoh berikut.

<?php

class DbUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct($db)
    {
        $this->db = $db;
    }

    public function findAll()
    {
    }

    public function findById($id)
    {
    }

    public function findByName($name)
    {
    }

    public function create($user)
    {
    }

    public function remove($user)
    {
    }

    public function update($user)
    {
    }
}

Masalah # 1: Terlalu banyak bidang

Semua metode pencarian ini menggunakan pendekatan pilih semua bidang ( SELECT *). Namun, di aplikasi saya, saya selalu berusaha membatasi jumlah bidang yang saya dapatkan, karena ini sering menambah overhead dan memperlambat segalanya. Bagi mereka yang menggunakan pola ini, bagaimana Anda menghadapinya?

Masalah # 2: Terlalu banyak metode

Walaupun kelas ini terlihat bagus sekarang, saya tahu bahwa dalam aplikasi dunia nyata saya memerlukan lebih banyak metode. Sebagai contoh:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Dll

Seperti yang Anda lihat, mungkin ada daftar metode yang sangat, sangat panjang. Dan kemudian jika Anda menambahkan masalah pemilihan bidang di atas, masalah bertambah buruk. Di masa lalu saya biasanya hanya menempatkan semua logika ini di controller saya:

<?php

class MyController
{
    public function users()
    {
        $users = User::select('name, email, status')
            ->byCountry('Canada')->orderBy('name')->rows();

        return View::make('users', array('users' => $users));
    }
}

Dengan pendekatan repositori saya, saya tidak ingin berakhir dengan ini:

<?php

class MyController
{
    public function users()
    {
        $users = $this->repo->get_first_name_last_name_email_username_status_by_country_order_by_name('Canada');

        return View::make('users', array('users' => $users))
    }

}

Masalah # 3: Mustahil untuk mencocokkan antarmuka

Saya melihat manfaat menggunakan antarmuka untuk repositori, jadi saya bisa menukar implementasi saya (untuk tujuan pengujian atau lainnya). Pemahaman saya tentang antarmuka adalah bahwa mereka mendefinisikan kontrak yang harus diikuti oleh suatu implementasi. Ini bagus sampai Anda mulai menambahkan metode tambahan ke repositori Anda findAllInCountry(). Sekarang saya perlu memperbarui antarmuka saya untuk juga memiliki metode ini, jika tidak, implementasi lain mungkin tidak memilikinya, dan itu dapat merusak aplikasi saya. Dengan ini terasa gila ... sebuah kasus ekor mengibas-ngibaskan anjing.

Pola spesifikasi?

Ini mengarah saya untuk percaya bahwa repositori hanya harus tetap memiliki jumlah metode (seperti save(), remove(), find(), findAll(), dll). Tapi lalu bagaimana cara menjalankan pencarian spesifik? Saya pernah mendengar tentang Pola Spesifikasi , tetapi bagi saya tampaknya ini hanya mengurangi seluruh set catatan (via IsSatisfiedBy()), yang jelas memiliki masalah kinerja utama jika Anda menarik dari database.

Tolong?

Jelas, saya perlu memikirkan kembali hal-hal sedikit ketika bekerja dengan repositori. Adakah yang bisa mengetahui bagaimana cara terbaik menangani ini?

Jawaban:


208

Saya pikir saya akan menjawab pertanyaan saya sendiri. Berikut ini hanyalah satu cara untuk menyelesaikan masalah 1-3 dalam pertanyaan awal saya.

Penafian: Saya mungkin tidak selalu menggunakan istilah yang tepat saat menjelaskan pola atau teknik. Maaf untuk itu.

Tujuan:

  • Buat contoh lengkap dari pengontrol dasar untuk melihat dan mengedit Users.
  • Semua kode harus sepenuhnya dapat diuji dan dapat dipermainkan.
  • Pengontrol seharusnya tidak tahu di mana data disimpan (artinya dapat diubah).
  • Contoh untuk menunjukkan implementasi SQL (paling umum).
  • Untuk kinerja maksimum, pengontrol hanya akan menerima data yang mereka butuhkan — tidak ada bidang tambahan.
  • Implementasi harus memanfaatkan beberapa jenis data mapper untuk kemudahan pengembangan.
  • Implementasi harus memiliki kemampuan untuk melakukan pencarian data yang kompleks.

Solusinya

Saya membagi interaksi penyimpanan (basis data) saya menjadi dua kategori: R (Baca) dan CUD (Buat, Perbarui, Hapus). Pengalaman saya adalah bahwa membaca benar-benar menyebabkan aplikasi melambat. Dan sementara manipulasi data (CUD) sebenarnya lebih lambat, itu terjadi jauh lebih jarang, dan karena itu jauh lebih sedikit dari masalah.

CUD (Buat, Perbarui, Hapus) mudah. Ini akan melibatkan bekerja dengan model yang sebenarnya , yang kemudian diteruskan ke saya Repositoriesuntuk kegigihan. Catatan, repositori saya masih akan menyediakan metode Baca, tetapi hanya untuk pembuatan objek, bukan tampilan. Lebih lanjut tentang itu nanti.

R (Baca) tidak mudah. Tidak ada model di sini, hanya nilai objek . Gunakan array jika Anda mau . Objek-objek ini dapat mewakili model tunggal atau campuran dari banyak model, apa pun sebenarnya. Ini tidak terlalu menarik pada mereka sendiri, tetapi bagaimana mereka dihasilkan. Saya menggunakan apa yang saya panggil Query Objects.

Kode:

Model Pengguna

Mari kita mulai dengan model pengguna dasar kami. Perhatikan bahwa tidak ada perluasan ORM atau basis data sama sekali. Hanya kemuliaan model murni. Tambahkan getter, setter, validasi, apa pun.

class User
{
    public $id;
    public $first_name;
    public $last_name;
    public $gender;
    public $email;
    public $password;
}

Antarmuka Repositori

Sebelum saya membuat repositori pengguna saya, saya ingin membuat antarmuka repositori saya. Ini akan menentukan "kontrak" yang harus diikuti oleh repositori agar dapat digunakan oleh pengontrol saya. Ingat, pengontrol saya tidak akan tahu di mana data sebenarnya disimpan.

Perhatikan bahwa repositori saya hanya akan berisi setiap tiga metode ini. The save()Metode bertanggung jawab untuk kedua menciptakan dan memperbarui pengguna, hanya tergantung pada apakah atau tidak objek pengguna memiliki set id.

interface UserRepositoryInterface
{
    public function find($id);
    public function save(User $user);
    public function remove(User $user);
}

Implementasi Repositori SQL

Sekarang untuk membuat implementasi antarmuka saya. Seperti yang disebutkan, contoh saya adalah dengan database SQL. Catat penggunaan data mapper untuk mencegah keharusan menulis query SQL berulang.

class SQLUserRepository implements UserRepositoryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function find($id)
    {
        // Find a record with the id = $id
        // from the 'users' table
        // and return it as a User object
        return $this->db->find($id, 'users', 'User');
    }

    public function save(User $user)
    {
        // Insert or update the $user
        // in the 'users' table
        $this->db->save($user, 'users');
    }

    public function remove(User $user)
    {
        // Remove the $user
        // from the 'users' table
        $this->db->remove($user, 'users');
    }
}

Antarmuka Objek Kueri

Sekarang dengan CUD (Buat, Perbarui, Hapus) diurus oleh repositori kami, kami dapat fokus pada R (Baca). Objek kueri hanyalah enkapsulasi dari beberapa jenis logika pencarian data. Mereka bukan pembangun permintaan. Dengan mengabstraksikannya seperti repositori kami, kami dapat mengubah implementasinya dan mengujinya lebih mudah. Contoh Obyek Kueri mungkin berupa AllUsersQueryatau AllActiveUsersQuery, atau bahkan MostCommonUserFirstNames.

Anda mungkin berpikir "tidak bisakah saya membuat metode di repositori saya untuk pertanyaan itu?" Ya, tetapi inilah mengapa saya tidak melakukan ini:

  • Repositori saya dimaksudkan untuk bekerja dengan objek model. Dalam aplikasi dunia nyata, mengapa saya harus mendapatkan passwordbidang jika saya ingin mendaftar semua pengguna saya?
  • Repositori sering model spesifik, namun permintaan sering melibatkan lebih dari satu model. Jadi repositori apa yang Anda masukkan dalam metode Anda?
  • Ini membuat repositori saya sangat sederhana — bukan kelas metode yang membengkak.
  • Semua pertanyaan sekarang diatur ke dalam kelas mereka sendiri.
  • Sungguh, pada titik ini, repositori ada hanya untuk abstrak lapisan database saya.

Sebagai contoh saya, saya akan membuat objek permintaan untuk mencari "AllUsers". Inilah antarmuka:

interface AllUsersQueryInterface
{
    public function fetch($fields);
}

Implementasi Objek Kueri

Di sinilah kita bisa menggunakan data mapper lagi untuk membantu mempercepat pengembangan. Perhatikan bahwa saya mengizinkan satu tweak untuk dataset yang dikembalikan - bidang. Sejauh ini saya ingin memanipulasi kueri yang dilakukan. Ingat, objek kueri saya bukan pembuat kueri. Mereka hanya melakukan kueri tertentu. Namun, karena saya tahu bahwa saya mungkin akan sering menggunakan ini, dalam sejumlah situasi yang berbeda, saya memberikan diri saya kemampuan untuk menentukan bidang. Saya tidak pernah ingin mengembalikan bidang yang tidak saya butuhkan!

class AllUsersQuery implements AllUsersQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch($fields)
    {
        return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
    }
}

Sebelum beralih ke controller, saya ingin menunjukkan contoh lain untuk menggambarkan betapa kuatnya ini. Mungkin saya memiliki mesin pelaporan dan perlu membuat laporan untuk AllOverdueAccounts. Ini bisa rumit dengan mapper data saya, dan saya mungkin ingin menulis beberapa aktual SQLdalam situasi ini. Tidak masalah, di sini terlihat seperti apa objek query ini:

class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
    protected $db;

    public function __construct(Database $db)
    {
        $this->db = $db;
    }

    public function fetch()
    {
        return $this->db->query($this->sql())->rows();
    }

    public function sql()
    {
        return "SELECT...";
    }
}

Ini dengan baik menyimpan semua logika saya untuk laporan ini dalam satu kelas, dan mudah untuk diuji. Saya dapat mengejeknya dengan sepenuh hati, atau bahkan menggunakan implementasi yang berbeda sama sekali.

Pengendali

Sekarang bagian yang menyenangkan — kumpulkan semuanya. Perhatikan bahwa saya menggunakan injeksi ketergantungan. Biasanya dependensi disuntikkan ke konstruktor, tetapi saya sebenarnya lebih suka menyuntikkannya langsung ke metode controller saya (rute). Ini meminimalkan grafik objek pengontrol, dan saya benar-benar merasa lebih terbaca. Catatan, jika Anda tidak menyukai pendekatan ini, cukup gunakan metode konstruktor tradisional.

class UsersController
{
    public function index(AllUsersQueryInterface $query)
    {
        // Fetch user data
        $users = $query->fetch(['first_name', 'last_name', 'email']);

        // Return view
        return Response::view('all_users.php', ['users' => $users]);
    }

    public function add()
    {
        return Response::view('add_user.php');
    }

    public function insert(UserRepositoryInterface $repository)
    {
        // Create new user model
        $user = new User;
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the new user
        $repository->save($user);

        // Return the id
        return Response::json(['id' => $user->id]);
    }

    public function view(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('view_user.php', ['user' => $user]);
    }

    public function edit(SpecificUserQueryInterface $query, $id)
    {
        // Load user data
        if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
            return Response::notFound();
        }

        // Return view
        return Response::view('edit_user.php', ['user' => $user]);
    }

    public function update(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Update the user
        $user->first_name = $_POST['first_name'];
        $user->last_name = $_POST['last_name'];
        $user->gender = $_POST['gender'];
        $user->email = $_POST['email'];

        // Save the user
        $repository->save($user);

        // Return success
        return true;
    }

    public function delete(UserRepositoryInterface $repository)
    {
        // Load user model
        if (!$user = $repository->find($id)) {
            return Response::notFound();
        }

        // Delete the user
        $repository->delete($user);

        // Return success
        return true;
    }
}

Pikiran terakhir:

Hal penting yang perlu diperhatikan di sini adalah ketika saya memodifikasi (membuat, memperbarui atau menghapus) entitas, saya bekerja dengan objek model nyata, dan melakukan persistensi melalui repositori saya.

Namun, ketika saya menampilkan (memilih data dan mengirimkannya ke tampilan), saya tidak bekerja dengan objek model, melainkan objek nilai lama yang polos. Saya hanya memilih bidang yang saya butuhkan, dan itu dirancang agar saya dapat memaksimalkan kinerja pencarian data saya.

Repositori saya tetap sangat bersih, dan sebagai gantinya "kekacauan" ini diatur dalam pertanyaan model saya.

Saya menggunakan data mapper untuk membantu pengembangan, karena itu konyol untuk menulis SQL berulang untuk tugas-tugas umum. Namun, Anda benar-benar dapat menulis SQL di mana diperlukan (pertanyaan rumit, pelaporan, dll.). Dan ketika Anda melakukannya, itu terselip dengan baik ke dalam kelas yang diberi nama dengan benar.

Saya ingin mendengar pendapat Anda tentang pendekatan saya!


Pembaruan Juli 2015:

Saya telah ditanya dalam komentar di mana saya berakhir dengan semua ini. Sebenarnya tidak terlalu jauh. Sejujurnya, saya masih tidak terlalu menyukai repositori. Saya menemukan mereka berlebihan untuk pencarian dasar (terutama jika Anda sudah menggunakan ORM), dan berantakan ketika bekerja dengan pertanyaan yang lebih rumit.

Saya biasanya bekerja dengan ORM gaya ActiveRecord, jadi paling sering saya hanya akan merujuk model-model itu langsung di seluruh aplikasi saya. Namun, dalam situasi di mana saya memiliki kueri yang lebih kompleks, saya akan menggunakan objek kueri untuk membuatnya lebih dapat digunakan kembali. Saya juga harus mencatat bahwa saya selalu menyuntikkan model saya ke dalam metode saya, membuatnya lebih mudah untuk mengejek dalam tes saya.


4
@PeeHaa Sekali lagi, itu untuk menjaga contoh sederhana. Sangat umum untuk meninggalkan potongan-potongan kode dari contoh jika mereka tidak berkaitan secara khusus dengan topik yang dihadapi. Pada kenyataannya, saya akan melewati ketergantungan saya.
Jonathan

4
Menarik bahwa Anda memisahkan Buat, Perbarui dan Hapus dari Baca Anda. Pikir itu akan layak menyebutkan Segregasi Command Query Responsibility (CQRS) yang secara formal melakukan hal itu. martinfowler.com/bliki/CQRS.html
Adam

2
@ Jonathan Sudah satu setengah tahun sejak Anda menjawab pertanyaan Anda sendiri. Saya bertanya-tanya apakah Anda masih senang dengan jawaban Anda dan apakah ini solusi utama Anda sekarang untuk sebagian besar proyek Anda? Beberapa minggu terakhir saya telah membaca membagikan repositori dan saya telah melihat banyak orang memiliki interpretasi mereka sendiri tentang bagaimana itu harus dilaksanakan. Anda menyebutnya objek permintaan, tapi ini pola yang ada kan? Saya pikir saya telah melihatnya digunakan dalam bahasa lain.
Boedy

1
@ Jonathan: Bagaimana Anda menangani pertanyaan yang seharusnya membuat pengguna tidak menjadi "ID" tetapi mis. Dengan "nama pengguna" atau bahkan pertanyaan yang lebih rumit dengan lebih dari satu syarat?
Gizzmo

1
@ Geizzmo Menggunakan objek kueri, Anda bisa mengirimkan parameter tambahan untuk membantu dengan pertanyaan Anda yang lebih rumit. Sebagai contoh, Anda dapat melakukan hal ini dalam constructor: new Query\ComplexUserLookup($username, $anotherCondition). Atau, lakukan ini melalui metode penyetel $query->setUsername($username);. Anda benar-benar dapat merancang ini namun masuk akal untuk aplikasi khusus Anda, dan saya pikir objek permintaan meninggalkan banyak fleksibilitas di sini.
Jonathan

48

Berdasarkan pengalaman saya, berikut adalah beberapa jawaban untuk pertanyaan Anda:

T: Bagaimana cara kita menangani pengembalian bidang yang tidak kita butuhkan?

A: Dari pengalaman saya, ini benar-benar bermuara pada berurusan dengan entitas lengkap versus permintaan ad-hoc.

Entitas yang lengkap adalah sesuatu seperti Userobjek. Ini memiliki properti dan metode, dll. Ini adalah warga negara kelas satu di basis kode Anda.

Kueri ad-hoc mengembalikan beberapa data, tetapi kami tidak tahu apa-apa selain itu. Ketika data dilewatkan di sekitar aplikasi, itu dilakukan tanpa konteks. Apakah ini User? A Userdengan beberapa Orderinformasi terlampir? Kami tidak benar-benar tahu.

Saya lebih suka bekerja dengan entitas penuh.

Anda benar bahwa Anda akan sering mengembalikan data yang tidak akan Anda gunakan, tetapi Anda dapat mengatasinya dengan berbagai cara:

  1. Cache entitas secara agresif sehingga Anda hanya membayar harga baca satu kali dari database.
  2. Luangkan lebih banyak waktu untuk memodelkan entitas Anda sehingga mereka memiliki perbedaan yang baik di antara mereka. (Pertimbangkan memecah entitas besar menjadi dua entitas yang lebih kecil, dll.)
  3. Pertimbangkan memiliki beberapa versi entitas. Anda dapat memiliki Useruntuk bagian belakang dan mungkin UserSmalluntuk panggilan AJAX. Seseorang mungkin memiliki 10 properti dan satu memiliki 3 properti.

Kelemahan bekerja dengan permintaan ad-hoc:

  1. Anda pada dasarnya memiliki data yang sama di banyak pertanyaan. Misalnya, dengan a User, Anda pada dasarnya akan menulis hal yang sama select *untuk banyak panggilan. Satu panggilan akan mendapatkan 8 dari 10 bidang, satu akan mendapatkan 5 dari 10, satu akan mendapatkan 7 dari 10. Mengapa tidak mengganti semua dengan satu panggilan yang mendapat 10 dari 10? Alasan ini buruk adalah karena pembunuhan adalah untuk re-factor / test / ejekan.
  2. Menjadi sangat sulit untuk beralasan pada tingkat tinggi tentang kode Anda dari waktu ke waktu. Alih-alih pernyataan seperti "Mengapa Userbegitu lambat?" Anda akhirnya melacak kueri satu kali dan karenanya perbaikan bug cenderung kecil dan terlokalisasi.
  3. Sangat sulit untuk mengganti teknologi yang mendasarinya. Jika Anda menyimpan semuanya di MySQL sekarang dan ingin pindah ke MongoDB, jauh lebih sulit untuk mengganti 100 panggilan ad-hoc daripada segelintir entitas.

T: Saya akan memiliki terlalu banyak metode di repositori saya.

A: Saya belum benar-benar melihat jalan keluar selain dari konsolidasi panggilan. Metode panggilan dalam repositori Anda benar-benar memetakan ke fitur-fitur dalam aplikasi Anda. Semakin banyak fitur, semakin banyak data panggilan khusus. Anda dapat menekan kembali fitur dan mencoba menggabungkan panggilan serupa menjadi satu.

Kompleksitas pada akhirnya harus ada di suatu tempat. Dengan pola repositori, kami telah mendorongnya ke antarmuka repositori alih-alih mungkin membuat banyak prosedur tersimpan.

Kadang-kadang saya harus mengatakan pada diri sendiri, "Yah, itu harus memberi di suatu tempat! Tidak ada peluru perak."


Terima kasih atas jawaban yang sangat teliti. Anda membuat saya berpikir sekarang. Perhatian utama saya di sini adalah bahwa semua yang saya baca mengatakan tidak SELECT *, bukan hanya memilih bidang yang Anda butuhkan. Sebagai contoh, lihat pertanyaan ini . Adapun semua pertanyaan ad-hock yang Anda bicarakan, saya tentu mengerti dari mana Anda berasal. Saya memiliki aplikasi yang sangat besar sekarang yang memiliki banyak dari mereka. Itu adalah "Yah, itu harus memberi di suatu tempat!" Saat ini, saya memilih kinerja maksimum. Namun, sekarang saya berurusan dengan BANYAK pertanyaan yang berbeda.
Jonathan

1
Satu pemikiran lanjutan. Saya telah melihat rekomendasi untuk menggunakan pendekatan R-CUD. Karena readssering kali muncul masalah kinerja, Anda dapat menggunakan pendekatan kueri yang lebih khusus untuk mereka, yang tidak diterjemahkan ke dalam objek bisnis nyata. Kemudian, untuk create, updatedan delete, gunakan ORM, yang bekerja dengan seluruh objek. Adakah pemikiran tentang pendekatan itu?
Jonathan

1
Sebagai catatan untuk menggunakan "pilih *". Saya sudah melakukannya di masa lalu dan berhasil - sampai kita menekan bidang varchar (maks). Itu membunuh pertanyaan kita. Jadi jika Anda memiliki tabel dengan int, bidang teks kecil, dll. Itu tidak terlalu buruk. Terasa tidak wajar, tetapi perangkat lunak seperti itu. Apa yang buruk tiba-tiba baik dan sebaliknya.
ryan1234

1
Pendekatan R-CUD sebenarnya adalah CQRS
MikeSW

2
@Ryan1234 "Kompleksitas pada akhir hari harus ada di suatu tempat." Terima kasih untuk ini. Membuat saya merasa lebih baik.
johnny

20

Saya menggunakan antarmuka berikut:

  • Repository - memuat, memasukkan, memperbarui, dan menghapus entitas
  • Selector - Menemukan entitas berdasarkan filter, dalam repositori
  • Filter - merangkum logika penyaringan

RepositoryDatabase saya agnostik; pada kenyataannya itu tidak menentukan kegigihan; itu bisa apa saja: database SQL, file xml, layanan jarak jauh, alien dari luar angkasa dll. Untuk kemampuan pencarian, Repositorykonstruk Selectoryang dapat disaring, LIMIT-ed, diurutkan dan dihitung. Pada akhirnya, pemilih mengambil satu atau lebih Entitiesdari ketekunan.

Berikut ini beberapa contoh kode:

<?php
interface Repository
{
    public function addEntity(Entity $entity);

    public function updateEntity(Entity $entity);

    public function removeEntity(Entity $entity);

    /**
     * @return Entity
     */
    public function loadEntity($entityId);

    public function factoryEntitySelector():Selector
}


interface Selector extends \Countable
{
    public function count();

    /**
     * @return Entity[]
     */
    public function fetchEntities();

    /**
     * @return Entity
     */
    public function fetchEntity();
    public function limit(...$limit);
    public function filter(Filter $filter);
    public function orderBy($column, $ascending = true);
    public function removeFilter($filterName);
}

interface Filter
{
    public function getFilterName();
}

Kemudian, satu implementasi:

class SqlEntityRepository
{
    ...
    public function factoryEntitySelector()
    {
        return new SqlSelector($this);
    }
    ...
}

class SqlSelector implements Selector
{
    ...
    private function adaptFilter(Filter $filter):SqlQueryFilter
    {
         return (new SqlSelectorFilterAdapter())->adaptFilter($filter);
    }
    ...
}
class SqlSelectorFilterAdapter
{
    public function adaptFilter(Filter $filter):SqlQueryFilter
    {
        $concreteClass = (new StringRebaser(
            'Filter\\', 'SqlQueryFilter\\'))
            ->rebase(get_class($filter));

        return new $concreteClass($filter);
    }
}

Idenya adalah bahwa generik Selectormenggunakan Filtertetapi implementasinya SqlSelectormenggunakan SqlFilter; yang SqlSelectorFilterAdaptermenyesuaikan generik Filterdengan beton SqlFilter.

Kode klien membuat Filterobjek (yang merupakan filter umum) tetapi dalam implementasi konkret dari pemilih filter tersebut ditransformasikan dalam filter SQL.

Implementasi pemilih lainnya, seperti InMemorySelector, mengubah dari Filterke InMemoryFiltermenggunakan khusus mereka InMemorySelectorFilterAdapter; jadi, setiap implementasi pemilih dilengkapi dengan adaptor filternya sendiri.

Menggunakan strategi ini, kode klien saya (di lapisan bisnis) tidak peduli dengan repositori atau implementasi pemilih tertentu.

/** @var Repository $repository*/
$selector = $repository->factoryEntitySelector();
$selector->filter(new AttributeEquals('activated', 1))->limit(2)->orderBy('username');
$activatedUserCount = $selector->count(); // evaluates to 100, ignores the limit()
$activatedUsers = $selector->fetchEntities();

PS Ini adalah penyederhanaan kode saya yang sebenarnya


"Repositori - memuat, menyisipkan, memperbarui, dan menghapus entitas" inilah yang dapat dilakukan oleh "lapisan layanan", "DAO", "BLL"
Yousha Aleayoub

5

Saya akan menambahkan sedikit tentang ini karena saya sedang mencoba untuk memahami semua ini sendiri.

# 1 dan 2

Ini adalah tempat yang sempurna bagi ORM Anda untuk melakukan pekerjaan berat. Jika Anda menggunakan model yang mengimplementasikan semacam ORM, Anda bisa menggunakan metode itu untuk menangani hal-hal ini. Buat fungsi orderBy Anda sendiri yang menerapkan metode Eloquent jika perlu. Menggunakan Eloquent misalnya:

class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }

Apa yang Anda cari adalah ORM. Tidak ada alasan Repositori Anda tidak dapat didasarkan pada satu. Ini akan mengharuskan Pengguna memperluas fasih, tapi saya pribadi tidak melihatnya sebagai masalah.

Namun, jika Anda ingin menghindari ORM, Anda harus "memutar sendiri" untuk mendapatkan apa yang Anda cari.

# 3

Antarmuka tidak seharusnya menjadi persyaratan yang sulit dan cepat. Sesuatu dapat mengimplementasikan antarmuka dan menambahkannya. Apa yang tidak bisa dilakukan adalah gagal untuk mengimplementasikan fungsi yang diperlukan dari antarmuka itu. Anda juga dapat memperluas antarmuka seperti kelas untuk menjaga hal-hal KERING.

Yang mengatakan, saya baru mulai memahami, tetapi realisasi ini telah membantu saya.


1
Apa yang saya tidak suka tentang metode ini adalah bahwa jika Anda memiliki MongoUserRepository, itu dan DbUserRepository Anda akan mengembalikan objek yang berbeda. Db mengembalikan Eloquent \ Model, dan Mongo sendiri. Tentunya implementasi yang lebih baik adalah memiliki kedua repositori mengembalikan instance / koleksi dari kelas Entity \ User yang terpisah. Dengan cara ini Anda tidak salah bergantung pada metode DB Eloquent \ Model ketika Anda beralih menggunakan MongoRepository
danharper

1
Saya pasti setuju dengan Anda tentang hal itu. Apa yang mungkin saya lakukan untuk menghindarinya adalah jangan pernah menggunakan metode-metode itu di luar kelas yang membutuhkan Eloquent. Jadi fungsi get mungkin harus pribadi dan hanya digunakan dalam kelas seperti itu, seperti yang Anda tunjukkan, akan mengembalikan sesuatu yang repositori lain tidak bisa.
Will

3

Saya hanya bisa berkomentar tentang cara kami (di perusahaan saya) menangani ini. Pertama-tama kinerja tidak terlalu menjadi masalah bagi kami, tetapi memiliki kode bersih / tepat adalah.

Pertama-tama kita mendefinisikan Model seperti UserModelyang menggunakan ORM untuk membuat UserEntityobjek. Ketika a UserEntitydimuat dari model, semua bidang dimuat. Untuk bidang yang mereferensikan entitas asing, kami menggunakan model asing yang sesuai untuk membuat entitas terkait. Untuk entitas tersebut, data akan dimuat pada permintaan. Sekarang reaksi awal Anda mungkin ... ??? ... !!! izinkan saya memberi Anda sedikit contoh:

class UserEntity extends PersistentEntity
{
    public function getOrders()
    {
        $this->getField('orders'); //OrderModel creates OrderEntities with only the ID's set
    }
}

class UserModel {
    protected $orm;

    public function findUsers(IGetOptions $options = null)
    {
        return $orm->getAllEntities(/*...*/); // Orm creates a list of UserEntities
    }
}

class OrderEntity extends PersistentEntity {} // user your imagination
class OrderModel
{
    public function findOrdersById(array $ids, IGetOptions $options = null)
    {
        //...
    }
}

Dalam kasus kami $dbadalah ORM yang dapat memuat entitas. Model menginstruksikan ORM untuk memuat sekumpulan entitas dari tipe tertentu. ORM berisi pemetaan dan menggunakannya untuk menyuntikkan semua bidang untuk entitas tersebut ke entitas. Namun untuk bidang asing hanya id dari objek-objek yang dimuat. Dalam hal ini, OrderModelmenciptakan OrderEntityhanya dengan id dari pesanan yang direferensikan. Ketika PersistentEntity::getFielddipanggil oleh OrderEntityentitas menginstruksikan itu model untuk malas memuat semua bidang ke dalam OrderEntitys. Semua yang OrderEntityterkait dengan satu UserEntity diperlakukan sebagai satu set hasil dan akan dimuat sekaligus.

Keajaiban di sini adalah bahwa model dan ORM kami menyuntikkan semua data ke entitas dan entitas hanya menyediakan fungsi wrapper untuk getFieldmetode generik yang disediakan oleh PersistentEntity. Untuk meringkas, kami selalu memuat semua bidang, tetapi bidang referensi entitas asing dimuat saat diperlukan. Hanya memuat sekelompok bidang sebenarnya bukan masalah kinerja. Namun memuat semua entitas asing yang mungkin akan menjadi penurunan kinerja BESAR.

Sekarang untuk memuat kumpulan pengguna tertentu, berdasarkan klausa tempat. Kami menyediakan paket kelas berorientasi objek yang memungkinkan Anda untuk menentukan ekspresi sederhana yang dapat direkatkan bersama. Dalam kode contoh saya menamainya GetOptions. Ini adalah pembungkus untuk semua opsi yang memungkinkan untuk kueri pemilihan. Ini berisi kumpulan klausa mana, grup dengan klausa dan yang lainnya. Klausa tempat kami cukup rumit tetapi Anda jelas dapat membuat versi yang lebih mudah.

$objOptions->getConditionHolder()->addConditionBind(
    new ConditionBind(
        new Condition('orderProduct.product', ICondition::OPERATOR_IS, $argObjProduct)
    )
);

Versi paling sederhana dari sistem ini adalah untuk melewatkan bagian WHERE dari kueri sebagai string langsung ke model.

Maaf atas tanggapan yang cukup rumit ini. Saya mencoba merangkum kerangka kerja kami secepat dan sejelas mungkin. Jika Anda memiliki pertanyaan tambahan, jangan ragu untuk bertanya dan saya akan memperbarui jawaban saya.

EDIT: Selain itu jika Anda benar-benar tidak ingin memuat beberapa bidang Anda bisa menentukan opsi pemuatan malas dalam pemetaan ORM Anda. Karena semua bidang pada akhirnya dimuat melalui getFieldmetode Anda dapat memuat beberapa bidang menit terakhir ketika metode itu dipanggil. Ini bukan masalah yang sangat besar di PHP, tetapi saya tidak akan merekomendasikan untuk sistem lain.


3

Ini adalah beberapa solusi berbeda yang pernah saya lihat. Ada pro dan kontra untuk masing-masing, tetapi bagi Anda untuk memutuskan.

Masalah # 1: Terlalu banyak bidang

Ini adalah aspek penting terutama ketika Anda memperhitungkan Pemindaian Hanya Indeks . Saya melihat dua solusi untuk mengatasi masalah ini. Anda dapat memperbarui fungsi Anda untuk mengambil parameter array opsional yang akan berisi daftar kolom untuk dikembalikan. Jika parameter ini kosong, Anda akan mengembalikan semua kolom dalam kueri. Ini bisa sedikit aneh; berdasarkan parameter Anda bisa mengambil objek atau array. Anda juga bisa menduplikasi semua fungsi Anda sehingga Anda memiliki dua fungsi berbeda yang menjalankan kueri yang sama, tetapi satu mengembalikan array kolom dan yang lainnya mengembalikan objek.

public function findColumnsById($id, array $columns = array()){
    if (empty($columns)) {
        // use *
    }
}

public function findById($id) {
    $data = $this->findColumnsById($id);
}

Masalah # 2: Terlalu banyak metode

Saya sempat bekerja dengan Propel ORM setahun yang lalu dan ini didasarkan pada apa yang saya ingat dari pengalaman itu. Propel memiliki opsi untuk menghasilkan struktur kelasnya berdasarkan skema database yang ada. Ini menciptakan dua objek untuk setiap tabel. Objek pertama adalah daftar panjang fungsi akses yang mirip dengan apa yang saat ini Anda daftarkan; findByAttribute($attribute_value). Objek berikutnya mewarisi dari objek pertama ini. Anda dapat memperbarui objek anak ini untuk membangun fungsi pengambil yang lebih kompleks.

Solusi lain akan digunakan __call()untuk memetakan fungsi yang tidak didefinisikan untuk sesuatu yang dapat ditindaklanjuti. __callMetode Anda akan dapat mem-parsing findById dan findByName ke dalam kueri yang berbeda.

public function __call($function, $arguments) {
    if (strpos($function, 'findBy') === 0) {
        $parameter = substr($function, 6, strlen($function));
        // SELECT * FROM $this->table_name WHERE $parameter = $arguments[0]
    }
}

Saya harap ini membantu setidaknya beberapa hal.



0

Saya setuju dengan @ ryan1234 bahwa Anda harus membagikan objek lengkap dalam kode dan harus menggunakan metode kueri umum untuk mendapatkan objek tersebut.

Model::where(['attr1' => 'val1'])->get();

Untuk penggunaan eksternal / endpoint saya sangat suka metode GraphQL.

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}

0

Masalah # 3: Mustahil untuk mencocokkan antarmuka

Saya melihat manfaat menggunakan antarmuka untuk repositori, jadi saya bisa menukar implementasi saya (untuk tujuan pengujian atau lainnya). Pemahaman saya tentang antarmuka adalah bahwa mereka mendefinisikan kontrak yang harus diikuti oleh suatu implementasi. Ini bagus sampai Anda mulai menambahkan metode tambahan ke repositori Anda seperti findAllInCountry (). Sekarang saya perlu memperbarui antarmuka saya untuk juga memiliki metode ini, jika tidak, implementasi lain mungkin tidak memilikinya, dan itu dapat merusak aplikasi saya. Dengan ini terasa gila ... sebuah kasus ekor mengibas-ngibaskan anjing.

Naluri saya memberi tahu saya ini mungkin memerlukan antarmuka yang mengimplementasikan metode yang dioptimalkan query bersama metode generik. Kueri sensitif kinerja harus memiliki metode yang ditargetkan, sementara kueri yang jarang atau ringan ditangani oleh penangan generik, mungkin biaya pengontrol melakukan sedikit juggling.

Metode generik akan memungkinkan setiap kueri diterapkan, dan juga akan mencegah pemutusan perubahan selama periode transisi. Metode yang ditargetkan memungkinkan Anda untuk mengoptimalkan panggilan saat masuk akal, dan itu dapat diterapkan ke beberapa penyedia layanan.

Pendekatan ini akan mirip dengan implementasi perangkat keras yang melakukan tugas-tugas khusus yang dioptimalkan, sementara implementasi perangkat lunak melakukan pekerjaan yang ringan atau implementasi yang fleksibel.


0

Saya pikir graphQL adalah kandidat yang baik dalam kasus seperti itu untuk menyediakan bahasa permintaan skala besar tanpa meningkatkan kompleksitas repositori data.

Namun, ada solusi lain jika Anda tidak ingin menggunakan graphQL untuk saat ini. Dengan menggunakan DTO di mana objek digunakan untuk mengarungi data antara proses, dalam hal ini antara layanan / pengontrol dan repositori.

Sebuah jawaban elegan sudah disediakan di atas, namun saya akan mencoba memberikan contoh lain yang menurut saya lebih sederhana dan dapat berfungsi sebagai titik awal untuk proyek baru.

Seperti yang ditunjukkan dalam kode, kita hanya membutuhkan 4 metode untuk operasi CRUD. yang findmetode akan digunakan untuk daftar dan membaca dengan melewati argumen objek. Layanan Backend dapat membangun objek kueri yang ditentukan berdasarkan string kueri URL atau berdasarkan parameter tertentu.

Objek permintaan ( SomeQueryDto) juga dapat mengimplementasikan antarmuka spesifik jika diperlukan. dan mudah untuk diperpanjang kemudian tanpa menambah kerumitan.

<?php

interface SomeRepositoryInterface
{
    public function create(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function update(SomeEnitityInterface $entityData): SomeEnitityInterface;
    public function delete(int $id): void;

    public function find(SomeEnitityQueryInterface $query): array;
}

class SomeRepository implements SomeRepositoryInterface
{
    public function find(SomeQueryDto $query): array
    {
        $qb = $this->getQueryBuilder();

        foreach ($query->getSearchParameters() as $attribute) {
            $qb->where($attribute['field'], $attribute['operator'], $attribute['value']);
        }

        return $qb->get();
    }
}

/**
 * Provide query data to search for tickets.
 *
 * @method SomeQueryDto userId(int $id, string $operator = null)
 * @method SomeQueryDto categoryId(int $id, string $operator = null)
 * @method SomeQueryDto completedAt(string $date, string $operator = null)
 */
class SomeQueryDto
{
    /** @var array  */
    const QUERYABLE_FIELDS = [
        'id',
        'subject',
        'user_id',
        'category_id',
        'created_at',
    ];

    /** @var array  */
    const STRING_DB_OPERATORS = [
        'eq' => '=', // Equal to
        'gt' => '>', // Greater than
        'lt' => '<', // Less than
        'gte' => '>=', // Greater than or equal to
        'lte' => '<=', // Less than or equal to
        'ne' => '<>', // Not equal to
        'like' => 'like', // Search similar text
        'in' => 'in', // one of range of values
    ];

    /**
     * @var array
     */
    private $searchParameters = [];

    const DEFAULT_OPERATOR = 'eq';

    /**
     * Build this query object out of query string.
     * ex: id=gt:10&id=lte:20&category_id=in:1,2,3
     */
    public static function buildFromString(string $queryString): SomeQueryDto
    {
        $query = new self();
        parse_str($queryString, $queryFields);

        foreach ($queryFields as $field => $operatorAndValue) {
            [$operator, $value] = explode(':', $operatorAndValue);
            $query->addParameter($field, $operator, $value);
        }

        return $query;
    }

    public function addParameter(string $field, string $operator, $value): SomeQueryDto
    {
        if (!in_array($field, self::QUERYABLE_FIELDS)) {
            throw new \Exception("$field is invalid query field.");
        }
        if (!array_key_exists($operator, self::STRING_DB_OPERATORS)) {
            throw new \Exception("$operator is invalid query operator.");
        }
        if (!is_scalar($value)) {
            throw new \Exception("$value is invalid query value.");
        }

        array_push(
            $this->searchParameters,
            [
                'field' => $field,
                'operator' => self::STRING_DB_OPERATORS[$operator],
                'value' => $value
            ]
        );

        return $this;
    }

    public function __call($name, $arguments)
    {
        // camelCase to snake_case
        $field = strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $name));

        if (in_array($field, self::QUERYABLE_FIELDS)) {
            return $this->addParameter($field, $arguments[1] ?? self::DEFAULT_OPERATOR, $arguments[0]);
        }
    }

    public function getSearchParameters()
    {
        return $this->searchParameters;
    }
}

Contoh penggunaan:

$query = new SomeEnitityQuery();
$query->userId(1)->categoryId(2, 'ne')->createdAt('2020-03-03', 'lte');
$entities = $someRepository->find($query);

// Or by passing the HTTP query string
$query = SomeEnitityQuery::buildFromString('created_at=gte:2020-01-01&category_id=in:1,2,3');
$entities = $someRepository->find($query);
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.