Właściwy projekt wzorca repozytorium w PHP?

291

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 ściśle powiązana z resztą mojej aplikacji. Czytałem o repozytoriach i używaniu kontenera IoC do „wstrzykiwania” go do moich kontrolerów. Bardzo fajne rzeczy. Ale teraz masz kilka praktycznych pytań na temat projektu repozytorium. Rozważ następujący 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 nr 1: Zbyt wiele pól

Wszystkie te metody wyszukiwania wykorzystują SELECT *podejście zaznacz wszystkie pola ( ). Jednak w moich aplikacjach zawsze staram się ograniczać liczbę pól, które otrzymuję, ponieważ często powoduje to zwiększenie kosztów i spowalnia działanie. Dla tych, którzy używają tego wzoru, jak sobie z tym poradzisz?

Problem nr 2: Zbyt wiele metod

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

  • findAllByNameAndStatus
  • findAllInCountry
  • findAllWithEmailAddressSet
  • findAllByAgeAndGender
  • findAllByAgeAndGenderOrderByAge
  • Itp.

Jak widać, może istnieć bardzo długa lista możliwych metod. A jeśli dodasz powyższy problem wyboru pola, problem się pogorszy. W przeszłości zwykle po prostu ustawiałem całą tę logikę w moim kontrolerze:

<?php

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

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

W moim podejściu do repozytorium nie chcę tego kończyć:

<?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 nr 3: Nie można dopasować interfejsu

Widzę korzyści z używania interfejsów do repozytoriów, więc mogę zamienić swoją implementację (do celów testowych lub innych). Rozumiem interfejsy, ponieważ definiują one umowę, której wdrożenie musi przestrzegać. Jest to świetne, dopóki nie zaczniesz dodawać dodatkowych metod do swoich repozytoriów, takich jak findAllInCountry(). Teraz muszę zaktualizować interfejs, aby mieć tę metodę, w przeciwnym razie inne implementacje mogą go nie mieć, co może uszkodzić moją aplikację. Przez to czuję się szalony ... przypadek ogona machającego psem.

Wzór specyfikacji?

Mnie to prowadzi do przekonania, że repozytorium powinien mieć tylko określoną liczbę metod (takich jak save(), remove(), find(), findAll(), etc). Ale w jaki sposób mogę uruchomić określone wyszukiwania? Słyszałem o Wzorcu specyfikacji , ale wydaje mi się, że zmniejsza to tylko cały zestaw rekordów (via IsSatisfiedBy()), który wyraźnie ma poważne problemy z wydajnością, jeśli pobierasz z bazy danych.

Wsparcie?

Oczywiście, muszę trochę przemyśleć różne rzeczy podczas pracy z repozytoriami. Czy ktoś może wyjaśnić, jak najlepiej sobie z tym poradzić?

Jonathan
źródło

Odpowiedzi:

208

Myślałem, że zaryzykuję odpowiedź na moje własne pytanie. Poniżej znajduje się tylko jeden sposób rozwiązania problemów 1-3 w moim pierwotnym pytaniu.

Oświadczenie: Nie zawsze używam właściwych terminów przy opisywaniu wzoró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 próbny.
  • Administrator nie powinien mieć pojęcia, gdzie przechowywane są dane (co oznacza, że ​​można je zmienić).
  • Przykład pokazujący implementację SQL (najczęściej).
  • Aby uzyskać maksymalną wydajność, administratorzy powinni otrzymywać tylko te dane, których potrzebują - bez dodatkowych pól.
  • Wdrożenie powinno wykorzystywać pewien rodzaj mapera danych dla ułatwienia rozwoju.
  • Implementacja powinna mieć możliwość wykonywania skomplikowanych wyszukiwań danych.

Rozwiązanie

Dzielę interakcję pamięci trwałej (bazy danych) na dwie kategorie: R (odczyt) i CUD (tworzenie, aktualizacja, usuwanie). Z mojego doświadczenia wynika, że ​​odczyty są tak naprawdę przyczyną spowolnienia działania aplikacji. I chociaż manipulowanie danymi (CUD) jest w rzeczywistości wolniejsze, zdarza się to znacznie rzadziej, a zatem stanowi o wiele mniejszy problem.

