PHP uygun depo desen tasarımı?


291

Önsöz: İlişkisel veritabanlarıyla bir MVC mimarisinde depo desenini kullanmaya çalışıyorum.

Geçenlerde PHP'de TDD öğrenmeye başladım ve veritabanımın uygulamamın geri kalanıyla çok yakından eşleştiğinin farkındayım. Depoları okudum ve denetleyicilerime "enjekte etmek" için bir IoC kapsayıcısı kullandım. Çok güzel şeyler. Ancak şimdi depo tasarımı hakkında bazı pratik sorularınız var. Aşağıdaki örneği düşünün.

<?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)
    {
    }
}

Sorun # 1: Çok fazla alan

Bu bulma yöntemlerinin tümü bir select all fields ( SELECT *) yaklaşımını kullanır. Ancak, uygulamalarımda, her zaman aldığım alan sayısını sınırlamaya çalışıyorum, çünkü bu genellikle ek yük ekler ve işleri yavaşlatır. Bu kalıbı kullananlar için bununla nasıl başa çıkıyorsunuz?

Sorun # 2: Çok fazla yöntem

Bu sınıf şu anda güzel görünse de, gerçek dünyadaki bir uygulamada çok daha fazla yönteme ihtiyacım olduğunu biliyorum. Örneğin:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Vb.

Gördüğünüz gibi, olası yöntemlerin çok ama çok uzun bir listesi olabilir. Ve sonra yukarıdaki alan seçimi sorununu eklerseniz, sorun daha da kötüleşir. Geçmişte normalde tüm bu mantığı kontrol cihazıma koyardım:

<?php

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

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

Depo yaklaşımımla bununla sonuçlanmak istemiyorum:

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

}

Sorun # 3: Bir arayüzle eşleşmesi imkansız

Depolar için arayüzler kullanmanın faydasını görüyorum, bu yüzden uygulamamı değiştirebiliyorum (test amacıyla veya diğer için). Arayüzler hakkındaki anlayışım, bir uygulamanın izlemesi gereken bir sözleşme tanımlamalarıdır. Depolarınıza ek yöntemler eklemeye başlayana kadar bu harika bir şeyfindAllInCountry() . Şimdi bu yöntemi kullanmak için arayüzümü güncellemem gerekiyor, aksi takdirde, diğer uygulamalar buna sahip olmayabilir ve bu da uygulamamı bozabilir. Bu çılgınca hissediyor ... köpeği sallayan kuyruk vakası.

Şartname Desen?

Bu potansiyel müşteriler beni depo sadece (gibi yöntemlerle sabit sayıda olması gerektiğine inanıyoruz için save(), remove(), find(), findAll(), vs). Ama sonra belirli aramaları nasıl çalıştırabilirim? Spesifikasyon Deseni'ni duydum , ancak bana göre bu sadece bir kayıt kümesini (üzerinden IsSatisfiedBy()) azaltır, ki bu bir veritabanından çekiyorsanız açıkça önemli performans sorunları vardır.

Yardım?

Açıkçası, depolarla çalışırken bazı şeyleri yeniden düşünmem gerekiyor. Herkes bunun en iyi nasıl ele alınacağını aydınlatabilir mi?

Yanıtlar:


208

Kendi sorumu cevaplamak için bir boşluk bırakacağımı düşündüm. Aşağıda, orijinal sorumdaki 1-3 arasındaki sorunları çözmenin sadece bir yolu var.

Feragatname: Paternleri veya teknikleri açıklarken her zaman doğru terimleri kullanmayabilirim. Bunun için özür dilerim.

Hedefler:

  • Görüntüleme ve düzenleme için temel bir denetleyicinin tam bir örneğini oluşturun Users.
  • Tüm kodlar tamamen test edilebilir ve taklit edilebilir olmalıdır.
  • Denetleyicinin verilerin nerede saklandığı hakkında hiçbir fikri olmamalıdır (yani değiştirilebilir).
  • Bir SQL uygulamasını göstermek için örnek (en yaygın).
  • Maksimum performans için kontrolörler yalnızca ihtiyaç duydukları verileri almalıdır; ekstra alan olmamalıdır.
  • Uygulama, geliştirme kolaylığı için bir tür veri eşleştiriciden yararlanmalıdır.
  • Uygulama, karmaşık veri aramaları yapabilme yeteneğine sahip olmalıdır.

