Strategie per evitare SQL nei controller ... o quanti metodi dovrei avere nei miei modelli?

17

Quindi una situazione in cui mi imbatto ragionevolmente spesso è quella in cui i miei modelli iniziano a:

  • Crescere in mostri con tonnellate e tonnellate di metodi

o

  • Permetti di passare a loro pezzi di SQL, in modo che siano abbastanza flessibili da non richiedere un milione di metodi diversi

Ad esempio, diciamo che abbiamo un modello "widget". Iniziamo con alcuni metodi di base:

  • get ($ id)
  • Inserisci ($ record)
  • aggiornamento ($ id, $ record)
  • delete ($ id)
  • getList () // ottiene un elenco di widget

Va tutto bene e dandy, ma poi abbiamo bisogno di alcuni rapporti:

  • listCreatedBetween ($ start_date, $ end_date)
  • listPurchasedBetween ($ start_date, $ end_date)
  • listOfPending ()

E poi il reporting inizia a diventare complesso:

  • listPendingCreatedBetween ($ start_date, $ end_date)
  • listForCustomer ($ CUSTOMER_ID)
  • listPendingCreatedBetweenForCustomer ($ customer_id, $ start_date, $ end_date)

Puoi vedere dove sta crescendo ... alla fine abbiamo tanti requisiti di query specifici che ho bisogno di implementare tonnellate e tonnellate di metodi, o qualche tipo di oggetto "query" che posso passare a un singolo - & gt ; metodo query (query $ query) ...

... o morde semplicemente il proiettile e inizia a fare qualcosa del genere:

  • list = MyModel- > query ("start_date > X AND end_date < Y AND in sospeso = 1 AND customer_id = Z")

C'è un certo fascino nell'avere un solo metodo come quello invece di 50 milioni di altri metodi più specifici ... ma a volte sembra "sbagliato" fare una pila di ciò che è fondamentalmente SQL nel controller.

Esiste un modo "giusto" per gestire situazioni come questa? Sembra accettabile riempire query del genere in un metodo generico - > query ()?

Ci sono strategie migliori?

    
posta Keith Palmer Jr. 06.04.2012 - 03:50
fonte

6 risposte

10

I Patterns of Enterprise Application Architecture di Martin Fowler descrivono un numero di paterns correlati a ORM, incluso l'uso dell'oggetto di query , che è quello che suggerirei.

Gli oggetti di query consentono di seguire il principio di responsabilità unica, separando la logica per ogni query in oggetti di strategia gestiti e gestiti singolarmente. O il controller può gestirne direttamente l'utilizzo o delegarlo a un controller secondario o ad un oggetto helper.

Ne avrai molti? Certamente. Alcuni possono essere raggruppati in query generiche? Sì di nuovo.

È possibile utilizzare l'iniezione di dipendenza per creare gli oggetti dai metadati? Ecco cosa fa la maggior parte degli strumenti ORM.

    
risposta data 06.04.2012 - 14:27
fonte
4

Non esiste un modo corretto per farlo. Molte persone usano gli ORM per astrarre tutta la complessità. Alcuni degli ORM più avanzati traducono le espressioni di codice in istruzioni SQL complicate. Anche gli ORM hanno i loro lati negativi, tuttavia per molte applicazioni i benefici superano i costi.

Se non stai lavorando con un enorme set di dati, la cosa più semplice da fare è selezionare l'intera tabella in memoria e filtrare il codice.

//pseudocode
List<Person> people = Sql.GetList<Person>("select * from people");
List<Person> over21 = people.Where(x => x.Age >= 21);

Per le applicazioni di reporting interno questo approccio probabilmente va bene. Se il set di dati è molto grande, inizierai a richiedere molti metodi personalizzati e indici appropriati sul tuo tavolo.

    
risposta data 06.04.2012 - 07:34
fonte
4

Alcuni ORM ti permettono di costruire query complesse partendo da metodi di base. Ad esempio

old_purchases = (Purchase.objects
    .filter(date__lt=date.today(),type=Purchase.PRESENT).
    .excude(status=Purchase.REJECTED)
    .order_by('customer'))

è una query perfettamente valida in Django ORM .

L'idea è che tu abbia un generatore di query (in questo caso Purchase.objects ) il cui stato interno rappresenta le informazioni su una query. Metodi come get , filter , exclude , order_by sono validi e restituiscono un nuovo generatore di query con uno stato aggiornato. Questi oggetti implementano un'interfaccia iterabile, in modo che quando si esegue un'iterazione su di essi, la query viene eseguita e si ottengono i risultati della query costruita finora. Sebbene questo esempio sia preso da Django, vedrai la stessa struttura in molti altri ORM.

    
risposta data 06.04.2012 - 07:49
fonte
2

C'è un terzo approccio.

Il tuo esempio specifico mostra una crescita esponenziale del numero di metodi necessari al crescere del numero di funzionalità richieste: vogliamo la capacità di offrire query avanzate, combinando ogni funzione di query ... se lo facciamo aggiungendo metodi, ne abbiamo uno metodo per una query di base, due se aggiungiamo una funzione opzionale, quattro se aggiungiamo due, otto se aggiungiamo tre, 2 ^ n se aggiungiamo n funzionalità.

Ovviamente è irrintracciabile oltre tre o quattro funzioni, e c'è un cattivo odore di un sacco di codice strettamente correlato che è quasi copiato tra i metodi.

