Prawidłowy projekt wzorca repozytorium w PHP?

Przedmowa: próbuję użyć wzorca repozytorium w architekturze MVC z relacyjnymi bazami danych.

Niedawno zacząłem uczyć się TDD w PHP i zdaję sobie sprawę, że moja baza danych jest zbyt blisko połączona z resztą mojej aplikacji. Czytałem o repozytoriach i używaniu kontenera IoC, aby "wstrzyknąć" go do moich kontrolerów. Bardzo fajne rzeczy. Ale teraz masz kilka praktycznych pytań dotyczących projektowania repozytoriów. Rozważ następujące przykład.

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

Problem # 1: zbyt wiele pól

Wszystkie te metody find wykorzystują metodę select all fields (SELECT *). Jednak w moich aplikacjach zawsze staram się ograniczyć liczbę pól, które otrzymuję, ponieważ często Zwiększa to koszty ogólne i spowalnia działanie. Jak sobie z tym poradzić?

Problem # 2: zbyt wiele metod

Chociaż ta klasa wygląda teraz ładnie, wiem, że w prawdziwej aplikacji potrzebuję o wiele więcej metod. Na przykład:

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findallbyageandgenderoorderbyage
  • itd.

Jak widzisz, może być bardzo, bardzo długa lista możliwych metod. A następnie, jeśli dodasz powyżej problem z wyborem pola, problem się pogarsza. W przeszłości normalnie po prostu włożyłem całą tę logikę do mojego kontrolera:

<?php

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

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

Z moim repozytorium podejscie, nie chce sie z tym skonczyc:

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

}
Problem # 3: niemożliwe dopasowanie interfejsu]}

Widzę korzyści w używaniu interfejsów dla repozytoriów, więc mogę wymienić swoją implementację (do celów testowych lub innych). Rozumiem, że interfejsy definiują umowę, której realizacja musi przestrzegać. Jest to świetne, dopóki nie zaczniesz dodawać dodatkowych metod do swoich repozytoriów, takich jak findAllInCountry(). Teraz muszę zaktualizować mój interfejs, aby również mieć tę metodę, w przeciwnym razie inne implementacje mogą tego nie mieć, a to może złamać moją aplikację. Przez to czuje się szalony...przypadek machania ogonem psa.

Wzór Specyfikacji?

To prowadzi mnie do przekonania, że repozytorium powinno mieć tylko określoną liczbę metod (takich jak save(), remove(), find(), findAll(), itp.). Ale jak uruchomić konkretne wyszukiwania? Słyszałem o wzorze specyfikacji , ale wydaje mi się, że zmniejsza to tylko cały zestaw rekordów (poprzez IsSatisfiedBy()), które wyraźnie ma poważne problemy z wydajnością, jeśli wyciągasz z bazy danych.

Pomocy?

Oczywiście, muszę przemyśleć kilka rzeczy podczas pracy z repozytoriami. Czy ktoś może oświecić, jak najlepiej to załatwić?

Author: Karl Hill, 2013-04-23

11 answers

Pomyślałem, że spróbuję odpowiedzieć na własne pytanie. Poniżej znajduje się tylko jeden sposób rozwiązania problemów 1-3 w moim pierwotnym pytaniu.

Zastrzeżenie: nie zawsze mogę używać właściwych terminów przy opisywaniu wzorców lub technik. Przepraszam za to.

Cele:

  • Utwórz kompletny przykład podstawowego kontrolera do przeglądania i edycji Users.
  • cały kod musi być w pełni testowalny i dający się wyśmiewać.
  • kontroler nie powinien mieć pomysł, gdzie dane są przechowywane (co oznacza, że można je zmienić).
  • przykład pokazujący implementację SQL (najczęściej).
  • Aby uzyskać maksymalną wydajność, Kontrolery powinny otrzymywać tylko potrzebne dane-bez dodatkowych pól.
  • implementacja powinna wykorzystywać pewien rodzaj mapera danych dla ułatwienia rozwoju.
  • implementacja powinna mieć możliwość wykonywania skomplikowanych poszukiwań danych.