Çözüm

Kalıcı depolama (veritabanı) etkileşimi iki kategoriye ayırıyorum : R (Oku) ve CUD (Oluştur, Güncelle, Sil). Deneyimlerim, bir uygulamanın yavaşlamasına neden olan okumaların gerçekten olmasıydı. Ve veri manipülasyonu (CUD) aslında daha yavaş olsa da, daha az sıklıkla gerçekleşir ve bu nedenle daha az endişe duyar.

CUD (Oluştur, Güncelle, Sil) kolaydır. Bu, daha sonra kalıcılık için bana aktarılan gerçek modellerle çalışmayı içerecektir Repositories. Depolarım hala bir Okuma yöntemi sunacak, ancak yalnızca nesne oluşturma için görüntülenmeyecek. Daha sonra.

R (Oku) o kadar kolay değil. Burada model yok, sadece nesnelere değer verin . Diziler kullanın isterseniz . Bu nesneler tek bir modeli veya birçok modelin bir karışımını temsil edebilir, gerçekten herhangi bir şey. Bunlar kendi başlarına çok ilginç değiller, ancak nasıl üretildikleri. Dediğim şeyi kullanıyorum Query Objects.

Kod:

Kullanıcı Modeli

Temel kullanıcı modelimizle basit başlayalım. Hiçbir ORM genişletme veya veritabanı öğesi olmadığını unutmayın. Sadece saf model görkemi. Alıcılarınızı, ayarlayıcılarınızı, doğrulamanızı, her neyse ekleyin.

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

Havuz Arayüzü

Kullanıcı veri havuzumu oluşturmadan önce veri havuzu arayüzümü oluşturmak istiyorum. Bu, denetleyicimin kullanabilmesi için depoların izlemesi gereken "sözleşmeyi" tanımlayacaktır. Unutmayın, denetleyicim verilerin gerçekte nerede depolandığını bilemez.

Depolarımın yalnızca bu üç yöntemi içereceğini unutmayın. save()Yöntem hem oluşturma ve kullanıcıları güncellemeye sadece kullanıcı nesnesi bir kimlik kümesi olup olmadığını bağlı olarak sorumludur.

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

SQL Veri Havuzu Uygulaması

Şimdi benim arayüz uygulama oluşturmak için. Bahsettiğim gibi, örneğim bir SQL veritabanı ile olacaktı. Tekrarlayan SQL sorguları yazmak zorunda kalmamak için bir veri eşleyici kullanıldığına dikkat edin .

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

Sorgu Nesnesi Arabirimi

Şimdi depomuz tarafından ilgilenilen CUD (Oluştur, Güncelle, Sil) ile R (Oku) ' ya odaklanabiliriz . Sorgu nesneleri, bir tür veri arama mantığının kapsüllenmesidir. Bunlar değil sorgu inşaatçılar. Depomuz gibi soyutlayarak uygulamayı değiştirebilir ve daha kolay test edebiliriz. Sorgu Nesnesi örneği bir AllUsersQueryveya AllActiveUsersQuery, hatta çift olabilir MostCommonUserFirstNames.

"Veri havuzlarımda bu sorgular için yöntem oluşturamaz mıyım?" Diye düşünüyor olabilirsiniz. Evet, ama bunu neden yapmıyorum:

  • Depolarım model nesnelerle çalışmak içindir. Gerçek bir dünya uygulamasında, passwordtüm kullanıcılarımı listelemek istiyorsam neden alanı elde etmem gerekiyor ?
  • Depolar genellikle modele özgüdür, ancak sorgular genellikle birden fazla model içerir. Peki yönteminizi hangi depoya yerleştiriyorsunuz?
  • Bu, depolarımı çok basit tutar; şişirilmiş bir yöntem sınıfı değil.
  • Tüm sorgular artık kendi sınıflarında düzenlenmiştir.
  • Gerçekten, bu noktada, veri tabanları sadece veritabanı katmanımı soyutlamak için var.

