Verwalten von Beziehungen in Laravel unter Einhaltung des Repository-Musters


120

Während ich eine App in Laravel 4 erstellte, nachdem ich T. Otwells Buch über gute Designmuster in Laravel gelesen hatte, stellte ich fest, dass ich für jede Tabelle in der Anwendung Repositorys erstellte.

Am Ende hatte ich folgende Tabellenstruktur:

  • Schüler: ID, Name
  • Kurse: ID, Name, Lehrer_ID
  • Lehrer: ID, Name
  • Aufgaben: ID, Name, Kurs-ID
  • Scores (fungiert als Dreh- und Angelpunkt zwischen Schülern und Aufgaben): student_id, assign_id, Scores

Ich habe Repository-Klassen mit Methoden zum Suchen, Erstellen, Aktualisieren und Löschen für alle diese Tabellen. Jedes Repository verfügt über ein eloquentes Modell, das mit der Datenbank interagiert. Beziehungen werden im Modell gemäß der Dokumentation von Laravel definiert: http://laravel.com/docs/eloquent#relationships .

Wenn ich einen neuen Kurs erstelle, rufe ich nur die create-Methode im Course Repository auf. Dieser Kurs hat Aufgaben, daher möchte ich beim Erstellen einer Aufgabe auch für jeden Kursteilnehmer einen Eintrag in der Punktetabelle erstellen. Ich mache das über das Assignment Repository. Dies impliziert, dass das Zuweisungs-Repository mit zwei eloquenten Modellen kommuniziert, mit dem Zuweisungs- und dem Studentenmodell.

Meine Frage ist: Da diese App wahrscheinlich größer wird und mehr Beziehungen eingeführt werden, empfiehlt es sich, mit verschiedenen Eloquent-Modellen in Repositorys zu kommunizieren, oder sollte dies stattdessen mit anderen Repositorys erfolgen (ich meine, andere Repositorys aus dem Zuweisungs-Repository aufzurufen) ) oder sollte es in den Eloquent-Modellen alle zusammen gemacht werden?

Ist es auch eine gute Praxis, die Punktetabelle als Dreh- und Angelpunkt zwischen Aufgaben und Schülern zu verwenden, oder sollte sie woanders durchgeführt werden?

Antworten:


71

Denken Sie daran, dass Sie nach Meinungen fragen: D.

Hier ist meins:

TL; DR: Ja, das ist in Ordnung.

Du machst das gut!

Ich mache genau das, was du oft machst und finde, dass es großartig funktioniert.

Ich organisiere jedoch häufig Repositorys nach Geschäftslogik, anstatt ein Repo-per-Table zu haben. Dies ist nützlich, da es sich um eine Sichtweise handelt, die sich darauf konzentriert, wie Ihre Anwendung Ihr "Geschäftsproblem" lösen soll.

Ein Kurs ist eine "Entität" mit Attributen (Titel, ID usw.) und sogar anderen Entitäten (Zuweisungen, die ihre eigenen Attribute und möglicherweise Entitäten haben).

Ihr "Kurs" -Repository sollte in der Lage sein, einen Kurs und die Attribute / Aufgaben des Kurses (einschließlich der Aufgabe) zurückzugeben.

Zum Glück können Sie das mit Eloquent erreichen.