È possibile evitare ciò aggiungendo un oggetto dati per contenere i parametri e disporre di un singolo metodo che costruisce la query in base all'insieme di parametri forniti (o non forniti). In tal caso, aggiungere una nuova funzionalità come un intervallo di date è semplice come aggiungere setter e getter per l'intervallo di date all'oggetto dati e quindi aggiungere un po 'di codice in cui viene creata la query con parametri:

if (dataObject.getStartDate() != null) {
    query += " AND (date BETWEEN ? AND ?) "
}

... e dove i parametri vengono aggiunti alla query:

if (dataObject.getStartDate() != null) {
    preparedStatement.setTime(dataObject.getStartDate());
    preparedStatement.setTime(dataObject.getEndDate());
}

Questo approccio consente la crescita lineare del codice quando vengono aggiunte funzionalità, senza dover consentire query arbitrarie e non parametrizzate.

    
risposta data 06.04.2012 - 18:42
fonte
0

Penso che il consenso generale sia mantenere quanto più possibile l'accesso ai dati nei tuoi modelli in MVC. Uno degli altri principi di progettazione è spostare alcune delle query più generiche (quelle che non sono direttamente correlate al modello) ad un livello più alto e più astratto in cui è possibile consentire che venga utilizzato anche da altri modelli. (In RoR, abbiamo qualcosa chiamato framework) C'è anche un'altra cosa che devi considerare e che è la manutenibilità del tuo codice. Man mano che il tuo progetto cresce, se hai accesso ai dati nei controller, diventerà sempre più difficile rintracciarlo (stiamo affrontando questo problema in un grande progetto) I modelli, sebbene ingombri di metodi forniscono un singolo punto di contatto per ogni controllore che potrebbe finire con l'interrogare dai tavoli. (Questo può anche portare a un riutilizzo del codice che a sua volta è vantaggioso)

    
risposta data 06.04.2012 - 11:17
fonte
0

L'interfaccia del livello di servizio può avere molti metodi, ma la chiamata al database può avere solo uno.

Un database ha 4 operazioni principali

  • Inserisci
  • Aggiorna
  • Elimina
  • Query

Un altro metodo facoltativo potrebbe essere l'esecuzione di alcune operazioni del database che non rientrano nelle operazioni di base del DB. Chiamiamo Esegui.

Inserisci e aggiornamenti possono essere combinati in un'unica operazione, chiamata Salva.

Molti dei tuoi metodi sono di query. Quindi puoi creare un'interfaccia generica per soddisfare la maggior parte delle necessità immediate. Ecco un'interfaccia generica di esempio:

 public interface IDALService
    {
        DataTransferObject<T> Save<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Search<T>(DataTransferObject<T> Dto) where T: IPOCO;
        DataTransferObject<T> Delete<T>(DataTransferObject<T> Dto) where T : IPOCO;
        DataTransferObject<T> Execute<T>(DataTransferObject<T> Dto) where T : IPOCO;
    }

L'oggetto di trasferimento dati è generico e contiene tutti i filtri, i parametri, l'ordinamento, ecc. contenuti al suo interno. Il livello dati sarebbe responsabile dell'analisi e dell'estrazione di questa operazione e dell'impostazione dell'operazione nel database tramite stored procedure, sql parametrizzato, linq ecc. Pertanto, SQL non viene passato tra i livelli. Questo è in genere ciò che fa un ORM, ma puoi eseguire il rollover e avere la tua mappatura.

Quindi, nel tuo caso hai i widget. I widget implementerebbero l'interfaccia IPOCO.

Quindi, nel tuo modello di livello di servizio avrebbe getList().

Avrebbe bisogno di un livello di mappatura per gestire la trasformazione di getList in

Search<Widget>(DataTransferObject<Widget> Dto)

e viceversa. Come altri hanno già detto, a volte questo viene fatto tramite un ORM, ma alla fine si finisce con un sacco di tipi di codice boilerplate, specialmente se si hanno centinaia di tabelle. L'ORM crea magicamente l'SQL parametrizzato e lo esegue rispetto al database. Se si esegue il rollover proprio, inoltre nel livello dati stesso, i mapper sarebbero necessari per impostare SP, linq ecc. (Fondamentalmente lo sql che va al database).

Come accennato in precedenza, il DTO è un oggetto costituito dalla composizione. Forse uno degli oggetti contenuti al suo interno è un oggetto chiamato QueryParameters. Questi sarebbero tutti i parametri per la query che verrebbero configurati e utilizzati dalla query. Un altro oggetto sarebbe un elenco di oggetti restituiti da querys, aggiornamenti, ext. Questo è il carico utile. In questo caso il carico utile sarebbe un elenco di elenchi di widget.

Quindi, la strategia di base è:

  • Chiamate a livello di servizio
  • Trasforma la chiamata al livello del servizio nel database usando una sorta di Repository / Mapping
  • Chiamata al database

Nel tuo caso penso che il modello possa avere molti metodi, ma in modo ottimale vuoi che la chiamata al database sia generica. Si finisce comunque con un sacco di codice di mappatura boilerplate (specialmente con SP) o codice ORM magico che crea dinamicamente l'SQL parametrizzato per te.

    
risposta data 06.04.2012 - 17:47
fonte

Leggi altre domande sui tag