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


17

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

  • Cresci in mostri con tonnellate e tonnellate di metodi

O

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

Ad esempio, supponiamo di avere un modello "widget". Iniziamo con alcuni metodi di base:

  • ottenere ($ id)
  • inserire ($ 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)
  • listPurchasedB Between ($ start_date, $ end_date)
  • listOfPending ()

E poi i rapporti iniziano a diventare complessi:

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

Puoi vedere dove sta crescendo ... alla fine abbiamo così tanti requisiti di query specifici che ho bisogno di implementare tonnellate e tonnellate di metodi, o una sorta di oggetto "query" che posso passare a una singola -> query (query $ query) metodo ...

... o semplicemente mordi 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 appello ad avere un metodo del genere invece di 50 milioni di altri metodi più specifici ... ma a volte sembra "sbagliato" riempire una pila di ciò che è fondamentalmente SQL nel controller.

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

Ci sono strategie migliori?


Sto attraversando questo stesso problema in questo momento in un progetto non MVC. La domanda continua a sorgere se il livello di accesso ai dati dovesse sottrarre ogni procedura memorizzata e lasciare agnostico il database del livello di logica aziendale, o il livello di accesso ai dati dovrebbe essere generico, a costo del livello aziendale che sa qualcosa sul database sottostante? Forse una soluzione intermedia è avere qualcosa come ExecuteSP (string spName, parametri oggetto params []), quindi includere tutti i nomi SP in un file di configurazione da leggere per il livello aziendale. Non ho davvero un'ottima risposta a questo, però.
Greg Jackson,

Risposte:


10

Patterns of Enterprise Application Architecture di Martin Fowler descrive una serie di patern relativi a ORM, incluso l'uso dell'oggetto query, che è ciò che suggerirei.

Gli oggetti query consentono di seguire il principio di responsabilità singola, separando la logica di ogni query in oggetti strategia gestiti e gestiti individualmente. Il controller può gestirne direttamente l'uso o delegarlo a un controller secondario o un oggetto helper.

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

Puoi usare l'iniezione delle dipendenze per creare gli oggetti dai metadati? Questo è ciò che fa la maggior parte degli strumenti ORM.


4

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

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

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

Per le applicazioni di reportistica interna questo approccio probabilmente va bene. Se il set di dati è davvero grande, inizierai a necessitare di molti metodi personalizzati e di indici appropriati sulla tua tabella.


1
+ 1 per "Non esiste un modo corretto per farlo"
ozz,

1
Sfortunatamente, il filtraggio al di fuori del set di dati non è in realtà un'opzione con nemmeno i set di dati più piccoli con cui lavoriamo, è solo troppo lento. :-( È bello sapere che altri hanno riscontrato il mio stesso problema. :-)
Keith Palmer Jr.

@KeithPalmer per curiosità, quanto sono grandi i tuoi tavoli?
dan

Centinaia di migliaia di file, se non di più. Troppi per filtrare con prestazioni accettabili al di fuori del database, in particolare con un'architettura distribuita in cui i database non si trovano sullo stesso computer dell'applicazione.
Keith Palmer Jr.

-1 per "Non esiste un modo corretto per farlo". Esistono diversi modi corretti. Raddoppiare il numero di metodi quando si aggiunge una funzione mentre l'OP stava facendo è un approccio non scalabile, e l'alternativa suggerita qui è ugualmente non scalabile, solo per quanto riguarda le dimensioni del database piuttosto che il numero di funzioni di query. Esistono approcci scalabili, vedi le altre risposte.
Theodore Murdock,

4

Alcuni ORM consentono di creare query complesse a partire da metodi di base. Per 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 hai un generatore di query (in questo caso Purchase.objects) il cui stato interno rappresenta informazioni su una query. Metodi come get, filter, exclude, order_bysono validi e restituiscono un nuovo generatore di query con lo stato aggiornato. Questi oggetti implementano un'interfaccia iterabile, in modo che quando si esegue l'iterazione su di essi, la query viene eseguita e si ottengono i risultati della query costruiti finora. Sebbene questo esempio sia preso da Django, vedrai la stessa struttura in molti altri ORM.


Non vedo quale vantaggio abbia su qualcosa come old_purchases = Purchases.query ("date> date.today () AND type = Purchase.PRESENT AND status! = Purchase.REJECTED"); Non stai riducendo la complessità o astrarre qualcosa semplicemente trasformando SQL AND e OR in metodi AND e OR - stai semplicemente cambiando la rappresentazione di AND e OR, giusto?
Keith Palmer Jr.