(Ich habe oft ein Repository pro Tabelle, aber einige Repositorys werden viel häufiger als andere verwendet und verfügen daher über viel mehr Methoden. Ihr Repository "Kurse" verfügt möglicherweise über viel mehr Funktionen als Ihr Repository "Zuweisungen", z Die Anwendung konzentriert sich mehr auf Kurse und weniger auf die Sammlung von Aufgaben.

Der schwierige Teil

Ich verwende häufig Repositorys in meinen Repositorys, um einige Datenbankaktionen auszuführen.

Jedes Repository, das Eloquent implementiert, um Daten zu verarbeiten, gibt wahrscheinlich Eloquent-Modelle zurück. Vor diesem Hintergrund ist es in Ordnung, wenn Ihr Kursmodell integrierte Beziehungen verwendet, um Zuweisungen (oder andere Anwendungsfälle) abzurufen oder zu speichern. Unsere "Implementierung" basiert auf Eloquent.

Aus praktischer Sicht ist dies sinnvoll. Es ist unwahrscheinlich, dass wir Datenquellen in etwas ändern, das Eloquent nicht verarbeiten kann (in eine Nicht-SQL-Datenquelle).

ORMS

Der schwierigste Teil dieses Setups besteht zumindest für mich darin, festzustellen, ob Eloquent uns tatsächlich hilft oder schadet. ORMs sind ein heikles Thema, da sie uns zwar aus praktischer Sicht sehr helfen, aber auch Ihren Code "Geschäftslogik-Entitäten" mit dem Code koppeln, der den Datenabruf durchführt.

Diese Art von Verwirrung darüber, ob die Verantwortung Ihres Repositorys tatsächlich für die Verarbeitung von Daten oder für das Abrufen / Aktualisieren von Entitäten (Geschäftsdomänenentitäten) liegt.

Darüber hinaus fungieren sie als die Objekte, die Sie an Ihre Ansichten übergeben. Wenn Sie später keine Eloquent-Modelle mehr in einem Repository verwenden müssen, müssen Sie sicherstellen, dass sich die an Ihre Ansichten übergebenen Variablen gleich verhalten oder über dieselben Methoden verfügen. Andernfalls führt das Ändern Ihrer Datenquellen zu einer Änderung Ihrer Ansichten, und Sie haben (teilweise) den Zweck verloren, Ihre Logik in erster Linie in Repositorys zu abstrahieren - die Wartbarkeit Ihres Projekts sinkt wie folgt.

Jedenfalls sind dies etwas unvollständige Gedanken. Sie sind, wie gesagt, nur meine Meinung, die zufällig das Ergebnis des Lesens von Domain Driven Design und des Ansehens von Videos wie der Keynote von "Onkel Bob" bei Ruby Midwest im letzten Jahr ist.


1
Wäre es Ihrer Meinung nach eine gute Alternative, wenn Repositorys Datenübertragungsobjekte anstelle von beredten Objekten zurückgeben? Dies würde natürlich eine zusätzliche Konvertierung von eloquent zu dto bedeuten, aber auf diese Weise isolieren Sie zumindest Ihre Controller / Ansichten von der aktuellen orm-Implementierung.
Federivo

1
Ich habe selbst ein bisschen damit experimentiert und fand es ein wenig unpraktisch. Davon abgesehen mag ich diese Idee abstrakt. Die Datenbank-Sammlungsobjekte von Illuminate verhalten sich jedoch genauso wie Arrays, und Modellobjekte verhalten sich genauso wie StdClass-Objekte, sodass wir praktisch bei Eloquent bleiben und auch in Zukunft Arrays / Objekte verwenden können, falls dies erforderlich sein sollte.
Fideloper

4
@fideloper Ich habe das Gefühl, dass ich die ganze Schönheit von ORM, die Eloquent bietet, verliere, wenn ich Repositories verwende. Beim Abrufen eines Kontoobjekts über meine Repository-Methode $a = $this->account->getById(1)kann ich Methoden wie nicht einfach verketten $a->getActiveUsers(). Okay, ich könnte verwenden $a->users->..., aber dann gebe ich eine Eloquent-Sammlung und kein stdClass-Objekt zurück und bin wieder an Eloquent gebunden. Was ist die Lösung dafür? Deklarieren Sie eine andere Methode im Benutzer-Repository wie $user->getActiveUsersByAccount($a->id);? Würde gerne hören, wie Sie dieses
Problem

1
ORMs sind für Architekturen auf Enterprise-Ebene (ish) schrecklich, weil sie solche Probleme verursachen. Am Ende müssen Sie entscheiden, was für Ihre Anwendung am sinnvollsten ist. Persönlich, wenn ich Repositorys mit Eloquent verwende (90% der Zeit!), Benutze ich Eloquent und versuche mein Bestes, um Modelle und Sammlungen wie stdClasses & Arrays zu behandeln (weil Sie können!). Wenn ich muss, ist es also möglich, zu etwas anderem zu wechseln.
Fideloper

5
Gehen Sie voran und verwenden Sie faul geladene Modelle. Sie können dafür sorgen, dass echte Domain-Modelle so funktionieren, wenn Sie Eloquent nicht mehr verwenden. Aber im Ernst, sind Sie Gonna Schalter aus Eloquent jemals? Auf einen Cent, auf ein Pfund! (Gehen Sie nicht über Bord und versuchen Sie, sich an "die Regeln" zu halten! Ich breche die ganze Zeit meine).
Fideloper

224

Ich beende ein großes Projekt mit Laravel 4 und musste alle Fragen beantworten, die Sie gerade stellen. Nachdem ich alle verfügbaren Laravel-Bücher bei Leanpub und jede Menge Googeln gelesen hatte, kam ich auf die folgende Struktur.

  1. Eine eloquente Modellklasse pro datierbarer Tabelle
  2. Eine Repository-Klasse pro eloquentem Modell
  3. Eine Serviceklasse, die zwischen mehreren Repository-Klassen kommunizieren kann.

Nehmen wir also an, ich baue eine Filmdatenbank auf. Ich hätte mindestens die folgenden folgenden eloquenten Modellklassen:

  • Film
  • Studio
  • Direktor
  • Darsteller
  • Rezension

Eine Repository-Klasse würde jede Eloquent Model-Klasse kapseln und für CRUD-Operationen in der Datenbank verantwortlich sein. Die Repository-Klassen könnten folgendermaßen aussehen:

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActorRepository
  • ReviewRepository

Jede Repository-Klasse würde eine BaseRepository-Klasse erweitern, die die folgende Schnittstelle implementiert:

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

Eine Service-Klasse wird zum Zusammenkleben mehrerer Repositorys verwendet und enthält die eigentliche "Geschäftslogik" der Anwendung. Controller kommunizieren nur mit Serviceklassen für Aktionen zum Erstellen, Aktualisieren und Löschen.

Wenn ich also einen neuen Movie-Datensatz in der Datenbank erstellen möchte, verfügt meine MovieController-Klasse möglicherweise über die folgenden Methoden:

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

Es liegt an Ihnen, zu bestimmen, wie Sie Daten an Ihre Controller senden. Angenommen, die von Input :: all () in der postCreate () -Methode zurückgegebenen Daten sehen ungefähr so ​​aus:

$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'
)