CUD (tworzenie, aktualizacja, usuwanie) jest łatwe. Będzie to wymagało pracy z rzeczywistymi modelami , które są następnie przekazywane do mnie Repositoriesza wytrwałość. Uwaga: moje repozytoria nadal będą zapewniać metodę odczytu, ale po prostu do tworzenia obiektów, a nie wyświetlania. Więcej o tym później.

R (Odczyt) nie jest takie łatwe. Nie ma tu modeli, wystarczy wycenić obiekty . Używaj tablic, jeśli wolisz . Obiekty te mogą reprezentować pojedynczy model lub połączenie wielu modeli, cokolwiek naprawdę. Nie są one bardzo interesujące same w sobie, ale sposób ich generowania jest. Używam tego, co nazywam Query Objects.

Kod:

Model użytkownika

Zacznijmy od prostego z naszym podstawowym modelem użytkownika. Zauważ, że w ogóle nie ma rozszerzenia ORM ani bazy danych. Po prostu czysta chwała modelu. Dodaj swoje pobierające, ustawiające, sprawdzanie poprawności, cokolwiek.

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

Interfejs repozytorium

Przed utworzeniem mojego repozytorium użytkowników chcę utworzyć interfejs mojego repozytorium. Spowoduje to zdefiniowanie „umowy”, której muszą przestrzegać repozytoria, aby mogły być używane przez mojego kontrolera. Pamiętaj, że mój administrator nie będzie wiedział, gdzie faktycznie są przechowywane dane.

Pamiętaj, że moje repozytoria zawierają tylko te trzy metody. Ta save()metoda odpowiada zarówno za tworzenie, jak i aktualizowanie użytkowników, po prostu w zależności od tego, czy obiekt użytkownika ma ustawiony identyfikator.

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

Implementacja repozytorium SQL

Teraz, aby utworzyć moją implementację interfejsu. Jak wspomniano, mój przykład miał być z bazą danych SQL. Zwróć uwagę na użycie mapera danych, aby uniknąć konieczności powtarzania zapytań SQL.

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, gdy CUD (Utwórz, Aktualizuj, Usuń) jest obsługiwany przez nasze repozytorium, możemy skupić się na R (Odczyt). Obiekty zapytania są po prostu enkapsulacją pewnego rodzaju logiki wyszukiwania danych. Są nie budowniczowie zapytań. Abstraktując go jak nasze repozytorium, możemy zmienić jego implementację i przetestować go łatwiej. Przykładem obiektu zapytania może być AllUsersQuerylub AllActiveUsersQuery, lub nawet MostCommonUserFirstNames.

Być może myślisz „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 modelu. Dlaczego w aplikacji ze świata rzeczywistego miałbym mieć takie passwordpole, jeśli chcę wyświetlić listę wszystkich moich użytkowników?
  • Repozytoria często zależą od modelu, ale zapytania często dotyczą więcej niż jednego modelu. Więc w jakim repozytorium umieściłeś swoją metodę?
  • Dzięki temu moje repozytoria są bardzo proste - nie rozdęta klasa metod.
  • Wszystkie zapytania są teraz zorganizowane w osobne klasy.
  • Naprawdę, w tym momencie repozytoria istnieją po prostu w celu wyodrębnienia mojej warstwy bazy danych.

Na przykład utworzę obiekt zapytania, aby wyszukać „AllUsers”. Oto interfejs:

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

Implementacja obiektu zapytania

W tym miejscu możemy ponownie użyć mapera danych, aby przyspieszyć rozwój. Zauważ, że zezwalam na jedną modyfikację zwróconego zestawu danych - pól. Jest to o tyle, o ile chcę przejść do manipulowania wykonanym zapytaniem. Pamiętaj, że moje obiekty zapytań nie są konstruktorami zapytań. Po prostu wykonują określone zapytanie. Ponieważ jednak wiem, że prawdopodobnie będę go często używał, 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 ilustrujący jego moc. Może mam silnik raportowania i muszę utworzyć raport dla AllOverdueAccounts. Może to być trudne w przypadku mojego mapera danych i SQLw tej sytuacji mogę chcieć napisać kilka faktów . Nie ma problemu, oto jak mógłby 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 utrzymuje całą moją logikę dla tego raportu w jednej klasie i jest łatwe do przetestowania. Mogę kpić z treści mojego serca, a nawet całkowicie użyć innej implementacji.