Rozwiązanie

Dzielę mój trwały magazyn (baza danych) interakcja na dwie kategorie: R (Czytaj) i CUD (Utwórz, zaktualizuj, Usuń). Z mojego doświadczenia wynika, że odczyty są naprawdę tym, co powoduje spowolnienie aplikacji. I chociaż manipulacja danymi (CUD) jest w rzeczywistości wolniejsza, zdarza się znacznie rzadziej, a zatem jest znacznie mniej niepokojąca.

CUD (Tworzenie, aktualizacja, usuwanie) jest łatwe. Będzie to wymagało pracy z rzeczywistymi modelami , które następnie są przekazywane do mojego Repositories dla wytrwałości. Uwaga, mój repozytoria nadal będą dostarczać metodę odczytu, ale tylko do tworzenia obiektów, a nie wyświetlania. Więcej na ten temat później.

R (Czytaj) nie jest takie proste. Nie ma tu modeli, tylko obiekty wartości . Użyj tablic , jeśli wolisz . Obiekty te mogą reprezentować pojedynczy model lub mieszankę wielu modeli, cokolwiek naprawdę. Nie są one bardzo interesujące same w sobie, ale jak są generowane jest. Używam tego, co nazywam Query Objects.

Kod:

Użytkownik Model

Zacznijmy od prostego z naszym podstawowym modelem użytkownika. Zauważ, że w ogóle nie ma rozszerzenia ORM ani bazy danych. Tylko czysta chwała modelki. Dodaj swoje gettery, settery, walidację, cokolwiek.

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

Interfejs Repozytorium

Zanim stworzę repozytorium użytkownika, chcę utworzyć interfejs repozytorium. Zdefiniuje to "umowę", której muszą przestrzegać repozytoria, aby mogły być używane przez mojego kontrolera. Pamiętaj, że mój kontroler nie będzie wiedział, gdzie są dane przechowywany.

Zauważ, że moje repozytoria będą zawierały tylko te trzy metody. Metoda save() jest odpowiedzialna zarówno za tworzenie, jak i aktualizowanie użytkowników, po prostu w zależności od tego, czy obiekt user ma ustawiony identyfikator.

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

Implementacja repozytorium SQL

Teraz do stworzenia mojej implementacji interfejsu. Jak wspomniano, mój przykład miał być z bazą danych SQL. Należy zwrócić uwagę na użycie data mapper , aby zapobiec konieczności pisania powtarzalnego SQL zapytania.

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

Interfejs Obiektu Zapytania

Teraz z CUD (Create, Update, Delete) zadbanym przez nasze repozytorium, możemy skupić się na R (Read). Obiekty zapytań są po prostu enkapsulacją pewnego rodzaju logiki wyszukiwania danych. Są tonie konstruktorzy zapytań. Abstrakując go jak nasze repozytorium, możemy zmienić jego implementację i łatwiej przetestować. Przykładem obiektu zapytania może być AllUsersQuery lub AllActiveUsersQuery, a nawet MostCommonUserFirstNames.

Możesz być myślenie "czy nie mogę po prostu tworzyć metod w moich repozytoriach dla tych zapytań?"Tak, ale oto dlaczego tego nie robię:

  • moje repozytoria są przeznaczone do pracy z obiektami modelowymi. W aplikacji w prawdziwym świecie, po co miałbym mieć pole password, Jeśli chcę wymienić wszystkich moich użytkowników?
  • repozytoria są często specyficzne dla modelu, jednak zapytania często dotyczą więcej niż jednego modelu. W jakim repozytorium umieścisz swoją metodę?
  • Dzięki temu moje repozytoria są bardzo proste-nie nadęta Klasa metod.
  • wszystkie zapytania są teraz zorganizowane w swoje własne klasy.
  • naprawdę, w tym momencie, repozytoria istnieją po prostu po to, aby wyodrębnić moją warstwę bazy danych.

