Gestire le relazioni in Laravel, aderendo al modello di repository


120

Durante la creazione di un'app in Laravel 4 dopo aver letto il libro di T. Otwell sui buoni modelli di progettazione in Laravel, mi sono ritrovato a creare repository per ogni tabella dell'applicazione.

Ho finito con la seguente struttura della tabella:

  • Studenti: id, nome
  • Corsi: id, name, teacher_id
  • Insegnanti: id, nome
  • Compiti: id, nome, course_id
  • Punteggi (funge da perno tra studenti e compiti): student_id, assignment_id, score

Ho classi di repository con metodi di ricerca, creazione, aggiornamento ed eliminazione per tutte queste tabelle. Ogni repository ha un modello Eloquent che interagisce con il database. Le relazioni sono definite nel modello secondo la documentazione di Laravel: http://laravel.com/docs/eloquent#relationships .

Quando creo un nuovo corso, tutto ciò che faccio è chiamare il metodo create nel Repository del corso. Quel corso ha dei compiti, quindi quando ne creo uno, voglio anche creare una voce nella tabella dei punteggi per ogni studente del corso. Lo faccio tramite l'Assignment Repository. Ciò implica che l'archivio delle assegnazioni comunica con due modelli Eloquent, con il modello Assignment e Student.

La mia domanda è: poiché questa app probabilmente aumenterà di dimensioni e verranno introdotte più relazioni, è buona norma comunicare con diversi modelli eloquenti nei repository o dovrebbe essere fatto utilizzando invece altri repository (intendo chiamare altri repository dal repository Assignment ) o dovrebbe essere fatto nei modelli Eloquent tutti insieme?

Inoltre, è buona pratica utilizzare la tabella dei punteggi come perno tra i compiti e gli studenti o dovrebbe essere eseguita da qualche altra parte?

Risposte:


71

Tieni presente che stai chiedendo opinioni: D

Ecco il mio:

TL; DR: Sì, va bene.

Stai facendo bene!

Faccio esattamente quello che fai spesso e trovo che funzioni alla grande.

Spesso, tuttavia, organizzo i repository in base alla logica aziendale invece di avere un repository per tabella. Questo è utile in quanto è un punto di vista incentrato su come la tua applicazione dovrebbe risolvere il tuo "problema aziendale".

Un corso è una "entità", con attributi (titolo, id, ecc.) E anche altre entità (compiti, che hanno i propri attributi e possibilmente entità).

Il tuo archivio "Corso" dovrebbe essere in grado di restituire un corso e gli attributi / compiti dei corsi (incluso il compito).

Puoi farlo con Eloquent, fortunatamente.

