Zarządzanie relacjami w Laravel, przestrzeganie wzorca repozytorium

120

Tworząc aplikację w Laravel 4, po przeczytaniu książki T. Otwella o dobrych wzorcach projektowych w Laravel, znalazłem repozytoria dla każdej tabeli w aplikacji.

Skończyło się na następującej strukturze tabeli:

  • Uczniowie: identyfikator, imię i nazwisko
  • Kursy: identyfikator, imię i nazwisko, identyfikator nauczyciela
  • Nauczyciele: id, imię
  • Zadania: identyfikator, imię i nazwisko, identyfikator kursu
  • Wyniki (działa jako punkt zwrotny między studentami i zadaniami): student_id, assignment_id, score

Mam klasy repozytorium z metodami wyszukiwania, tworzenia, aktualizowania i usuwania dla wszystkich tych tabel. Każde repozytorium ma model elokwentny, który współdziała z bazą danych. Relacje są zdefiniowane w modelu zgodnie z dokumentacją Laravel: http://laravel.com/docs/eloquent#relationships .

Podczas tworzenia nowego kursu wszystko, co robię, to wywoływanie metody create w repozytorium kursów. Ten kurs ma przypisane zadania, więc podczas tworzenia takiego kursu chcę również utworzyć wpis w tabeli wyników dla każdego ucznia na kursie. Robię to za pośrednictwem repozytorium przydziałów. Oznacza to, że repozytorium przydziałów komunikuje się z dwoma elokwentnymi modelami, z modelem przydziału i ucznia.

Moje pytanie brzmi: ponieważ ta aplikacja prawdopodobnie powiększy się i zostanie wprowadzonych więcej relacji, czy dobrą praktyką jest komunikowanie się z różnymi modelami Eloquent w repozytoriach, czy też powinno to być zrobione przy użyciu innych repozytoriów (mam na myśli wywoływanie innych repozytoriów z repozytorium przydziałów ) czy też należy to zrobić w modelach Eloquent razem?

Ponadto, czy dobrą praktyką jest używanie tabeli wyników jako punktu odniesienia między zadaniami a uczniami, czy też należy to zrobić gdzie indziej?

ehp
źródło

Odpowiedzi:

71

Pamiętaj, że pytasz o opinie: D

To moje:

TL; DR: Tak, w porządku.

Dobrze Ci idzie!

Robię dokładnie to, co robisz często i uważam, że działa świetnie.

Często jednak organizuję repozytoria zgodnie z logiką biznesową, zamiast mieć repozytorium dla każdej tabeli. Jest to przydatne, ponieważ jest to punkt widzenia skupiony na tym, jak aplikacja powinna rozwiązać Twój „problem biznesowy”.

Kurs jest „jednostką” z atrybutami (tytuł, identyfikator itp.), A nawet innymi jednostkami (Zadaniami, które mają własne atrybuty i być może jednostki).

Twoje repozytorium „Kursów” powinno być w stanie zwrócić Kurs i atrybuty / Zadania kursów (w tym Zadania).

Na szczęście możesz to osiągnąć dzięki Eloquent.

(Często otrzymuję repozytorium na tabelę, ale niektóre repozytoria są używane znacznie częściej niż inne, więc mają o wiele więcej metod. Twoje repozytorium „kursów” może być o wiele bardziej funkcjonalne niż repozytorium Projektów, na przykład, jeśli centra aplikacji bardziej wokół kursów, a mniej na temat zbioru Zadań kursów).

Trudna część

Często korzystam z repozytoriów w moich repozytoriach, aby wykonać pewne działania na bazie danych.

Każde repozytorium, które implementuje Eloquent w celu obsługi danych, prawdopodobnie zwróci modele Eloquent. W tym świetle jest w porządku, jeśli model kursu używa wbudowanych relacji w celu pobierania lub zapisywania Projektów (lub dowolnego innego przypadku użycia). Nasza „implementacja” opiera się na elokwentnym.

Z praktycznego punktu widzenia ma to sens. Jest mało prawdopodobne, że zmienimy źródła danych na coś, czego Eloquent nie może obsłużyć (na źródło danych inne niż sql).

ORMS

Najtrudniejszą częścią tej konfiguracji, przynajmniej dla mnie, jest ustalenie, czy Eloquent faktycznie nam pomaga, czy szkodzi. ORMy to trudny temat, ponieważ chociaż bardzo nam pomagają z praktycznego punktu widzenia, łączą również kod „jednostek logiki biznesowej” z kodem wykonującym wyszukiwanie danych.

Ten rodzaj niejasności wprowadza w błąd, czy odpowiedzialność repozytorium faktycznie polega na przetwarzaniu danych, czy na obsłudze pobierania / aktualizacji podmiotów (podmiotów domeny biznesowej).