W moim przykładzie stworzę obiekt zapytania do wyszukiwania "AllUsers". Oto interfejs:

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

Implementacja Obiektu Zapytania

Tutaj możemy ponownie użyć mapera danych, aby przyspieszyć rozwój. Zauważ, że zezwalam na jedno dostosowanie do zwracanego zbioru danych-the pola. To jest tak daleko, jak chcę iść z manipulowania wykonywanym zapytaniem. Pamiętaj, że moje obiekty zapytań nie są konstruktorami zapytań. Po prostu wykonują konkretne zapytanie. Ponieważ jednak Wiem, że prawdopodobnie będę z niego często korzystał, w wielu różnych sytuacjach daję sobie możliwość określenia pól. Nigdy nie chcę zwracać pól, których nie potrzebuję!

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

Zanim przejdę do kontrolera, chcę pokazać inny przykład, aby zilustrować, jak potężne jest to. Może mam silnik raportowania i muszę utworzyć raport dla AllOverdueAccounts. To może być trudne z moim maperem danych, i może będę chciał napisać kilka rzeczywistych SQL w tej sytuacji. Nie ma problemu, oto jak może wyglądać ten obiekt zapytania:

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

To ładnie trzyma całą moją logikę do tego raportu w jednej klasie, i jest łatwe do przetestowania. Mogę wyśmiewać się z tego do głębi serca, a nawet użyć zupełnie innej realizacji.

Kontroler

Teraz zabawna część-sprowadzanie wszystkich kawałki razem. Zauważ, że używam dependency injection. Zazwyczaj zależności są wtryskiwane do konstruktora, ale w rzeczywistości wolę wstrzyknąć je bezpośrednio do moich metod kontrolera (tras). Minimalizuje to Wykres obiektowy kontrolera i uważam go za bardziej czytelny. Zauważ, że jeśli nie podoba ci się to podejście, po prostu użyj tradycyjnej metody konstruktora.

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

Myśli Końcowe:

Ważne jest to, że podczas modyfikowania (tworzenia, aktualizowanie lub usuwanie) encji, pracuję z prawdziwymi obiektami modelu i wykonuję persistance poprzez moje repozytoria.

Jednak podczas wyświetlania (wybierania danych i wysyłania ich do widoków) nie pracuję z obiektami modelu, a raczej zwykłymi obiektami wartości. Wybieram tylko pola, których potrzebuję, i jest zaprojektowany tak, abym mógł uzyskać maksymalną wydajność wyszukiwania danych.

Moje repozytoria pozostają bardzo czyste, a zamiast tego ten "bałagan" jest zorganizowany w moim modelu zapytania.

Używam mapera danych, aby pomóc w rozwoju, ponieważ pisanie powtarzalnego SQL dla typowych zadań jest po prostu śmieszne. Jednak absolutnie możesz pisać SQL tam, gdzie jest to potrzebne (skomplikowane zapytania, raportowanie itp.). A kiedy to zrobisz, jest ładnie schowany w odpowiednio nazwanej klasie.

Chciałbym usłyszeć twoje zdanie na temat mojego podejścia!

Lipiec 2015 Aktualizacja:

Zostałem zapytany w komentarzach, gdzie skończyłem z tym wszystkim. Dobrze., nie aż tak daleko. Prawdę mówiąc, nadal nie lubię repozytoriów. Uważam je za przesadę dla podstawowych wyszukiwań (zwłaszcza jeśli używasz już ORM) i bałagan podczas pracy z bardziej skomplikowanymi zapytaniami.

Generalnie pracuję z ORM w stylu ActiveRecord, więc najczęściej będę odwoływał się do tych modeli bezpośrednio w całej mojej aplikacji. Jednak w sytuacjach, w których mam bardziej złożone zapytania, użyję obiektów zapytania, aby uczynić je bardziej wielokrotnego użytku. Należy również zauważyć, że zawsze wprowadzaj moje modele do moich metod, ułatwiając im wyśmiewanie w moich testach.

 220