Kontroler

Teraz część zabawy - zebranie wszystkich elementów. Zauważ, że używam zastrzyku zależności. Zazwyczaj zależności są wstrzykiwane do konstruktora, ale tak naprawdę wolę wstrzykiwać je bezpośrednio do metod kontrolera (tras). Minimalizuje to graf obiektowy kontrolera i uważam, że jest bardziej czytelny. Uwaga: 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;
    }
}

Końcowe przemyślenia:

Ważną rzeczą do odnotowania tutaj jest to, że kiedy modyfikuję (tworzę, aktualizuję lub usuwam) byty, pracuję z obiektami modelu rzeczywistego i wykonuję utrwalanie poprzez moje repozytoria.

Jednak podczas wyświetlania (wybierania danych i wysyłania ich do widoków) nie pracuję z obiektami modelu, ale raczej zwykłymi obiektami o starej wartości. Wybieram tylko pola, których potrzebuję, a jego konstrukcja pozwala mi maksymalnie zwiększyć wydajność wyszukiwania danych.

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

Używam mapera danych do pomocy w programowaniu, ponieważ pisanie powtarzalnego SQL dla typowych zadań jest po prostu śmieszne. Jednak absolutnie możesz pisać SQL w razie potrzeby (skomplikowane zapytania, raportowanie itp.). A kiedy to zrobisz, jest ładnie schowany w klasie o odpowiedniej nazwie.

Chciałbym usłyszeć twoje podejście do mojego podejścia!


Aktualizacja z lipca 2015 r .:

Zostałem zapytany w komentarzach, w których skończyłem z tym wszystkim. Właściwie nie tak daleko. Szczerze mówiąc, nadal nie lubię repozytoriów. Uważam, że są przesadzone w podstawowych przeglądach (szczególnie jeśli już używasz ORM) i są nieporządne 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 mojej aplikacji. Jednak w sytuacjach, w których mam bardziej złożone zapytania, użyję obiektów zapytania, aby uczynić je bardziej użytecznymi. Powinienem również zauważyć, że zawsze wstrzykuję moje modele do moich metod, dzięki czemu łatwiej mi z nich kpić w moich testach.

Jonathan
źródło
4
@PeeHaa Znowu chodziło o to, by przykłady były proste. Bardzo często pomija się fragmenty kodu, jeśli nie dotyczą konkretnego tematu. W rzeczywistości przekazałbym swoje zależności.
Jonathan
4
Interesujące jest to, że podzieliłeś swoje Utwórz, Aktualizuj i Usuń z Read. Pomyślałem, że warto wspomnieć o segregacji odpowiedzialności za polecenia (CQRS), która formalnie właśnie to robi. martinfowler.com/bliki/CQRS.html
Adam
2
@Jathanathan Minęło półtora roku, odkąd odpowiedziałeś na własne pytanie. Zastanawiałem się, czy nadal jesteś zadowolony ze swojej odpowiedzi i czy jest to obecnie główne rozwiązanie dla większości twoich projektów? W ciągu ostatnich kilku tygodni czytałem przydziały w repozytoriach i widziałem, że przydział ludzi ma własną interpretację tego, w jaki sposób należy go wdrożyć. Nazywasz to obiektami zapytania, ale to istniejący wzorzec, prawda? Myślę, że widziałem, jak jest używany w innych językach.
Boedy
1
@Jathanathan: Jak radzisz sobie z zapytaniami, które nie powinny oznaczać „ID” użytkownika, ale np. „Nazwa użytkownika” lub nawet bardziej skomplikowane zapytania z więcej niż jednym warunkiem?
Gizzmo
1
@Gizzmo Korzystając z obiektów zapytań, możesz przekazać dodatkowe parametry, aby pomóc w bardziej skomplikowanych zapytaniach. Na przykład możesz to zrobić w konstruktorze:new Query\ComplexUserLookup($username, $anotherCondition) . Lub zrób to za pomocą metod ustawiających $query->setUsername($username);. Możesz to naprawdę zaprojektować, jednak ma to sens dla konkretnej aplikacji i myślę, że obiekty zapytań pozostawiają tutaj dużą elastyczność.
Jonathan
48

