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 Repositories
za 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ć AllUsersQuery
lub 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
password
pole, 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 SQL
w 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.
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ść.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
User
obiektu. 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
? AUser
zOrder
załą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:
User
dla zaplecza i możeUserSmall
dla 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:
User
skończysz pisać zasadniczo tak samoselect *
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.User
tak wolno?” kończy się to na wyszukiwaniu jednorazowych zapytań, więc poprawki błędów wydają się być małe i zlokalizowane.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”.
źródło
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.reads
czę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, dlacreate
,update
idelete
używać ORM, który pracuje z całych obiektów. Wszelkie przemyślenia na temat tego podejścia?Korzystam z następujących interfejsów:
Repository
- ładuje, wstawia, aktualizuje i usuwa jednostkiSelector
- wyszukuje jednostki w repozytorium na podstawie filtrówFilter
- hermetyzuje logikę filtrowaniaMoja
Repository
baza 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 wyszukiwaniaRepository
konstrukcje,Selector
które można filtrowaćLIMIT
, sortować, sortować i zliczać. Na koniec selektor pobiera jeden lub więcejEntities
z trwałości.Oto przykładowy kod:
Następnie jedna implementacja:
Ideą jest to, że
Selector
zastosowania ogólne,Filter
aleSqlSelector
zastosowania implementacyjneSqlFilter
;SqlSelectorFilterAdapter
dostosowuje ogólnyFilter
do betonuSqlFilter
.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ę zFilter
naInMemoryFilter
specyficzneInMemorySelectorFilterAdapter
; 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.
PS To jest uproszczenie mojego prawdziwego kodu
źródło
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:
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.
źródło
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,
UserModel
które używają ORM do tworzeniaUserEntity
obiektów. Kiedy aUserEntity
jest ł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:W naszym przypadku
$db
jest 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 przypadkuOrderModel
tworzyOrderEntity
s tylko z identyfikatorami przywoływanych zamówień. Gdy jednostkaPersistentEntity::getField
zostanie wywołanaOrderEntity
, instruuje swój model, aby leniwie załadował wszystkie pola doOrderEntity
s. WszystkieOrderEntity
s 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
getField
metody dostarczanej przezPersistentEntity
. 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ę.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ą
getField
metody, 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.źródło
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.
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__call
metoda byłaby w stanie przeanalizować findById i findByName w różnych zapytaniach.Mam nadzieję, że to przynajmniej pomoże.
źródło
Sugeruję https://packagist.org/packages/prettus/l5-repository jako dostawcę do wdrażania repozytoriów / kryteriów itp. W Laravel5: D
źródło
Zgadzam się z @ ryan1234, że powinieneś przekazywać pełne obiekty w kodzie i używać ogólnych metod zapytań, aby uzyskać te obiekty.
Do użytku zewnętrznego / końcowego bardzo podoba mi się metoda GraphQL.
źródło
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ą.
źródło
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.
find
sposó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
SomeQueryDto
razie 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.Przykładowe użycie:
źródło