Author: Jonathan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:29

Bazując na moim doświadczeniu, oto kilka odpowiedzi na twoje pytania:

P: jak radzimy sobie z przywracaniem pól, których nie potrzebujemy?

O: z mojego doświadczenia wynika, że naprawdę sprowadza się to do czynienia z kompletnymi podmiotami kontra zapytaniami ad-hoc.

Kompletny byt jest czymś w rodzaju User obiektu. Ma właściwości i metody itp. To obywatel pierwszej klasy w Twojej bazie kodów.

Zapytanie ad-hoc zwraca niektóre dane, ale nie wiemy wszystko poza tym. Gdy dane są przekazywane wokół aplikacji, odbywa się to bez kontekstu. Czy to User? User z dołączonymi informacjami Order? Nie wiemy.

Wolę pracować z pełnymi bytami.

Masz rację, że często przywozisz dane, których nie użyjesz, ale możesz się tym zająć na różne sposoby:

  1. agresywnie buforuj encje, więc płacisz cenę odczytu tylko raz z bazy danych.
  2. poświęć więcej czasu na modelowanie Twoje byty, więc mają dobre rozróżnienia między nimi. (Rozważ podzielenie dużego podmiotu na dwa mniejsze podmioty itp.)
  3. rozważ posiadanie wielu wersji Bytów. Możesz mieć User dla back endu i może UserSmall dla połączeń AJAX. Jeden może mieć 10 Właściwości, a jeden ma 3 właściwości.

Wady pracy z zapytaniami ad-hoc:

  1. otrzymujesz zasadniczo te same dane w wielu zapytaniach. Na przykład, za pomocą User, zakończysz up pisząc zasadniczo to samo select * dla wielu połączeń. Jedno połączenie dostanie 8 z 10 pól, jedno dostanie 5 z 10, jedno dostanie 7 z 10. Dlaczego nie zastąpić wszystkich jednym telefonem, który dostaje 10 na 10? Powodem, dla którego jest to złe, jest to, że morderstwo jest ponowne czynnik / test/wyśmiewanie.
  2. z czasem bardzo trudno jest na wysokim poziomie rozumować o Twoim kodzie. Zamiast wypowiedzi typu " Dlaczego User jest tak wolno?"w końcu śledzisz jednorazowe zapytania, więc poprawki błędów są małe i zlokalizowane.
  3. Naprawdę trudno jest zastąpić podstawową technologię. Jeśli teraz przechowujesz wszystko w MySQL i chcesz przenieść się do MongoDB, o wiele trudniej jest zastąpić 100 wywołań ad-hoc niż garstka podmiotów.

P: będę miał zbyt wiele metod w moim repozytorium.

Odp: [13] Wywołania metody w repozytorium naprawdę mapują do funkcji w aplikacji. Im więcej funkcji, tym więcej połączeń specyficznych dla danych. Możesz odepchnąć funkcje i spróbować połączyć podobne połączenia w jeden.

Złożoność na końcu dnia musi gdzieś istnieć. Dzięki wzorcowi repozytorium wepchnęliśmy go do interfejsu repozytorium, zamiast być może tworzyć kilka procedur składowanych.

Czasami muszę sobie powiedzieć, " Cóż to musiało dać gdzieś! Nie ma srebrnych kul."

 51
Author: ryan1234,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-23 22:33:47

Używam następujących interfejsów:

  • Repository - ładuje, wstawia, aktualizuje i usuwa encje
  • Selector - wyszukuje encje na podstawie filtrów, w repozytorium
  • Filter - enkapsuluje logikę filtrowania

Mój Repository jest agnostykiem bazy danych; w rzeczywistości nie określa żadnej trwałości; może to być cokolwiek: baza danych SQL, plik xml, usługa zdalna, obcy z kosmosu itp. Dla możliwości wyszukiwania, Repository konstruuje Selector, które mogą być filtrowane, LIMIT-ed, sortowane i liczone. Na końcu selektor pobiera jedną lub więcej Entities z persistence.