Örneğim için "AllUsers" araması için bir sorgu nesnesi oluşturacağım. İşte arayüz:

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

Sorgu Nesnesi Uygulaması

Burası, veri hızlandırıcıyı geliştirmeyi hızlandırmak için tekrar kullanabiliriz. Döndürülen veri kümesine (alanlara) bir ayar yapmaya izin verdiğime dikkat edin. Bu yapılan sorgu manipüle ile gitmek istiyorum kadarıyla ilgili. Unutmayın, sorgu nesnelerim sorgu oluşturucu değildir. Sadece belirli bir sorgu gerçekleştirirler. Ancak, bunu muhtemelen çok fazla kullanacağımı bildiğim için, birkaç farklı durumda, kendime alanları belirleme yeteneği veriyorum. Asla ihtiyacım olmayan alanları iade etmek istemiyorum!

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

Denetleyiciye geçmeden önce, bunun ne kadar güçlü olduğunu göstermek için başka bir örnek göstermek istiyorum. Belki bir raporlama motorum var ve bunun için bir rapor oluşturmam gerekiyor AllOverdueAccounts. Bu benim veri haritacım için zor olabilir ve ben SQLbu durumda bazı gerçek yazmak isteyebilirsiniz . Sorun değil, işte bu sorgu nesnesi neye benzeyebilir:

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...";
    }
}

Bu, bu rapor için tüm mantığımı tek bir sınıfta tutar ve test edilmesi kolaydır. Bunu kalbimin içeriğiyle alay edebilir, hatta tamamen farklı bir uygulama kullanabilirim.

Kontrol eden, denetleyici

Şimdi eğlenceli kısım — tüm parçaları bir araya getiriyor. Bağımlılık enjeksiyonu kullandığımı unutmayın. Tipik olarak bağımlılıklar yapıcıya enjekte edilir, ancak aslında onları doğrudan denetleyici yöntemlerime (rotalara) enjekte etmeyi tercih ederim. Bu, denetleyicinin nesne grafiğini en aza indirir ve aslında daha okunaklı buluyorum. Bu yaklaşımı beğenmediyseniz, yalnızca geleneksel yapıcı yöntemini kullanın.

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

Son düşünceler:

Burada dikkat edilmesi gereken önemli noktalar, varlıkları değiştirirken (oluştururken, güncellerken veya silerken), gerçek model nesnelerle çalışıyorum ve depolarım aracılığıyla kalıcılığı gerçekleştiriyorum.

Ancak, (veri seçme ve görünümlere gönderme) görüntülerken model nesneleri ile değil, düz eski değer nesneleri ile çalışıyorum. Yalnızca ihtiyacım olan alanları seçiyorum ve veri arama performansımı en üst düzeye çıkarabileceğim şekilde tasarlandı.

Depolarım çok temiz kalıyor ve bunun yerine bu "karmaşa" model sorgularımda organize ediliyor.

Genel görevler için tekrarlayan SQL yazmak sadece saçma olduğundan, geliştirmeye yardımcı olması için bir veri eşleyici kullanıyorum. Ancak, gerektiğinde SQL yazabilirsiniz (karmaşık sorgular, raporlama vb.). Ve bunu yaptığınızda, düzgün bir şekilde adlandırılmış bir sınıfa sokulur.

Yaklaşımımı benimsediğini duymak isterim!


Temmuz 2015 Güncellemesi:

Tüm bunlarla sonuçlandığım yorumlarda bana sorulmuştur. Aslında o kadar uzakta değil. Doğrusu, hala depoları gerçekten sevmiyorum. Onları temel aramalar için aşırıya kaçmış buluyorum (özellikle zaten bir ORM kullanıyorsanız) ve daha karmaşık sorgularla çalışırken dağınık buluyorum.

Genellikle ActiveRecord tarzı ORM ile çalışıyorum, bu yüzden çoğu zaman bu modelleri doğrudan uygulamam boyunca referans göstereceğim. Ancak, daha karmaşık sorgularım olduğu durumlarda, bunları daha yeniden kullanılabilir hale getirmek için sorgu nesnelerini kullanacağım. Ayrıca, modellerimi her zaman yöntemlerime enjekte ettiğimi ve testlerimde alay etmelerini kolaylaştıracağımı not etmeliyim.