Na podstawie mojego doświadczenia, oto kilka odpowiedzi na twoje pytania:

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

Odp .: Z mojego doświadczenia wynika, że ​​tak naprawdę sprowadza się to do radzenia sobie z kompletnymi bytami zamiast zapytań ad hoc.

Kompletny byt jest czymś w rodzaju Userobiektu. Ma właściwości i metody itp. Jest obywatelem pierwszej klasy w twojej bazie kodu.

Zapytanie ad-hoc zwraca niektóre dane, ale nie wiemy nic poza tym. Gdy dane są przekazywane do aplikacji, odbywa się to bez kontekstu. Czy to jest User? A Userz Orderzałączonymi informacjami? Naprawdę nie wiemy.

Wolę pracować z pełnymi bytami.

Masz rację, że często przywracasz dane, których nie używasz, ale możesz rozwiązać ten problem na różne sposoby:

  1. Agresywnie buforuj jednostki, aby płacić cenę odczytu tylko raz z bazy danych.
  2. Poświęć więcej czasu na modelowanie swoich bytów, aby miały między nimi dobre rozróżnienie. (Rozważ podzielenie dużej jednostki na dwie mniejsze jednostki itp.)
  3. Rozważ posiadanie wielu wersji encji. Możesz mieć Userdla zaplecza i może UserSmalldla połączeń AJAX. Jedna może mieć 10 właściwości, a druga 3 właściwości.

Wady pracy z zapytaniami ad hoc:

  1. Otrzymujesz w zasadzie te same dane w wielu zapytaniach. Na przykład za pomocąUser skończysz pisać zasadniczo tak samo select *dla wielu połączeń. Jedno połączenie otrzyma 8 z 10 pól, jedno dostanie 5 z 10, jedno dostanie 7 z 10. Dlaczego nie zastąpić wszystkich jednym połączeniem, które otrzyma 10 z 10? Powodem, dla którego jest to złe, jest fakt, że morderstwem jest ponowne uwzględnienie / przetestowanie / wykpienie.
  2. Z biegiem czasu bardzo trudno jest na wysokim poziomie uzasadnić swój kod. Zamiast stwierdzeń typu „Dlaczego jestUser tak wolno?” kończy się to na wyszukiwaniu jednorazowych zapytań, więc poprawki błędów wydają się być małe i zlokalizowane.
  3. Naprawdę trudno jest wymienić podstawową technologię. Jeśli teraz przechowujesz wszystko w MySQL i chcesz przejść do MongoDB, o wiele trudniej jest zastąpić 100 połączeń ad-hoc niż garść podmiotów.

P: W moim repozytorium będę mieć zbyt wiele metod.

ZA: Tak naprawdę nie widziałem innego sposobu niż konsolidacja połączeń. Wywołania metod w repozytorium naprawdę są mapowane na funkcje w aplikacji. Im więcej funkcji, tym bardziej specyficzne połączenia danych. Możesz włączyć funkcje i spróbować połączyć podobne połączenia w jedno.

Złożoność na koniec dnia musi gdzieś istnieć. Za pomocą wzorca repozytorium umieściliśmy go w interfejsie repozytorium zamiast być może tworząc wiele procedur przechowywanych.

Czasami muszę sobie powtarzać: „Cóż, musiało to gdzieś dawać! Nie ma srebrnych kul”.