4
In realtà no. Stai eliminando l'SQL, che ti offre molti vantaggi. Innanzitutto, eviti l'iniezione. Quindi, è possibile modificare il database sottostante senza preoccuparsi di versioni leggermente diverse del dialetto SQL, poiché l'ORM gestisce questo per te. In molti casi, puoi anche inserire un back-end NoSQL senza accorgertene. Terzo, questi generatori di query sono oggetti che è possibile passare come qualsiasi altra cosa. Ciò significa che il tuo modello può costruire metà della query (ad esempio potresti avere alcuni metodi per i casi più comuni) e quindi può essere perfezionato nel controller per gestire il ..
Andrea

2
... casi più specifici. Un esempio tipico è la definizione di un ordinamento predefinito per i modelli in Django. Tutti i risultati della query seguiranno tale ordine, salvo diversamente specificato. In quarto luogo, se hai mai bisogno di denormalizzare i tuoi dati per motivi di prestazioni, devi solo modificare l'ORM anziché riscrivere tutte le tue query.
Andrea,

+1 Per linguaggi di query dinamici come quello menzionato e LINQ.
Evan Plaice,

2

C'è un terzo approccio.

Il tuo esempio specifico mostra una crescita esponenziale del numero di metodi necessari all'aumentare del numero di funzionalità richieste: vogliamo la possibilità di offrire query avanzate, combinando tutte le funzionalità di query ... se lo facciamo aggiungendo metodi, abbiamo un metodo per un query di base, due se si aggiunge una funzione opzionale, quattro se si aggiungono due, otto se si aggiungono tre, 2 ^ n se si aggiungono n funzionalità.

Questo è ovviamente irraggiungibile oltre tre o quattro funzionalità, e c'è un cattivo odore di un sacco di codice strettamente correlato che è quasi incollato tra i metodi.

È possibile evitare ciò aggiungendo un oggetto dati per contenere i parametri e disporre di un singolo metodo che crea 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 di codice lineare man mano che vengono aggiunte funzionalità, senza dover consentire query arbitrarie e non parametrizzate.


0

Penso che il consenso generale sia quello di mantenere il più possibile l'accesso ai dati nei tuoi modelli in MVC. Uno degli altri principi di progettazione è quello di spostare alcune delle tue query più generiche (quelle che non sono direttamente correlate al tuo modello) a un livello più alto e più astratto in cui puoi permetterne l'utilizzo 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 (al momento stiamo affrontando questo problema in un grande progetto) I modelli, sebbene ingombri di metodi forniscono un unico punto di contatto per qualsiasi controller che potrebbe finire per eseguire una query dalle tabelle. (Ciò può anche portare a un riutilizzo del codice che a sua volta è vantaggioso)


1
Esempio di cosa stai parlando ...?
Keith Palmer Jr.

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

  • Inserire
  • Aggiornare
  • Elimina
  • domanda

Un altro metodo facoltativo potrebbe essere quello di eseguire alcune operazioni del database che non rientrano nelle operazioni di base del DB. Chiamiamolo Execute.

Inserisci e Aggiornamenti possono essere combinati in un'unica operazione, denominata Salva.

Molti dei tuoi metodi sono query. Quindi puoi creare un'interfaccia generica per soddisfare la maggior parte delle esigenze 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 trasferimento dati è generico e contiene tutti i filtri, i parametri, l'ordinamento, ecc. Al suo interno. Il livello dati sarebbe responsabile dell'analisi, dell'estrazione e della configurazione dell'operazione nel database tramite stored procedure, sql con parametri, linq ecc. Quindi, SQL non viene passato tra i livelli. Questo è in genere ciò che fa un ORM, ma è possibile eseguire il roll-up e disporre della propria mappatura.

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

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

Avrebbe bisogno di uno strato di mappatura per manico tranforming getListin

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 codice di tipo boilerplate, specialmente se si hanno centinaia di tabelle. L'ORM crea magicamente un SQL parametizzato e lo esegue sul database. Se esegui il rollup del tuo, inoltre nel livello dati stesso, i mapper sarebbero necessari per impostare SP, linq ecc. (Fondamentalmente sql che va al database).

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

Quindi, la strategia di base è:

  • Chiamate del livello di servizio
  • Trasforma la chiamata del livello di servizio al database utilizzando 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. Finisci ancora con un sacco di codice di mappatura boilerplate (specialmente con SP) o magico codice ORM che crea dinamicamente l'SQL parametrizzato per te.

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.