4
@PeeHaa Yine, örnekleri basit tutmaktı. Elinizdeki konu ile ilgili değilse, kod parçalarını bir örnek dışında bırakmak çok yaygındır. Gerçekte, bağımlılıklarımı geçirdim.
Jonathan

4
İlginçtir, Okuma, Güncelleme ve Silme işlemlerinizi Okuma alanınızdan ayırmanız. Resmi olarak sadece bunu yapan Komut Sorgusu Sorumluluk Ayrımı'ndan (CQRS) bahsetmeye değeceğini düşündüm. martinfowler.com/bliki/CQRS.html
Adam

2
@Jonathan Kendi sorunuzu cevaplamanızdan bu yana bir buçuk yıl geçti. Hala cevabınızdan memnun olup olmadığınızı ve projelerinizin çoğu için ana çözümünüz olup olmadığını merak ediyordum? Son birkaç haftada depolar üzerinde çok şey okudum ve insanların bir sürü nasıl uygulanması gerektiğine dair kendi yorumlarına sahip gördüm. Buna nesne çağırıyorsunuz, ama bu var olan bir kalıp değil mi? Sanırım diğer dillerde kullanıldığını gördüm.
Boedy

1
@Jonathan: Bir kullanıcının "ID" değil, örneğin "kullanıcı adı" veya birden fazla koşulu olan daha karmaşık sorgular ile sonuçlanması gereken sorguları nasıl ele alırsınız?
Gizzmo

1
@Gizzmo Sorgu nesnelerini kullanarak, daha karmaşık sorgularınıza yardımcı olmak için ek parametreler iletebilirsiniz. Örneğin, yapıcı yapabilirsiniz: new Query\ComplexUserLookup($username, $anotherCondition). Veya bunu setter yöntemleriyle yapın $query->setUsername($username);. Bunu gerçekten tasarlayabilirsiniz, ancak özel uygulamanız için mantıklıdır ve bence sorgu nesneleri burada çok fazla esneklik bırakır.
Jonathan

48

Deneyimlerime dayanarak, sorularınızın yanıtlarını aşağıda bulabilirsiniz:

S: İhtiyacımız olmayan alanları geri getirmekle nasıl başa çıkabiliriz?

C: Deneyimlerimden bu, tam sorgular ve geçici sorgularla başa çıkmak için gerçekten kaygılanıyor.

Tam varlık bir Usernesne gibidir. Özellikleri ve yöntemleri vb. Vardır. Kod tabanınızdaki birinci sınıf bir vatandaştır.

Geçici bir sorgu bazı veriler döndürür, ancak bunun ötesinde hiçbir şey bilmiyoruz. Veriler uygulamanın etrafından geçtikçe, içerik olmadan yapılır. Bu bir Usermi? Bir Userbazılarıyla Orderbilgi ekli? Gerçekten bilmiyoruz.

Tam varlıklar ile çalışmayı tercih ederim.

Sıklıkla kullanmayacağınız verileri geri getireceğiniz konusunda haklısınız, ancak bunu çeşitli şekillerde ele alabilirsiniz:

  1. Varlıkları agresif bir şekilde önbelleğe alır, böylece okunan fiyatı veritabanından yalnızca bir kez ödersiniz.
  2. Varlıklarınızı modellemek için daha fazla zaman harcayın, böylece aralarında iyi ayrımlar yapın. (Büyük bir varlığı iki küçük işletmeye vb. Bölmeyi düşünün.)
  3. Varlıkların birden çok sürümüne sahip olun. Bir olabilir Userbelki bir arka uç için ve UserSmallAJAX aramalar için. Birinin 10 özelliği ve birinin 3 özelliği olabilir.