Da das MovieRepository nicht wissen sollte, wie Schauspieler-, Director- oder Studio-Datensätze in der Datenbank erstellt werden, verwenden wir unsere MovieService-Klasse, die ungefähr so ​​aussehen könnte:

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

Was uns also bleibt, ist eine nette, vernünftige Trennung der Bedenken. Repositorys kennen nur das Eloquent-Modell, das sie einfügen und aus der Datenbank abrufen. Controller interessieren sich nicht für Repositorys, sie geben lediglich die vom Benutzer gesammelten Daten weiter und geben sie an den entsprechenden Dienst weiter. Dem Dienst ist es egal, wie die empfangenen Daten in der Datenbank gespeichert werden. Er gibt lediglich die relevanten Daten, die er vom Controller erhalten hat, an die entsprechenden Repositorys weiter.


8
Dieser Kommentar ist bei weitem der sauberere, skalierbarere und wartbarere Ansatz.
Andreas

4
+1! Das wird mir sehr helfen, danke, dass du es mit uns geteilt hast! Wenn Sie sich fragen, wie Sie es geschafft haben, Dinge innerhalb von Diensten zu validieren, wenn möglich, können Sie kurz erklären, was Sie getan haben? Trotzdem danke! :)
Paulo Freitas

6
Wie @PauloFreitas sagte, wäre es interessant zu sehen, wie Sie mit dem Validierungsteil umgehen, und ich würde mich auch für den Ausnahmeteil interessieren (verwenden Sie Ausnahmen, Ereignisse oder behandeln Sie dies einfach so, wie Sie es in Ihrem vorschlagen) Controller über eine boolesche Rückgabe in Ihren Diensten?). Vielen Dank!
Nicolas

11
Gutes Schreiben, obwohl ich nicht sicher bin, warum Sie movieRepository in MovieController einfügen, da der Controller nichts direkt mit dem Repository tun sollte und Ihre postCreate-Methode das movieRepository nicht verwendet. Ich gehe also davon aus, dass Sie es versehentlich belassen haben ?
Davidnknight