Ponadto zachowują się jak przedmioty, które przekazujesz swoim poglądom. Jeśli później będziesz musiał odejść od używania modeli elokwentnych w repozytorium, musisz upewnić się, że zmienne przekazywane do twoich widoków zachowują się w ten sam sposób lub mają dostępne te same metody, w przeciwnym razie zmiana źródeł danych spowoduje zmianę widoki, a Ty (częściowo) straciłeś cel wyabstrahowania swojej logiki do repozytoriów w pierwszej kolejności - łatwość utrzymania projektu spada jako.

W każdym razie są to nieco niepełne myśli. Są one, jak już wspomniałem, jedynie moją opinią, która jest wynikiem czytania Domain Driven Design i oglądania filmów, takich jak myśl przewodnia „wujka boba” na Ruby Midwest w ciągu ostatniego roku.

fideloper
źródło
1
Czy Twoim zdaniem byłoby dobrą alternatywą, gdyby repozytoria zwracały obiekty przesyłania danych zamiast obiektów elokwentnych? Oczywiście oznaczałoby to dodatkową konwersję z elokwentnego do dto, ale w ten sposób przynajmniej izolujesz swoje kontrolery / widoki od bieżącej implementacji ORM.
federivo
1
Sam trochę z tym eksperymentowałem i stwierdziłem, że jest to trochę niepraktyczne. Biorąc to pod uwagę, podoba mi się ten pomysł w skrócie. Jednak obiekty kolekcji bazy danych Illuminate działają tak samo jak tablice, a obiekty Model działają tak samo jak obiekty StdClass na tyle, że możemy, praktycznie rzecz biorąc, trzymać się Eloquent i nadal używać tablic / obiektów w przyszłości, jeśli zajdzie taka potrzeba.
fideloper
4
@fideloper Czuję, że jeśli korzystam z repozytoriów, tracę całe piękno ORM, które zapewnia Eloquent. Podczas pobierania obiektu konta za pomocą mojej metody repozytorium $a = $this->account->getById(1)nie mogę po prostu łączyć metod, takich jak $a->getActiveUsers(). OK, mogę użyć $a->users->..., ale zwracam kolekcję Eloquent bez obiektu stdClass i ponownie jestem powiązany z Eloquent. Jakie jest na to rozwiązanie? Deklarowanie innej metody w repozytorium użytkowników, na przykład $user->getActiveUsersByAccount($a->id);? Chciałbym usłyszeć, jak rozwiązujesz ten ...
santacruz
1
ORMy są okropne dla architektury na poziomie Enterprise (ish), ponieważ powodują takie problemy. Ostatecznie musisz zdecydować, co jest najbardziej sensowne dla Twojej aplikacji. Osobiście podczas korzystania z repozytoriów z Eloquent (90% czasu!) Używam Eloquent i staram się traktować modele i kolekcje jak stdClasses i Arrays (ponieważ możesz!), Więc jeśli zajdzie taka potrzeba, przejście na coś innego jest możliwe.
fideloper
5
Śmiało i używaj leniwie ładowanych modeli. Możesz sprawić, że prawdziwe modele domen będą działać w ten sposób, jeśli kiedykolwiek pominiesz korzystanie z Eloquent. Ale poważnie, czy zamierzasz kiedykolwiek zmienić Eloquent? Za pensa, za funta! (Nie przesadzaj, próbując trzymać się „zasad”! Cały czas łamię swoje).
fideloper
224

Kończę duży projekt przy użyciu Laravel 4 i musiałem odpowiedzieć na wszystkie pytania, które teraz zadajesz. Po przeczytaniu wszystkich dostępnych książek Laravel w Leanpub i mnóstwie google, wymyśliłem następującą strukturę.

  1. Jedna klasa Eloquent Model na tabelę datowalną
  2. Jedna klasa repozytorium na model elokwentny
  3. Klasa usług, która może komunikować się między wieloma klasami repozytorium.

Powiedzmy, że tworzę bazę danych filmów. Miałbym przynajmniej następujące klasy Eloquent Model:

  • Film
  • Studio
  • Dyrektor
  • Aktor
  • Przejrzeć

Klasa repozytorium hermetyzowałaby każdą klasę modelu elokwentnego i byłaby odpowiedzialna za operacje CRUD w bazie danych. Klasy repozytorium mogą wyglądać następująco:

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActorRepository
  • ReviewRepository

Każda klasa repozytorium rozszerzyłaby klasę BaseRepository, która implementuje następujący interfejs:

interface BaseRepositoryInterface
{
    public function errors();

    public function all(array $related = null);

    public function get($id, array $related = null);

    public function getWhere($column, $value, array $related = null);

    public function getRecent($limit, array $related = null);

