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.