15
Frage dazu: Warum verwenden Sie in diesem Beispiel Repositorys? Dies ist eine ehrliche Frage - für mich sieht es so aus, als würden Sie Repositorys verwenden, aber zumindest in diesem Beispiel macht das Repository nichts anderes als die gleiche Schnittstelle wie Eloquent, und am Ende sind Sie immer noch an Eloquent gebunden, weil Ihre Serviceklasse verwendet eloquent direkt darin ( $studio->movies()->associate($movie);).
Kevin Mitchell

5

Ich denke gerne darüber nach, was mein Code tut und wofür er verantwortlich ist, anstatt "richtig oder falsch". So breche ich meine Verantwortlichkeiten auseinander:

  • Controller sind die HTTP-Schicht und leiten Anforderungen an die zugrunde liegende API weiter (auch bekannt als Steuerung des Flusses).
  • Modelle stellen das Datenbankschema dar und teilen der Anwendung mit, wie die Daten aussehen, welche Beziehungen sie haben können und welche globalen Attribute möglicherweise erforderlich sind (z. B. eine Namensmethode zum Zurückgeben eines verketteten Vor- und Nachnamens).
  • Repositorys stellen die komplexeren Abfragen und Interaktionen mit den Modellen dar (ich mache keine Abfragen zu Modellmethoden).
  • Suchmaschinen - Klassen, mit denen ich komplexe Suchanfragen erstellen kann.

Vor diesem Hintergrund ist es jedes Mal sinnvoll, ein Repository zu verwenden (ob Sie interfaces.etc. Erstellen, ist ein ganz anderes Thema). Ich mag diesen Ansatz, weil ich genau weiß, wohin ich gehen muss, wenn ich bestimmte Arbeiten ausführen muss.

Ich neige auch dazu, ein Basis-Repository zu erstellen, normalerweise eine abstrakte Klasse, die die Hauptstandards definiert - im Grunde CRUD-Operationen, und dann kann jedes Kind einfach nach Bedarf Methoden erweitern und hinzufügen oder die Standardeinstellungen überladen. Durch das Injizieren Ihres Modells ist dieses Muster auch recht robust.


Können Sie Ihre Implementierung Ihres BaseRepository zeigen? Ich mache das auch und bin gespannt, was du gemacht hast.
Odyssee

Denken Sie an getById, getByName, getByTitle und speichern Sie den Typ methods.etc. - im Allgemeinen Methoden, die für alle Repositorys in verschiedenen Domänen gelten.
Oddman

5

Stellen Sie sich Repositories als einen konsistenten Aktenschrank Ihrer Daten vor (nicht nur Ihrer ORMs). Die Idee ist, dass Sie Daten in einer konsistenten, einfach zu verwendenden API erfassen möchten.

Wenn Sie nur Model :: all (), Model :: find (), Model :: create () ausführen, werden Sie wahrscheinlich nicht viel davon profitieren, ein Repository zu abstrahieren. Wenn Sie andererseits Ihre Abfragen oder Aktionen etwas geschäftlicher gestalten möchten, möchten Sie möglicherweise ein Repository erstellen, um die Verwendung der API für den Umgang mit Daten zu vereinfachen.

Ich denke, Sie haben gefragt, ob ein Repository der beste Weg ist, um mit der ausführlicheren Syntax umzugehen, die zum Verbinden verwandter Modelle erforderlich ist. Abhängig von der Situation kann ich einige Dinge tun:

  1. Wenn ich ein neues untergeordnetes Modell von einem übergeordneten Modell (eins-eins oder eins-viele) abhänge, füge ich dem untergeordneten Repository eine Methode hinzu, die createWithParent($attributes, $parentModelInstance)einfach $parentModelInstance->idin das parent_idFeld der Attribute eingefügt wird, und rufe create auf.

  2. Wenn ich eine Viele-Viele-Beziehung anhänge, erstelle ich tatsächlich Funktionen für die Modelle, damit ich $ instance-> attachChild ($ childInstance) ausführen kann. Beachten Sie, dass hierfür auf beiden Seiten vorhandene Elemente erforderlich sind.

  3. Wenn ich verwandte Modelle in einem Durchgang erstelle, erstelle ich etwas, das ich als Gateway bezeichne (es kann etwas von Fowlers Definitionen abweichen). So kann ich $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) anstelle einer Reihe von Logik aufrufen, die sich möglicherweise ändert oder die Logik, die ich in einem Controller oder Befehl habe, komplizieren würde.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.