ryan1234
źródło
Dzięki za bardzo dokładną odpowiedź. Teraz myślisz. Moją wielką obawą jest to, że wszystko, co czytam, mówi, że nie SELECT *, raczej wybieram tylko pola, których potrzebujesz. Na przykład zobacz to pytanie . Co do wszystkich zapytań ad-hock, o których mówisz, z pewnością rozumiem, skąd pochodzisz. Mam teraz bardzo dużą aplikację, która ma wiele z nich. To było moje „Cóż, musiało gdzieś dać!” moment, zdecydowałem się na maksymalną wydajność. Jednak teraz mam do czynienia z WIELKIMI różnymi zapytaniami.
Jonathan
1
Jedna myśl następcza. Widziałem zalecenie stosowania metody R-CUD. Ponieważ readsczęsto zdarzają się problemy z wydajnością, można zastosować bardziej niestandardowe podejście do zapytań, które nie przekładają się na rzeczywiste obiekty biznesowe. Następnie, dla create, updatei deleteużywać ORM, który pracuje z całych obiektów. Wszelkie przemyślenia na temat tego podejścia?
Jonathan
1
Uwaga dotycząca używania „wybierz *”. Zrobiłem to w przeszłości i działało dobrze - dopóki nie trafiliśmy na pola varchar (max). Ci zabili nasze zapytania. Więc jeśli masz tabele z liczbami całkowitymi, małymi polami tekstowymi itp., Nie jest tak źle. Wydaje się nienaturalne, ale oprogramowanie idzie w tym kierunku. To, co było złe, nagle stało się dobre i na odwrót.
ryan1234
1
Podejście R-CUD to tak naprawdę CQRS
MikeSW
2
@ ryan1234 „Złożoność pod koniec dnia musi gdzieś istnieć.” Dziękuję Ci za to. Sprawia, że ​​czuję się lepiej.
Johnny
20

Korzystam z następujących interfejsów:

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

Moja Repositorybaza danych jest agnostyczna; w rzeczywistości nie określa żadnej trwałości; może to być wszystko: baza danych SQL, plik xml, usługa zdalna, kosmita z kosmosu itp. Dla możliwości wyszukiwania Repositorykonstrukcje, Selectorktóre można filtrować LIMIT, sortować, sortować i zliczać. Na koniec selektor pobiera jeden lub więcej Entitiesz trwałości.

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

Ideą jest to, że Selectorzastosowania ogólne, Filterale SqlSelectorzastosowania implementacyjne SqlFilter; SqlSelectorFilterAdapterdostosowuje ogólny Filterdo betonu SqlFilter.

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

Inne implementacje selektorów, na przykład InMemorySelector, przekształcają się z Filterna InMemoryFilterspecyficzne InMemorySelectorFilterAdapter; więc każda implementacja selektora ma własny adapter filtra.

Korzystając z tej strategii, mój kod klienta (w warstwie biznesowej) nie dba o konkretne repozytorium lub 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();

PS To jest uproszczenie mojego prawdziwego kodu

Constantin Galbenu
źródło
„Repozytorium - ładuje, wstawia, aktualizuje i usuwa byty” to jest to, co może zrobić „warstwa usługi”, „DAO”, „BLL”
Yousha Aleayoub
5

Dodam trochę o tym, ponieważ obecnie próbuję to wszystko zrozumieć.

# 1 i 2

Jest to idealne miejsce dla Twojego ORM do podnoszenia ciężkich przedmiotów. Jeśli używasz modelu, który implementuje pewien rodzaj ORM, możesz po prostu użyć jego metod, aby zająć się tymi rzeczami. Twórz własne zamówienia, korzystając z funkcji, które w razie potrzeby implementują metody Elokwentne. Na przykład używając Eloquent:

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. Bez powodu Twoje Repozytorium nie może być oparte na jednym. Wymagałoby to przedłużenia wymowy przez użytkownika, ale osobiście nie uważam tego za problem.

Jeśli jednak chcesz uniknąć ORM, musisz „rzucić własną”, aby uzyskać to, czego szukasz.

# 3

Interfejsy nie powinny być trudnymi i szybkimi wymaganiami. Coś może zaimplementować interfejs i dodać do niego. Nie może jednak nie wdrożyć wymaganej funkcji tego interfejsu. Możesz także rozszerzyć interfejsy, takie jak klasy, aby zachować SUSZENIE.