    public function create(array $data);

    public function update(array $data);

    public function delete($id);

    public function deleteWhere($column, $value);
}

Klasa Service służy do sklejania ze sobą wielu repozytoriów i zawiera prawdziwą „logikę biznesową” aplikacji. Kontrolery komunikują się tylko z klasami usług w przypadku akcji tworzenia, aktualizowania i usuwania.

Więc kiedy chcę utworzyć nowy rekord filmu w bazie danych, moja klasa MovieController może mieć następujące metody:

public function __construct(MovieRepositoryInterface $movieRepository, MovieServiceInterface $movieService)
{
    $this->movieRepository = $movieRepository;
    $this->movieService = $movieService;
}

public function postCreate()
{
    if( ! $this->movieService->create(Input::all()))
    {
        return Redirect::back()->withErrors($this->movieService->errors())->withInput();
    }

    // New movie was saved successfully. Do whatever you need to do here.
}

Do Ciebie należy określenie, w jaki sposób POST przekazujesz dane swoim kontrolerom, ale powiedzmy, że dane zwrócone przez Input :: all () w metodzie postCreate () wyglądają mniej więcej tak:

$data = array(
    'movie' => array(
        'title'    => 'Iron Eagle',
        'year'     => '1986',
        'synopsis' => 'When Doug\'s father, an Air Force Pilot, is shot down by MiGs belonging to a radical Middle Eastern state, no one seems able to get him out. Doug finds Chappy, an Air Force Colonel who is intrigued by the idea of sending in two fighters piloted by himself and Doug to rescue Doug\'s father after bombing the MiG base.'
    ),
    'actors' => array(
        0 => 'Louis Gossett Jr.',
        1 => 'Jason Gedrick',
        2 => 'Larry B. Scott'
    ),
    'director' => 'Sidney J. Furie',
    'studio' => 'TriStar Pictures'
)

Ponieważ MovieRepository nie powinno wiedzieć, jak tworzyć rekordy aktora, reżysera lub studia w bazie danych, użyjemy naszej klasy MovieService, która może wyglądać mniej więcej tak:

public function __construct(MovieRepositoryInterface $movieRepository, ActorRepositoryInterface $actorRepository, DirectorRepositoryInterface $directorRepository, StudioRepositoryInterface $studioRepository)
{
    $this->movieRepository = $movieRepository;
    $this->actorRepository = $actorRepository;
    $this->directorRepository = $directorRepository;
    $this->studioRepository = $studioRepository;
}

public function create(array $input)
{
    $movieData    = $input['movie'];
    $actorsData   = $input['actors'];
    $directorData = $input['director'];
    $studioData   = $input['studio'];

    // In a more complete example you would probably want to implement database transactions and perform input validation using the Laravel Validator class here.

    // Create the new movie record
    $movie = $this->movieRepository->create($movieData);

    // Create the new actor records and associate them with the movie record
    foreach($actors as $actor)
    {
        $actorModel = $this->actorRepository->create($actor);
        $movie->actors()->save($actorModel);
    }

    // Create the director record and associate it with the movie record
    $director = $this->directorRepository->create($directorData);
    $director->movies()->associate($movie);

    // Create the studio record and associate it with the movie record
    $studio = $this->studioRepository->create($studioData);
    $studio->movies()->associate($movie);

    // Assume everything worked. In the real world you'll need to implement checks.
    return true;
}

Pozostaje więc przyjemne, rozsądne oddzielenie obaw. Repozytoria znają tylko model elokwentny, który wstawiają i pobierają z bazy danych. Administratorzy nie dbają o repozytoria, po prostu przekazują zebrane od użytkownika dane i przekazują je do odpowiedniej usługi. Serwis nie dba o to, w jaki sposób otrzymane dane są zapisywane w bazie danych, po prostu przekazuje odpowiednie dane, które otrzymała od administratora do odpowiednich repozytoriów.