Geçici sorgular ile çalışmanın olumsuz yanları:

  1. Pek çok sorguda temelde aynı verileri elde edersiniz. Örneğin, a ile birçok aramada Useresasen aynı select *yazılır. Bir çağrı 10 alanın 8'ini alır, bir 10'un 5'ini alır, bir 10'un 7'sini alır. Neden hepsini 10'dan 10'u alan bir çağrı ile değiştirmiyorsunuz? Bunun kötü olmasının nedeni, yeniden faktör / test / alaycılığa cinayet olmasıdır.
  2. Zaman içinde kodunuz hakkında üst düzeyde mantık yürütmek çok zorlaşır. "Neden bu Userkadar yavaş?" tek seferlik sorguları izlemenizi sağlar ve böylece hata düzeltmeleri küçük ve yerelleştirilir.
  3. Altta yatan teknolojiyi değiştirmek gerçekten zor. Şimdi MySQL'de her şeyi saklarsanız ve MongoDB'ye geçmek istiyorsanız, 100 ad-hoc çağrıyı değiştirmek bir avuç varlıktan daha zordur.

S: Depomda çok fazla yöntemim olacak.

C: Bu konuda, aramaları birleştirmekten başka bir yol görmedim. Deponuzdaki yöntem çağrıları gerçekten uygulamanızdaki özelliklere eşlenir. Daha fazla özellik, daha fazla veriye özel arama. Özellikleri geri itebilir ve benzer çağrıları bir araya getirebilirsiniz.

Günün sonundaki karmaşıklığın bir yerde olması gerekir. Bir depo modeliyle, belki bir grup saklı yordam yapmak yerine depo arayüzüne ittik.

Bazen kendime şunu söylemeliyim ki: "Bir yere vermek zorundaydı! Gümüş mermi yok."


Çok kapsamlı bir cevap için teşekkürler. Beni şimdi düşündürdün. Buradaki büyük endişem, okuduğum her şeyin değil SELECT *, sadece ihtiyacınız olan alanları seçmektir. Örneğin, bu soruya bakın . Bahsettiğiniz tüm bu ad-hock sorgularına gelince, nereden geldiğinizi kesinlikle anlıyorum. Şu anda birçoğuna sahip çok büyük bir uygulamam var. Bu benim "Peki bir yere vermek zorundaydı!" maksimum performansı seçtim. Ancak, şimdi çok farklı sorgularla uğraşıyorum.
Jonathan

1
Bir takip düşüncesi. R-CUD yaklaşımını kullanma önerisi gördüm. Yana readsperformans sorunları ortaya nerede genellikle, gerçek iş nesneleri çevirmek yok onlar için daha özel sorgu yaklaşımı, kullanabilirsiniz. Ardından için create, updateve deletebütün nesneler ile çalışan bir ORM kullanın. Bu yaklaşımla ilgili düşünceleriniz var mı?
Jonathan

1
"Select *" işlevini kullanma notu olarak. Geçmişte yaptım ve varchar (max) alanlarına çarpana kadar tamam çalıştı. Bunlar sorgularımızı öldürdü. Ints, küçük metin alanları vb. İçeren tablolarınız varsa, o kadar da kötü değil. Doğal değil, ancak yazılım bu şekilde gider. Kötü olan aniden iyiydi ve tam tersi.
ryan1234

1
R-CUD yaklaşımı aslında CQRS
MikeSW

2
@ ryan1234 "Günün sonunda karmaşıklık bir yerde var olmalı." Bunun için teşekkür ederim. Beni daha iyi hissettiriyor.
johnny

20

Aşağıdaki arayüzleri kullanıyorum:

  • Repository - varlıkları yükler, ekler, günceller ve siler
  • Selector - depoda filtrelere dayalı varlıkları bulur
  • Filter - filtreleme mantığını kapsar

Benim Repositoryveritabanı agnostik olduğunu; aslında herhangi bir kalıcılık belirtmez; herhangi bir şey olabilir: SQL veritabanı, xml dosyası, uzaktan servis, uzaydan bir uzaylı vb. Arama yetenekleri için, Repositoryyapıları Selectorfiltre edilebilir, LIMIT-ed, sıralanabilir ve sayılabilir. Sonunda, seçici Entitieskalıcılıktan bir veya daha fazlasını alır.

İşte bazı örnek kod:

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