To powiedziawszy, dopiero zaczynam rozumieć, ale te realizacje pomogły mi.

Będzie
źródło
1
W tej metodzie nie podoba mi się to, że gdybyś miał MongoUserRepository, to i twoje DbUserRepository zwróciłyby różne obiekty. Db zwraca Eloquent \ Model, a Mongo coś własnego. Z pewnością lepszą implementacją jest, aby oba repozytoria zwracały instancje / kolekcje osobnej klasy Entity \ User. W ten sposób nie będziesz błędnie polegać na metodach DB Eloquent \ Model po przejściu na używanie MongoRepository
danharper
1
Zdecydowanie się z tobą zgodzę w tej sprawie. To, co prawdopodobnie zrobiłbym, aby tego uniknąć, to nigdy nie używać tych metod poza wymowną klasą Eloquent. Zatem funkcja get prawdopodobnie powinna być prywatna i używana tylko w klasie, ponieważ, jak wskazałeś, zwróci coś, czego inne repozytoria nie mogłyby.
Czy
3

Mogę jedynie skomentować sposób, w jaki my (w mojej firmie) sobie z tym radzimy. Przede wszystkim wydajność nie stanowi dla nas większego problemu, ale posiadanie czystego / właściwego kodu jest.

Przede wszystkim definiujemy modele, takie jak a, UserModelktóre używają ORM do tworzenia UserEntityobiektów. Kiedy a UserEntityjest ładowane z modelu, wszystkie pola są ładowane. W przypadku pól odwołujących się do podmiotów zagranicznych stosujemy odpowiedni model obcy, aby utworzyć odpowiednie podmioty. W przypadku tych podmiotów dane będą ładowane na żądanie. Teraz twoją początkową reakcją może być ... ??? ... !!! dam wam przykład trochę przykładu:

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 $dbjest to ORM, który jest w stanie załadować jednostki. Model instruuje ORM, aby załadował zestaw encji określonego typu. ORM zawiera mapowanie i wykorzystuje je do wstrzyknięcia wszystkich pól dla tej encji do encji. Jednak w przypadku pól obcych ładowane są tylko identyfikatory tych obiektów. W tym przypadku OrderModeltworzy OrderEntitys tylko z identyfikatorami przywoływanych zamówień. Gdy jednostka PersistentEntity::getFieldzostanie wywołana OrderEntity, instruuje swój model, aby leniwie załadował wszystkie pola do OrderEntitys. Wszystkie OrderEntitys powiązane z jednym UserEntity są traktowane jako jeden zestaw wyników i zostaną załadowane jednocześnie.

Magia polega na tym, że nasz model i ORM wstrzykują wszystkie dane do encji, a encje tylko zapewniają funkcje otoki dla ogólnej getFieldmetody dostarczanej przez PersistentEntity. Podsumowując, zawsze ładujemy wszystkie pola, ale pola odnoszące się do obcego obiektu są ładowane, gdy jest to konieczne. Samo ładowanie kilku pól nie jest tak naprawdę problemem z wydajnością. Załaduj wszystkie możliwe podmioty zagraniczne byłoby jednak OGROMNYM spadkiem wydajności.

Teraz przejdźmy do ładowania określonego zestawu użytkowników na podstawie klauzuli where. Zapewniamy obiektowy pakiet klas, który pozwala określić proste wyrażenie, które można skleić. W przykładowym kodzie nazwałem go GetOptions. Jest to opakowanie dla wszystkich możliwych opcji dla wybranego zapytania. Zawiera kolekcję klauzul where, grupę po klauzuli i wszystko inne. Nasze klauzule gdzie są dość skomplikowane, ale oczywiście można łatwo stworzyć prostszą wersję.

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

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

Przepraszam za tę dość skomplikowaną odpowiedź. Starałem się podsumować nasze ramy tak szybko i jasno, jak to możliwe. Jeśli masz dodatkowe pytania, zadaj je, a ja zaktualizuję swoją odpowiedź.

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

TFennis
źródło
3

Oto kilka różnych rozwiązań, które widziałem. Każdy z nich ma swoje wady i zalety, ale to Ty decydujesz.