(Spesso mi ritrovo con un repository per tabella, ma alcuni repository sono usati molto più di altri, quindi ho molti più metodi. Il tuo repository "corsi" potrebbe essere molto più completo del tuo repository Compiti, ad esempio, se il tuo l'applicazione si concentra più sui corsi e meno sulla raccolta di compiti di un corso).

La parte difficile

Uso spesso i repository all'interno dei miei repository per eseguire alcune azioni sul database.

Qualsiasi repository che implementa Eloquent per gestire i dati probabilmente restituirà modelli Eloquent. Alla luce di ciò, va bene se il modello del corso utilizza relazioni integrate per recuperare o salvare i compiti (o qualsiasi altro caso d'uso). La nostra "implementazione" si basa su Eloquent.

Da un punto di vista pratico, questo ha senso. È improbabile che cambieremo le origini dati in qualcosa che Eloquent non può gestire (in un'origine dati non sql).

ORMS

La parte più complicata di questa configurazione, almeno per me, è determinare se Eloquent ci sta effettivamente aiutando o danneggiando. Gli ORM sono un argomento delicato, perché mentre ci aiutano molto da un punto di vista pratico, accoppiano anche il codice delle "entità logiche di business" con il codice che esegue il recupero dei dati.

Questo tipo di confusione si confonde se la responsabilità del tuo repository è effettivamente per la gestione dei dati o la gestione del recupero / aggiornamento di entità (entità di dominio aziendale).

Inoltre, agiscono come gli stessi oggetti che passi alle tue opinioni. Se in un secondo momento devi evitare di utilizzare modelli eloquenti in un repository, dovrai assicurarti che le variabili passate alle tue viste si comportino allo stesso modo o abbiano gli stessi metodi disponibili, altrimenti la modifica delle origini dati cambierà il tuo visualizzazioni, e hai (parzialmente) perso lo scopo di astrarre la tua logica nei repository in primo luogo: la manutenibilità del tuo progetto diminuisce come.

Comunque, questi sono pensieri alquanto incompleti. Sono, come affermato, solo la mia opinione, che sembra essere il risultato della lettura di Domain Driven Design e della visione di video come il keynote di "zio bob" al Ruby Midwest nell'ultimo anno.


1
Secondo te, sarebbe una buona alternativa se i repository restituissero oggetti di trasferimento dati invece di oggetti eloquenti? Ovviamente ciò implicherebbe una conversione extra da eloquente a dto, ma in questo modo, almeno, isolerai i tuoi controller / viste dall'attuale implementazione di orm.
federivo

1
L'ho sperimentato un po 'da solo e l'ho trovato un po' poco pratico. Detto questo, mi piace l'idea in astratto. Tuttavia, gli oggetti Collection del database di Illuminate agiscono proprio come array e gli oggetti Model agiscono proprio come oggetti StdClass abbastanza da poter, in pratica, restare con Eloquent e continuare a utilizzare array / oggetti in futuro, se necessario.
fideloper

4
@fideloper Sento che se uso i repository perdo tutta la bellezza di ORM che Eloquent fornisce. Quando recupero un oggetto account tramite il mio metodo di repository $a = $this->account->getById(1)non posso semplicemente concatenare metodi come $a->getActiveUsers(). Va bene, potrei usare $a->users->..., ma poi restituisco una raccolta Eloquent e nessun oggetto stdClass e sono di nuovo legato a Eloquent. Qual è la soluzione a questo? Dichiarare un altro metodo nel repository utente come $user->getActiveUsersByAccount($a->id);? Mi piacerebbe sapere come risolvi questo ...
Santacruz

1
Gli ORM sono terribili per l'architettura di livello Enterprise (ish) perché causano problemi come questo. Alla fine, devi decidere cosa ha più senso per la tua applicazione. Personalmente quando uso i repository con Eloquent (il 90% delle volte!) Uso Eloquent e faccio del mio meglio per trattare modelli e collezioni come stdClasses e Arrays (perché puoi!) Quindi, se necessario, passare a qualcos'altro è possibile.
fideloper

5
Vai avanti e usa modelli pigri. Puoi fare in modo che i modelli di dominio reali funzionino in questo modo se non usi Eloquent. Ma seriamente, cambierai mai Eloquent? Per un centesimo, per una libbra! (Non esagerare cercando di attenermi alle "regole"! Infrango tutte le mie tutto il tempo).
fideloper

224

Sto finendo un grande progetto usando Laravel 4 e ho dovuto rispondere a tutte le domande che mi stai ponendo in questo momento. Dopo aver letto tutti i libri di Laravel disponibili su Leanpub e un sacco di ricerche su Google, ho trovato la seguente struttura.

  1. Una classe Eloquent Model per tabella databile
  2. Una classe di repository per modello eloquente
  3. Una classe di servizio che può comunicare tra più classi di repository.

Quindi diciamo che sto costruendo un database di film. Avrei almeno le seguenti classi di modello eloquente:

  • Film
  • Studio
  • Direttore
  • Attore
  • Revisione

Una classe di repository incapsulerebbe ogni classe del modello eloquente e sarebbe responsabile delle operazioni CRUD sul database. Le classi del repository potrebbero assomigliare a questa:

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActorRepository
  • ReviewRepository

Ogni classe di repository estenderebbe una classe BaseRepository che implementa la seguente interfaccia:

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

Una classe Service viene utilizzata per incollare più repository insieme e contiene la vera "logica di business" dell'applicazione. I controller comunicano solo con le classi di servizio per le azioni Crea, Aggiorna ed Elimina.

Quindi, quando voglio creare un nuovo record di film nel database, la mia classe MovieController potrebbe avere i seguenti metodi:

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

Sta a te determinare come POST i dati ai tuoi controller, ma diciamo che i dati restituiti da Input :: all () nel metodo postCreate () hanno un aspetto simile a questo:

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

Poiché MovieRepository non dovrebbe sapere come creare record Attore, Regista o Studio nel database, useremo la nostra classe MovieService, che potrebbe essere simile a questa:

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

Quindi quello che ci resta è una bella e sensata separazione delle preoccupazioni. I repository riconoscono solo il modello eloquente che inseriscono e recuperano dal database. I controller non si preoccupano dei repository, si limitano a trasferire i dati che raccolgono dall'utente e li passano al servizio appropriato. Il servizio non si preoccupa di come i dati che riceve vengono salvati nel database, si limita a trasferire i dati rilevanti che sono stati forniti dal controller ai repository appropriati.


8
Questo commento è di gran lunga l'approccio più pulito, più scalabile e manutenibile.
Andreas

4
+1! Questo mi aiuterà molto, grazie per aver condiviso con noi! Chiedendosi come sei riuscito a convalidare le cose all'interno dei servizi, se possibile, potresti spiegare brevemente cosa hai fatto? Grazie lo stesso! :)
Paulo Freitas

6
Come ha detto @PauloFreitas, sarebbe interessante vedere come gestisci la parte di convalida, e sarei interessato anche alla parte delle eccezioni (usi eccezioni, eventi o gestisci questo come sembri suggerire nel tuo controller tramite un ritorno booleano nei tuoi servizi?). Grazie!
Nicolas

11
Buona scrittura, anche se non sono sicuro del motivo per cui stai iniettando movieRepository in MovieController poiché il controller non dovrebbe fare nulla direttamente con il repository, né il tuo metodo postCreate utilizza il movieRepository, quindi presumo che tu lo abbia lasciato per errore ?
davidnknight

15
Domanda su questo: perché stai usando i repository in questo esempio? Questa è una domanda onesta: per me sembra che tu stia usando i repository ma almeno in questo esempio il repository in realtà non fa altro che fornire la stessa interfaccia di Eloquent, e alla fine sei ancora legato a Eloquent perché la tua classe di servizio sta usando eloquent direttamente in esso ( $studio->movies()->associate($movie);).
Kevin Mitchell

5

Mi piace pensarlo in termini di ciò che fa il mio codice e di cosa è responsabile, piuttosto che "giusto o sbagliato". Ecco come rompo le mie responsabilità:

  • I controller sono il livello HTTP e instradano le richieste attraverso le API sottostanti (ovvero, controlla il flusso)
  • I modelli rappresentano lo schema del database e indicano all'applicazione l'aspetto dei dati, le relazioni che possono avere, nonché gli attributi globali che potrebbero essere necessari (come un metodo del nome per restituire un nome e un cognome concatenati)
  • I repository rappresentano le query e le interazioni più complesse con i modelli (non eseguo query sui metodi del modello).
  • Motori di ricerca: classi che mi aiutano a creare query di ricerca complesse.

Con questo in mente, ha senso ogni volta usare un repository (se crei interfaces.etc. È un altro argomento). Mi piace questo approccio, perché significa che so esattamente dove andare quando ho bisogno di fare un determinato lavoro.

Tendo anche a costruire un repository di base, di solito una classe astratta che definisce i valori predefiniti principali - fondamentalmente operazioni CRUD, e quindi ogni bambino può semplicemente estendere e aggiungere metodi se necessario, o sovraccaricare i valori predefiniti. L'iniezione del modello aiuta anche questo modello a essere abbastanza robusto.


Puoi mostrare la tua implementazione del tuo BaseRepository? In realtà lo faccio anche io e sono curioso di cosa hai fatto.
Odyssee

Pensa a getById, getByName, getByTitle, save type methods.etc. - generalmente metodi che si applicano a tutti i repository all'interno di vari domini.
Oddman

5

Pensa ai repository come a un archivio coerente dei tuoi dati (non solo dei tuoi ORM). L'idea è che si desidera acquisire i dati in un'API coerente e semplice da usare.

Se ti ritrovi a fare solo Model :: all (), Model :: find (), Model :: create () probabilmente non trarrai grande beneficio dall'astrazione di un repository. D'altra parte, se vuoi fare un po 'più di logica aziendale alle tue query o azioni, potresti voler creare un repository per rendere un'API più facile da usare per la gestione dei dati.

Penso che ti stavi chiedendo se un repository sarebbe il modo migliore per gestire alcune delle sintassi più dettagliate richieste per connettere modelli correlati. A seconda della situazione, ci sono alcune cose che posso fare:

  1. Appendere un nuovo modello figlio da un modello genitore (uno-uno o uno-molti), aggiungerei un metodo al repository figlio qualcosa di simile createWithParent($attributes, $parentModelInstance)e questo aggiungerei semplicemente il $parentModelInstance->idnel parent_idcampo degli attributi e chiamerei create.

  2. Collegando una relazione molti-molti, creo effettivamente funzioni sui modelli in modo da poter eseguire $ instance-> attachChild ($ childInstance). Nota che questo richiede elementi esistenti su entrambi i lati.

  3. Creando modelli correlati in una volta, creo qualcosa che chiamo Gateway (potrebbe essere un po 'diverso dalle definizioni di Fowler). Modo in cui posso chiamare $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) invece di un mucchio di logica che potrebbe cambiare o che complicherebbe la logica che ho in un controller o comando.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.