Sonra bir uygulama:

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

İdeea jenerik Selectorkullanır Filterama uygulama SqlSelectorkullanır SqlFilter; SqlSelectorFilterAdaptergenel adapte Filterbir betona SqlFilter.

İstemci kodu Filter (genel filtreler olan) nesneler , ancak seçicinin somut uygulamasında bu filtreler SQL filtrelerinde dönüştürülür.

Diğer seçici uygulamaları gibi InMemorySelector, gelen dönüşümü Filteriçin InMemoryFilterkendi spesifik kullanılarakInMemorySelectorFilterAdapter ; bu nedenle, her seçici uygulaması kendi filtre adaptörüyle birlikte gelir.

Bu stratejiyi kullanarak müşteri kodum (bussines katmanında) belirli bir havuz veya seçici uygulamasını umursamıyor.

/** @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 Bu benim gerçek kod basitleştirme


"Depo - varlıkları yükler, ekler, günceller ve siler" bu bir "hizmet katmanı", "DAO", "BLL" yapabilirsiniz
Yousha Aleayoub

5

Şu anda tüm bunları kendim kavramaya çalıştığım için buna biraz ekleyeceğim.

1 ve 2

Bu, ORM'nizin ağır kaldırmayı yapması için mükemmel bir yerdir. Bir çeşit ORM uygulayan bir model kullanıyorsanız, sadece bu şeylerle ilgilenmek için yöntemlerini kullanabilirsiniz. Gerekirse Eloquent yöntemlerini uygulayan işlevlerle. Örneğin Eloquent kullanımı:

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

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

Aradığın şey bir ORM. Deponuzun bir tanesine dayanamamasının nedeni yok. Bu, Kullanıcı'nın anlamlı bir şekilde genişletilmesini gerektirir, ancak kişisel olarak bunu bir sorun olarak görmüyorum.

Ancak bir ORM'den kaçınmak isterseniz, aradığınız şeyi elde etmek için "kendinizinkini yuvarlamanız" gerekir.

3.

Arayüzlerin zor ve hızlı gereksinimler olması gerekmez. Bir şey bir arabirim uygulayabilir ve ona ekleyebilir. Yapamayacağı şey, o arayüzün gerekli bir fonksiyonunu yerine getirmemek. Ayrıca şeyleri KURU tutmak için sınıflar gibi arayüzleri genişletebilirsiniz.

Dedi ki, sadece kavramaya başladım, ama bu gerçekleşmeler bana yardımcı oldu.


1
Bu yöntem hakkında sevmediğim bir MongoUserRepository varsa, o ve DbUserRepository farklı nesneleri dönecek olmasıdır. Bir Eloquent \ Modeli ve Mongo kendi başına bir şey döndüren Db. Elbette daha iyi bir uygulama, her iki havuzun ayrı bir Varlık \ Kullanıcı sınıfının örneklerini / koleksiyonlarını döndürmesini sağlamaktır. Bu şekilde MongoRepository
danharper

1
Bu konuda kesinlikle aynı fikirdeyim. Bunu önlemek için ne yapacağımı asla bu yöntemleri Eloquent gerektiren sınıfın dışında kullanmaktır. Bu nedenle get işlevi muhtemelen özel olmalı ve işaret ettiğiniz gibi diğer depoların yapamayacağı bir şey döndüreceği için sınıf içinde kullanılmalıdır.
Will

3

Sadece (şirketimde) bununla nasıl başa çıktığımız hakkında yorum yapabilirim. Her şeyden önce performans bizim için çok fazla bir sorun değil, temiz / uygun kodlara sahip olmak.

Her şeyden önce UserModel, UserEntitynesneler oluşturmak için ORM kullanan bir Modeller tanımlarız . Bir UserEntitymodelden a yüklendiğinde tüm alanlar yüklenir. Yabancı varlıkları referans alan alanlarda, ilgili varlıkları oluşturmak için uygun yabancı modeli kullanırız. Bu varlıklar için veriler isteğe bağlı olarak yüklenecektir. Şimdi ilk tepkiniz ... ??? ... !!! size bir örnek vereyim:

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)
    {
        //...
    }
}

Bizim durumumuzda $dbvarlıkları yükleyebilen bir ORM var. Model, ORM'ye belirli bir tür varlıkları yüklemesini bildirir. ORM bir eşleme içerir ve bunu ilgili varlığa ait tüm alanları varlığa enjekte etmek için kullanır. Ancak yabancı alanlar için yalnızca bu nesnelerin kimlikleri yüklenir. Bu durumda, yalnızca başvurulan siparişlerin kimliğiyle OrderModeloluşturur OrderEntity. Ne zaman PersistentEntity::getFieldtarafından çağrılır OrderEntityvarlık tüm alanları içine tembel yüke 's modelini talimatını OrderEntitys. OrderEntityBir UserEntity ile ilişkili tüm s bir sonuç kümesi olarak kabul edilir ve bir kerede yüklenir.

Buradaki sihir, modelimizin ve ORM'in tüm verileri varlıklara enjekte etmesi ve varlıkların yalnızca getFieldsağlanan genel yöntem için sarıcı işlevler sağlamasıdır PersistentEntity. Özetlemek gerekirse her zaman tüm alanları yükleriz, ancak yabancı bir varlığı referans alan alanlar gerektiğinde yüklenir. Sadece bir grup alanı yüklemek gerçekten bir performans sorunu değildir. Mümkün olan tüm yabancı varlıkları yükleyin, BÜYÜK bir performans düşüşü olacaktır.

Şimdi nerede bir maddeye dayalı olarak belirli bir kullanıcı kümesini yüklemeye devam ediyoruz. Birlikte yapıştırılabilecek basit ifadeleri belirtmenize olanak tanıyan nesne tabanlı bir sınıf paketi sunuyoruz. Örnek kodda adını verdim GetOptions. Seçme sorgusu için olası tüm seçenekler için bir sarıcıdır. Nerede bir cümle, bir cümle bir grup ve diğer her şeyi içerir. Nerede cümleciklerimiz oldukça karmaşıktır, ancak açık bir şekilde kolayca daha basit bir sürüm yapabilirsiniz.

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

Bu sistemin en basit sürümü, sorgunun WHERE kısmını doğrudan modele bir dize olarak geçirmektir.

Bu oldukça karmaşık cevap için özür dilerim. Çerçevemizi mümkün olduğunca hızlı ve açık bir şekilde özetlemeye çalıştım. Başka sorularınız varsa, bunları sormaktan çekinmeyin, cevabımı güncelleyeceğim.

EDIT: Ayrıca bazı alanları hemen yüklemek istemiyorsanız, ORM eşlemenizde tembel bir yükleme seçeneği belirleyebilirsiniz. Tüm alanlar sonunda getFieldyöntemle yüklendiğinden, bu yöntemi çağırdığınızda bazı alanları son dakika yükleyebilirsiniz. Bu PHP çok büyük bir sorun değil, ama diğer sistemler için tavsiye etmem.


3

Bunlar gördüğüm bazı farklı çözümler. Her birinin artıları ve eksileri vardır, ancak karar vermeniz gerekir.

Sorun # 1: Çok fazla alan

Bu, özellikle Yalnızca Dizin Taramalarını dikkate aldığınızda önemli bir husustur . Bu sorunla başa çıkmak için iki çözüm görüyorum. İşlevlerinizi, döndürülecek sütunların listesini içeren isteğe bağlı bir dizi parametresi alacak şekilde güncelleyebilirsiniz. Bu parametre boşsa, sorgudaki tüm sütunları döndürürsünüz. Bu biraz garip olabilir; parametreye dayanarak bir nesneyi veya diziyi alabilirsiniz. Aynı sorguyu çalıştıran iki ayrı işleve sahip olmanız için tüm işlevlerinizi çoğaltabilirsiniz, ancak biri bir dizi sütun döndürür ve diğeri bir nesneyi döndürür.

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

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

Sorun # 2: Çok fazla yöntem

Bir yıl önce Propel ORM ile kısaca çalıştım ve bu, bu deneyimden hatırlayabildiğim şeylere dayanıyor. Propel, mevcut veritabanı şemasını temel alarak sınıf yapısını oluşturma seçeneğine sahiptir. Her tablo için iki nesne oluşturur. İlk nesne, şu anda listelediklerinize benzer uzun bir erişim işlevi listesidir; findByAttribute($attribute_value). Bir sonraki nesne bu ilk nesneden miras alır. Daha karmaşık alıcı işlevlerinizi oluşturmak için bu alt nesneyi güncelleyebilirsiniz.

Başka bir çözüm, __call()tanımlanmamış işlevleri eyleme geçirilebilir bir şeyle eşlemek için kullanmak olacaktır . Sizin __callolacağını yöntem farklı sorgular içine findById ve FindByName ayrıştırmak mümkün olacaktır.

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]
    }
}

Umarım bu en azından bazılarına yardımcı olur.



0

@ Ryan1234 ile kod içinde tam nesnelerin etrafından geçmeniz ve bu nesneleri almak için genel sorgu yöntemlerini kullanmanız gerektiğini kabul ediyorum.

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

Harici / uç nokta kullanımı için GraphQL yöntemini çok seviyorum.

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

0

Sorun # 3: Bir arayüzle eşleşmesi imkansız

Depolar için arayüzler kullanmanın faydasını görüyorum, bu yüzden uygulamamı değiştirebiliyorum (test amacıyla veya diğer için). Arayüzler hakkındaki anlayışım, bir uygulamanın izlemesi gereken bir sözleşme tanımlamalarıdır. Havuzlarınıza findAllInCountry () gibi ek yöntemler eklemeye başlayana kadar bu harika bir şeydir. Şimdi bu yöntemi kullanmak için arayüzümü güncellemem gerekiyor, aksi takdirde, diğer uygulamalar buna sahip olmayabilir ve bu da uygulamamı bozabilir. Bu çılgınca hissediyor ... köpeği sallayan kuyruk vakası.

Benim bağırsak bana bu belki genel yöntemler yanında sorgu optimize edilmiş yöntemler uygulayan bir arabirim gerektirir söylüyor. Performansa duyarlı sorgular hedeflenen yöntemlere sahip olmalıdır, ancak sık olmayan veya hafif sorgular genel bir işleyici tarafından işlenirken, denetleyicinin maliyeti biraz daha hokkabazlık yapıyor olabilir.

Genel yöntemler, herhangi bir sorgunun uygulanmasına izin verir ve böylece bir geçiş döneminde değişikliklerin bozulmasını önler. Hedeflenen yöntemler, bir çağrıyı anlamlı hale getirdiğinizde optimize etmenize olanak tanır ve birden fazla servis sağlayıcıya uygulanabilir.

Bu yaklaşım, belirli optimize edilmiş görevleri yerine getiren donanım uygulamalarına benzerken, yazılım uygulamaları hafif veya esnek uygulama yapar.


0

Ben grafik veri veri depolarının karmaşıklığını artırmak olmadan büyük ölçekli bir sorgu dili sağlamak için böyle bir durumda iyi bir aday olduğunu düşünüyorum .

Ancak, şu anda graphQL için gitmek istemiyorsanız başka bir çözüm var. Bir nesnenin işlemler arasında, bu durumda hizmet / denetleyici ve depo arasında veri taşımak için kullanıldığı bir DTO kullanarak .

Yukarıda zarif bir cevap zaten verilmiştir, ancak bunun daha basit olduğunu ve yeni bir proje için başlangıç ​​noktası olabileceğini düşündüğüm başka bir örnek vermeye çalışacağım.

Kodda gösterildiği gibi, CRUD işlemleri için sadece 4 yönteme ihtiyacımız var. findyöntem listeleme ve nesne argüman ileterek okunması için kullanılacak. Arka uç hizmetleri, tanımlanan sorgu nesnesini bir URL sorgu dizesine veya belirli parametrelere dayalı olarak oluşturabilir.

Sorgu nesnesi ( SomeQueryDto) de gerekirse belirli bir arabirim uygulayabilir. ve karmaşıklık eklemeden daha sonra genişletilmesi kolaydır.

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

Örnek kullanım:

$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);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.