Kyle Noland
źródło
8
Ten komentarz jest zdecydowanie czystszym, bardziej skalowalnym i łatwiejszym do utrzymania podejściem.
Andreas
4
+1! To mi bardzo pomoże, dziękuję za podzielenie się z nami! Zastanawiasz się, jak udało ci się zweryfikować rzeczy w usługach, jeśli to możliwe, czy mógłbyś pokrótce wyjaśnić, co zrobiłeś? Mimo wszystko dziekuję! :)
Paulo Freitas
6
Jak powiedział @PauloFreitas, byłoby interesujące zobaczyć, jak radzisz sobie z częścią walidacyjną i byłbym również zainteresowany częścią dotyczącą wyjątków (czy używasz wyjątków, zdarzeń, czy po prostu radzisz sobie z tym, jak wydaje się sugerować w swoim kontroler poprzez zwrot logiczny w twoich usługach?). Dzięki!
Nicolas
11
Dobry opis, chociaż nie jestem pewien, dlaczego wstrzykujesz movieRepository do MovieController, ponieważ kontroler nie powinien nic robić bezpośrednio z repozytorium, ani twoja metoda postCreate przy użyciu movieRepository, więc zakładam, że zostawiłeś to przez pomyłkę ?
davidnknight,
15
Pytanie na ten temat: dlaczego w tym przykładzie używasz repozytoriów? To jest uczciwe pytanie - dla mnie wygląda na to, że używasz repozytoriów, ale przynajmniej w tym przykładzie repozytorium tak naprawdę nie robi nic, ale zapewnia ten sam interfejs co Eloquent, a ostatecznie nadal jesteś przywiązany do Eloquent, ponieważ Twoja klasa usług używa elokwencji bezpośrednio w it ( $studio->movies()->associate($movie);).
Kevin Mitchell
5

Lubię myśleć o tym w kategoriach tego, co robi mój kod i za co jest odpowiedzialny, a nie „dobrze czy źle”. Oto jak rozdzielam swoje obowiązki:

  • Kontrolery to warstwa HTTP, która kieruje żądania do bazowego interfejsu API (czyli kontroluje przepływ)
  • Modele reprezentują schemat bazy danych i informują aplikację, jak wyglądają dane, jakie mogą mieć relacje, a także wszelkie atrybuty globalne, które mogą być konieczne (takie jak nazwa metoda zwracania połączonego imienia i nazwiska)
  • Repozytoria reprezentują bardziej złożone zapytania i interakcje z modelami (nie robię żadnych zapytań dotyczących metod modelowych).
  • Wyszukiwarki - klasy, które pomagają mi tworzyć złożone zapytania wyszukiwania.

Mając to na uwadze, sensowne jest używanie repozytorium za każdym razem (niezależnie od tego, czy tworzysz interfejsy. Itd. To zupełnie inny temat). Podoba mi się to podejście, ponieważ oznacza to, że dokładnie wiem, gdzie iść, kiedy muszę wykonać określoną pracę.

Mam również tendencję do budowania podstawowego repozytorium, zwykle abstrakcyjnej klasy, która definiuje główne ustawienia domyślne - w zasadzie operacje CRUD, a następnie każde dziecko może po prostu rozszerzyć i dodać metody w razie potrzeby lub przeciążać wartości domyślne. Wstrzyknięcie modelu również pomaga temu wzorowi być dość solidnym.

Dziwny człowiek
źródło
Czy możesz pokazać implementację swojego BaseRepository? Właściwie to też to robię i jestem ciekawy, co zrobiłeś.
Odyssee
Pomyśl o getById, getByName, getByTitle, zapisz typ methods.etc. - ogólnie metody, które mają zastosowanie do wszystkich repozytoriów w różnych domenach.
Oddman
5

Pomyśl o repozytoriach jako o spójnej szafce na dane (nie tylko o ORMach). Chodzi o to, że chcesz pobierać dane w spójnym, prostym w użyciu interfejsie API.

Jeśli okaże się, że po prostu robisz Model :: all (), Model :: find (), Model :: create (), prawdopodobnie nie odniesiesz większych korzyści z wyodrębnienia repozytorium. Z drugiej strony, jeśli chcesz wprowadzić nieco więcej logiki biznesowej do swoich zapytań lub działań, możesz utworzyć repozytorium, aby ułatwić korzystanie z interfejsu API do obsługi danych.

Myślę, że pytałeś, czy repozytorium byłoby najlepszym sposobem radzenia sobie z niektórymi bardziej szczegółowymi składniami wymaganymi do połączenia powiązanych modeli. W zależności od sytuacji mogę zrobić kilka rzeczy:

  1. Wieszając nowy model podrzędny z modelu nadrzędnego (jeden-jeden lub jeden-wiele), dodałbym metodę do repozytorium podrzędnego coś podobnego, createWithParent($attributes, $parentModelInstance)a to po prostu dodałoby $parentModelInstance->iddo parent_idpola atrybutów i wywołał create.

  2. Dołączając relację wiele-wiele, faktycznie tworzę funkcje w modelach, aby móc uruchomić $ instance-> attachChild ($ childInstance). Zwróć uwagę, że wymaga to istniejących elementów po obu stronach.

  3. Tworząc powiązane modele w jednym przebiegu, tworzę coś, co nazywam Gateway (może to być trochę odbiegające od definicji Fowlera). Sposób, w jaki mogę wywołać $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) zamiast zbioru logiki, która może się zmienić lub która skomplikowałaby logikę, którą mam w kontrolerze lub poleceniu.

Ryan Tablada
źródło