Oto przykładowy 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();
}

Następnie jedna implementacja:

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

Ideea polega na tym, że rodzajnik Selector używa Filter, ale implementacja SqlSelector używa SqlFilter; SqlSelectorFilterAdapter dostosowuje rodzajnik Filter do konkretnego SqlFilter.

Kod klienta tworzy Filter obiekty (które są typowymi filtrami), ale w konkretnej implementacji selektora filtry te są przekształcane w filtry SQL.

Inne implementacje selektora, takie jak InMemorySelector, przekształcają się z Filter do InMemoryFilter używając ich specyficznego InMemorySelectorFilterAdapter; Tak więc każda implementacja selektora ma swój własny adapter Filtra.

Używając tej strategii mój kod klienta (w warstwie bussines) nie dba o konkretne repozytorium czy implementację selektora.

/** @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();

P. S. To jest uproszczenie mojego prawdziwego kodu

 22
Author: Constantin Galbenu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-18 09:45:31

Dodam trochę na ten temat, ponieważ obecnie staram się zrozumieć to wszystko sam.

#1 i 2

To idealne miejsce dla ORM do podnoszenia ciężarów. Jeśli używasz modelu, który implementuje jakiś ORM, możesz po prostu użyć jego metod, aby zająć się tymi rzeczami. Zrób własne orderBy funkcje, które implementują wymowne metody, jeśli trzeba. Użycie elokwentne na przykład:
class DbUserRepository implements UserRepositoryInterface
{
    public function findAll()
    {
        return User::all();
    }

    public function get(Array $columns)
    {
       return User::select($columns);
    }
Wygląda na to, że szukasz ORM. No reason your Repozytorium nie może być oparte na jednym. To wymagałoby User extend eloquent, ale ja osobiście nie widzę w tym problemu.

Jeśli jednak chcesz uniknąć ORM, będziesz musiał "rzucić się", aby uzyskać to, czego szukasz.

#3

Interfejsy nie powinny być trudne i szybkie. Coś może zaimplementować interfejs i dodać do niego. To, czego nie może zrobić, to nie zaimplementować wymaganej funkcji tego interfejsu. Można również rozszerzyć interfejsy takie jak zajęcia, aby wszystko było suche.

To powiedziawszy, dopiero zaczynam rozumieć, ale te realizacje mi pomogły.
 5
Author: Will,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-25 21:06:51

[16]}mogę tylko skomentować sposób, w jaki (w mojej firmie) sobie z tym radzimy. Po pierwsze wydajność nie jest dla nas zbyt dużym problemem, ale posiadanie czystego/właściwego kodu jest.

Przede wszystkim definiujemy modele takie jak UserModel, które używają ORM do tworzenia UserEntity obiektów. Gdy UserEntity jest ładowany z modelu, wszystkie pola są ładowane. W przypadku pól odnoszących się do podmiotów zagranicznych używamy odpowiedniego modelu zagranicznego do tworzenia odpowiednich podmiotów. Dla tych podmiotów dane zostaną załadowane ondemand. Teraz Twoja początkowa reakcja może być ...???...!!! podam przykład:

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

W naszym przypadku $db jest ORM, który jest w stanie załadować byty. Model nakazuje ORM załadować zestaw jednostek określonego typu. ORM zawiera mapowanie i używa go do wprowadzania wszystkich pól dla tego encji do encji. W przypadku pól obcych ładowane są tylko identyfikatory tych obiektów. W tym przypadku OrderModel tworzy OrderEntity s tylko z identyfikatorami odwołanych rozkazów. Gdy PersistentEntity::getField zostanie wywołana przez OrderEntity, encja poleca modelowi leniwe ładowanie wszystkich pól do OrderEntity s. Wszystkie OrderEntity s związane z jednym użytkownikiem są traktowane jako jeden zestaw wyników i będą ładowane jednocześnie.

Magia polega na tym, że nasz model i ORM wstrzykują wszystkie dane do encji i że encje dostarczają jedynie funkcje wrappera dla ogólnej metody getField dostarczanej przez PersistentEntity. Podsumowując zawsze ładujemy wszystkie pola, ale pola odnoszące się do obcego podmiotu są załadowany w razie potrzeby. Samo ładowanie kilku pól nie jest tak naprawdę problemem z wydajnością. Załadować wszystkie możliwe podmioty zagraniczne jednak byłby ogromny spadek wydajności.

Przejdźmy teraz do ładowania określonego zestawu użytkowników, opartego na klauzuli where. Zapewniamy zorientowany obiektowo pakiet klas, które pozwalają określić proste wyrażenie, które można sklejać. W przykładowym kodzie nazwałem go GetOptions. Jest to opakowanie dla wszystkich możliwych opcji dla zapytania select. Zawiera zbiór gdzie klauzule, Grupa po klauzuli I Wszystko inne. Nasze klauzule where są dość skomplikowane, ale oczywiście można łatwo zrobić prostszą wersję.

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

Najprostszą wersją tego systemu byłoby przekazanie części WHERE zapytania jako ciągu znaków bezpośrednio do modelu.

Przepraszam za tę dość skomplikowaną odpowiedź. Starałem się podsumować nasze ramy tak szybko i klarownie, jak to możliwe. Jeśli masz jakieś dodatkowe pytania, możesz je zadać, a ja zaktualizuję mój odpowiedz.

EDIT: Dodatkowo, jeśli naprawdę nie chcesz od razu ładować niektórych pól, możesz określić opcję leniwego ładowania w mapowaniu ORM. Ponieważ wszystkie pola są ostatecznie ładowane za pomocą metody getField, można załadować niektóre pola w ostatniej chwili, gdy ta metoda jest wywoływana. Nie jest to zbyt duży problem w PHP, ale nie polecałbym innych systemów.

 3
Author: TFennis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-25 14:26:17

To są różne rozwiązania, które widziałem. Są plusy i minusy każdego z nich, ale to do ciebie należy decyzja.

Problem # 1: zbyt wiele pól

Jest to ważny aspekt, szczególnie jeśli wziąć pod uwagę skany tylko indeksowe. Widzę dwa rozwiązania tego problemu. Możesz zaktualizować swoje funkcje, aby przyjąć opcjonalny parametr array, który zawierałby listę kolumn, które mają zostać zwrócone. Jeśli ten parametr jest pusty, zwrócisz wszystkie kolumny w zapytanie. Może to być trochę dziwne; na podstawie parametru można pobrać obiekt lub tablicę. Można również zduplikować wszystkie funkcje, aby mieć dwie różne funkcje, które uruchamiają to samo zapytanie, ale jedna zwraca tablicę kolumn, a druga zwraca obiekt.

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

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

Problem # 2: zbyt wiele metod

Krótko pracowałem z Propel ORM rok temu i jest to oparte na tym, co pamiętam z tego doświadczenia. Propel ma możliwość generowania jego struktura klas oparta na istniejącym schemacie bazy danych. Tworzy dwa obiekty dla każdej tabeli. Pierwszy obiekt to długa lista funkcji dostępu, podobna do tej, którą aktualnie wymieniasz; findByAttribute($attribute_value). Następny obiekt dziedziczy z tego pierwszego obiektu. Ten obiekt potomny można zaktualizować, aby wbudować bardziej złożone funkcje gettera.

Innym rozwiązaniem byłoby użycie __call() do mapowania nie zdefiniowanych funkcji do czegoś możliwego do zastosowania. Twoja metoda __call będzie w stanie przetworzyć findById i findByName w różnych zapytaniach.

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]
    }
}
Mam nadzieję, że to chociaż trochę pomoże.
 3
Author: Logan Bailey,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-25 14:58:58

Proponuję https://packagist.org/packages/prettus/l5-repository jako sprzedawca do implementacji repozytoriów / kryteriów itp ... w Laravel5: d

 0
Author: abenevaut,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-11-12 17:47:47

Zgadzam się z @ryan1234, że powinieneś przekazywać kompletne obiekty w kodzie i używać ogólnych metod zapytań, aby uzyskać te obiekty.

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

Do użytku zewnętrznego / końcowego bardzo podoba mi się metoda GraphQL.

POST /api/graphql
{
    query: {
        Model(attr1: 'val1') {
            attr2
            attr3
        }
    }
}
 0
Author: AVProgrammer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-12-22 00:57:39
Problem # 3: niemożliwe dopasowanie interfejsu

Widzę korzyści w korzystaniu z interfejsów dla repozytoriów, więc mogę zamienić z mojej implementacji (do celów testowych lub innych). My rozumienie interfejsów polega na tym, że definiują one umowę, która wdrożenie musi nastąpić. To jest świetne, dopóki nie zaczniesz dodawać dodatkowe metody do repozytoriów, takie jak findAllInCountry (). Teraz Ja trzeba zaktualizować mój interfejs, aby również mieć tę metodę, w przeciwnym razie, Inne implementacje mogą tego nie mieć, a to może złamać moją aplikację. Przez to czuje się szalony...przypadek machania ogonem psa.

Moje przeczucie mówi mi, że może to wymagać interfejsu, który implementuje metody zoptymalizowane pod kątem zapytań obok metod ogólnych. Zapytania wrażliwe na wydajność powinny mieć ukierunkowane metody, podczas gdy rzadkie lub lekkie zapytania są obsługiwane przez ogólny handler, być może koszt kontrolera robi trochę więcej żonglowania.

The generic metody pozwoliłyby na implementację dowolnych zapytań, a tym samym zapobiegłyby łamaniu zmian w okresie przejściowym. Ukierunkowane metody pozwalają zoptymalizować połączenie, gdy ma to sens i może być stosowane do wielu dostawców usług.

Takie podejście byłoby podobne do implementacji sprzętowych wykonujących określone zoptymalizowane zadania, podczas gdy implementacje oprogramowania wykonują lekką pracę lub elastyczną implementację.

 0
Author: Brian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-02-03 23:05:01

Myślę, że graphQL jest dobrym kandydatem w takim przypadku do zapewnienia języka zapytań na dużą skalę bez zwiększania złożoności repozytoriów danych.

Jest jednak inne rozwiązanie, jeśli na razie nie chcesz korzystać z graphQL. Za pomocą DTO, gdzie obiekt jest używany do przenoszenia danych między procesami, w tym przypadku między usługą/kontrolerem a repozytorium.

Elegancka ODPOWIEDŹ jest już podana powyżej, jednak postaram się podaj inny przykład, który moim zdaniem jest prostszy i może służyć jako punkt wyjścia do nowego projektu.

Jak pokazano w kodzie, potrzebujemy tylko 4 metod do operacji CRUD. Metoda find będzie używana do wyświetlania i odczytu przez przekazanie argumentu obiektu. Usługi zaplecza mogą budować zdefiniowany obiekt zapytania na podstawie łańcucha zapytania URL lub na podstawie określonych parametrów.

Obiekt zapytania (SomeQueryDto) może również zaimplementować określony interfejs w razie potrzeby. i jest łatwy do rozszerzenia później bez dodawania złożoności.

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

Przykładowe użycie:

$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);
 0
Author: kordy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-03-03 17:06:57
   class Criteria {}
   class Select {}
   class Count {}
   class Delete {}
   class Update {}
   class FieldFilter {}
   class InArrayFilter {}
   // ...

   $crit = new Criteria();  
   $filter = new FieldFilter();
   $filter->set($criteria, $entity, $property, $value);
   $select = new Select($criteria);
   $count = new Count($criteria);
   $count->getRowCount();
   $select->fetchOne(); // fetchAll();

Więc myślę

 0
Author: Sudo,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-29 21:18:13