Problem nr 1: Zbyt wiele pól

Jest to ważny aspekt, zwłaszcza gdy bierzesz pod uwagę skany tylko do indeksu . Widzę dwa rozwiązania radzenia sobie z tym problemem. Możesz zaktualizować swoje funkcje, aby zawierały opcjonalny parametr tablicy, który zawierałby listę kolumn do zwrócenia. Jeśli ten parametr jest pusty, zwracane są wszystkie kolumny w zapytaniu. To może być trochę dziwne; na podstawie parametru można pobrać obiekt lub tablicę. Możesz także zduplikować wszystkie swoje funkcje, aby mieć dwie różne funkcje, które wykonują 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 nr 2: Zbyt wiele metod

Rok temu krótko współpracowałem z Propel ORM i jest to oparte na tym, co pamiętam z tego doświadczenia. Propel ma opcję generowania struktury klas na podstawie istniejącego schematu bazy danych. Tworzy dwa obiekty dla każdej tabeli. Pierwszy obiekt to długa lista funkcji dostępu podobna do tej, którą obecnie wymieniasz;findByAttribute($attribute_value). Następny obiekt dziedziczy po tym pierwszym obiekcie. Możesz zaktualizować ten obiekt podrzędny, aby wbudować bardziej złożone funkcje pobierające.

Innym rozwiązaniem byłoby __call()odwzorowanie niezdefiniowanych funkcji na coś, co można wykonać. Twoja __callmetoda byłaby w stanie przeanalizować 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 przynajmniej pomoże.

Logan Bailey
źródło
0

Zgadzam się z @ ryan1234, że powinieneś przekazywać pełne 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
        }
    }
}
AVProgrammer
źródło
0

Problem nr 3: Nie można dopasować interfejsu

Widzę korzyści z używania interfejsów do repozytoriów, więc mogę zamienić swoją implementację (do celów testowych lub innych). Rozumiem interfejsy, ponieważ definiują one umowę, której wdrożenie musi przestrzegać. Jest to świetne, dopóki nie zaczniesz dodawać do swoich repozytoriów dodatkowych metod, takich jak findAllInCountry (). Teraz muszę zaktualizować interfejs, aby mieć tę metodę, w przeciwnym razie inne implementacje mogą go nie mieć, co może uszkodzić moją aplikację. Przez to czuję się szalony ... przypadek ogona machającego psem.

Moja intuicja mówi mi, że może to wymagać interfejsu, który implementuje metody zoptymalizowane pod kątem zapytań obok metod ogólnych. Kwerendy wrażliwe na wydajność powinny mieć ukierunkowane metody, podczas gdy rzadkie lub lekkie kwerendy są obsługiwane przez ogólny moduł obsługi, być może kosztem kontrolera jest trochę żonglowanie.

Metody ogólne pozwoliłyby na zaimplementowanie dowolnego zapytania, a tym samym zapobiegłyby łamaniu zmian w okresie przejściowym. Wybrane metody pozwalają zoptymalizować połączenie, gdy ma to sens, i można je zastosować 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 implementację elastyczną.

Brian
źródło
0

Myślę, że GraphQL jest dobrym kandydatem w takim przypadku, aby zapewnić język zapytań na dużą skalę bez zwiększania złożoności repozytoriów danych.

Istnieje jednak inne rozwiązanie, jeśli na razie nie chcesz korzystać z grafQL. Korzystając z DTO którym obiekt jest używany do przenoszenia danych między procesami, w tym przypadku między usługą / kontrolerem a repozytorium.

Elegancka odpowiedźPowyżej podano , ale postaram się podać 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, potrzebowalibyśmy tylko 4 metod do operacji CRUD. findsposób zostaną wykorzystane na aukcji i czytania przekazując obiekt argument. Usługi zaplecza mogą budować zdefiniowany obiekt zapytania na podstawie ciągu zapytania adresu URL lub na podstawie określonych parametrów.

W SomeQueryDtorazie potrzeby obiekt zapytania ( ) może również implementować określony interfejs. i można go łatwo rozszerzyć później bez zwiększania 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);
kordy
źródło