Incapsulamento per query complesse

7

Ho provato a porre questa domanda prima su StakOverflow in un modo più concreto, ma dopo essere stato indicato qui ho capito che dovrei riformularlo in termini più generali; tuttavia, puoi comunque consultare la domanda originale , se voglio più dettagli concreti sul mio caso d'uso specifico.

Assumi un'interfaccia di reporting relativamente complessa. Per semplicità, supponiamo di presentare i dati risultanti solo in forma tabellare, ma stiamo visualizzando un aggregato che non è mappato su oggetti di dominio esistenti. L'utente può selezionare date di inizio / fine da un calendario, può limitare il proprio report in base a criteri specifici e può ordinare i risultati per colonne specifiche; tutti questi sono opzionali. La loro richiesta viene elaborata da diversi livelli di codice, ma dopo l'elaborazione ci ritroviamo con una singola query SQL che contiene condizioni di filtro, direttive di ordinamento e istruzioni JOIN per aggregare dati da diverse tabelle.

Sul database, sto usando un ORM (NHibernate) che gestisce la persistenza e mi costringe a implementare entità rigorose / oggetti di dominio; questo è chiaro e funziona già bene.

Dall'altra parte (verso l'utente), ho la vista che presenta cose all'utente e un controller che interpreta i dati in / out. Questo è anche chiaro, ed è gestito correttamente.

La mia domanda è questa: cosa dovrebbe accadere tra questi livelli? Qual è l'approccio raccomandato per un corretto incapsulamento? Il mio approccio intuitivo è stato quello di consentire alla mia business logic (BL) di chiamare vari helper di query che gradualmente costruivano varie parti dell'oggetto query, a seconda dell'input dell'utente. Questo ha iniziato a sollevare bandiere rosse quando ho finito per dover gestire i JOIN in modo condizionale nel BL, che ovviamente è fuori uso.

Sono d'accordo con la risposta di Frédéric alla mia domanda precedente su StackOverflow : sposta tutto il codice che costruisce la query più in profondità, più vicino al livello di persistenza e mantiene libero il BL da qualsiasi conoscenza sul modello. Questo è certamente un approccio più pulito, e potrei facilmente definire un Data Transfer Object (DTO) per portare i risultati del database "verso l'alto", dal livello di persistenza verso il controller e, infine, al livello di vista. Tuttavia, l'utente può inserire molti input opzionali; ciò significa che avrò bisogno anche di una classe ausiliaria che trasporta i dati al contrario, dal livello controller, attraverso il livello di servizio in cui vivono i BL, e fino in fondo agli helper della query stessi sul livello di persistenza. Non so come vengano chiamati, quindi chiamiamoli "QTO", per "Query Transfer Objects" ("query" nel senso di "query utente", non nel senso di SQL). Il livello di persistenza quindi interpreterà questi QTO in SQL, eseguirà l'SQL e convertirà il set di risultati in DTO che si ripristinano.

Il mio problema con questo approccio è che aggiungerei solo un paio di classi intermedie per il trasporto dei dati, ma la stessa gestione sarebbe la stessa (solo su un livello diverso). Avrei ancora bisogno di aggiungere tutti questi JOIN in modo condizionale, aggiungere criteri in modo condizionale, aggiungere condizionamenti di ordinamento in modo condizionale - solo ora dovrei fare lo sforzo extra di compilare i QTO in modo condizionale anche nel controller! E il futuro refactoring non sarebbe più semplice: qualsiasi cambiamento nella struttura del QTO interesserebbe sia il controller che i livelli del modello - ed è quasi garantito che eventuali modifiche rilevanti alla struttura del database porterebbero modifiche sia a QTO che a DTOs bene.

Nella tua esperienza, qual è un buon modo di progettarlo? Sono troppo ingegnerizzato? In che modo vengono trattati i miei problemi di complessità nel design che stai utilizzando / suggerendo? O ci sono dei benefici che non vedo che controbilanciano la complessità aggiunta?

    
posta Bogdan Stăncescu 14.03.2016 - 14:52
fonte

4 risposte

0

Versione breve : non sembra esistere un modello di progettazione completo e standardizzato e supporto per questo, almeno non in NHibernate.

Versione lunga

Ho passato le ultime due settimane a indagare su questo a intermittenza. Dopo molte ricerche, ho finalmente trovato una tecnica applicabile per la tecnologia che sto usando (e per la mia preferenza personale, ovvero NHibernate usando QueryOver) su blog di Andrew Whitaker : QueryOver Series - Part 6: Query Building Techniques .

Mi rendo conto che ciò che Andrew sta descrivendo è più una tecnica, piuttosto che un modello di progettazione. Tuttavia, il fatto che qualcuno abbia sentito il bisogno di affrontare in profondità questo specifico argomento in un tutorial così breve (ha solo nove parti, una delle quali si rivolge a questo) dimostra che questo è un problema sostanziale, è relativamente frequente, e in effetti lo fa merita di essere affrontato correttamente sia come pattern che come tecnica (forse nelle librerie sottostanti).

La soluzione di Andrew è abbastanza elegante, fino a quando la sezione " Alias potrebbe dover essere distribuita ". Questo è in particolare il problema che ho riscontrato anche io e questo mi ha spinto a postare queste domande qui (quella su StackOverflow e questa).

Anche se la soluzione di Andrew nella sezione " Alias potrebbe dover essere distribuita " sicuramente funziona, penso che sia piuttosto inelegante, e rompe l'isolamento in grande tempo (ed è proprio quello che volevo indirizzare).

Confronta la sua soluzione intermediario (prima dell'ultima sezione)

IList<ProductDTO> products = session.QueryOver<Product>()
    .ApplyColorFilter(colors)
    .ApplyRatingFilter(minimumRating)
    .SelectList(list => list
        .Select(pr => pr.Name).WithAlias(() => result.Name)
        .Select(pr => pr.Color).WithAlias(() => result.Color)
        .Select(pr => pr.ListPrice).WithAlias(() => result.ListPrice))
    .TransformUsing(Transformers.AliasToBean<ProductDTO>())
    .List<ProductDTO>();

return products;

alla sua soluzione finale (descritta in quella sezione)

ProductReview reviewAlias = null;

var query = session.QueryOver<Product>()
    .JoinAlias(pr => pr.Reviews, () => reviewAlias);

FilterQueryByRating(query, () => reviewAlias);

query
    .SelectList(list => list
        .SelectGroup(pr => pr.Id)
        .SelectMax(() => reviewAlias.Rating))
    .List<object[]>();

Mentre l'approccio iniziale è davvero limitato e deve essere affrontato (spiegazioni nel suo post), è ovviamente molto più elegante, leggibile, manutenibile e molto più isolato della sua soluzione finale.

Dal momento che non sembra esserci una soluzione migliore a questo problema in NHibernate, che è relativamente usato e viene attivamente sviluppato, posso solo concludere che non esiste una soluzione pulita a questo punto. Questo mi sorprende, visto che stiamo parlando di query generate dinamicamente, qualcosa con cui le persone hanno lottato da almeno un decennio.

Se dovessero comparire risposte migliori, aggiornerò volentieri la risposta accettata a quel punto (dopotutto sarebbe utile anche a me - e sarebbe sicuramente meglio di questa risposta).

    
risposta data 23.05.2017 - 14:40
fonte
1

Sei saggio diffidare delle cose sovra-ingegneristiche. Tuttavia, sembra che il contesto problematico delle "query complesse" potrebbe essere una buona soluzione per investire nel modello di interprete , in Per aiutarti ad astrarre (nel tuo BL) l'uso del tuo livello di persistenza, per scopi di interrogazione?

Quindi la domanda diventa "un interprete per quale linguaggio di interrogazione?" Quindi, tornerei a "fissare" la forma dei vari JOIN che avresti finito per scrivere nel BL con il primo approccio che hai menzionato, e proverei a escogitare un semplice DSL che potrebbe essere il linguaggio di query sorgente su il tuo dominio per i modelli di query più frequenti / comuni.

In alternativa (visto che hai menzionato NHibernate, con una "N") c'è anche la possibilità di implementare il tuo QueryProvider se trovi la sintassi di comprensione di Linq abbastanza potente da poter generare quelle query.

    
risposta data 14.03.2016 - 16:16
fonte
1

Ecco un possibile progetto:

Lo compongo in 2 aree:

  • Mappatura
  • Costruzione di query

La mappatura gestiva BL / DTO per la mappatura di oggetti DB, la costruzione di relazioni, ecc., sia in entrata che in uscita. Nota che per IN dovrai mappare e costruire, e per OUT (ritorno) avrai solo bisogno di mappatura poiché nessuna query sarà costruita durante il round trip /

La creazione di query gestirà gli oggetti mappati in precedenza per creare una query SQL. Una query sql è composta da parti in modo da poterlo suddividere anche:

  • Costruisci SELECT
  • Build FROM
  • Crea DOVE
  • Crea GROUP BY
  • Crea ORDINA DA
  • ecc ...

Si potrebbe appiattire tutto in un livello, ma penso che si voglia un'area di astrazione tra gli oggetti di business (DTO) e il database (livello di mappatura).

Quindi è possibile testare la costruzione della query separatamente dal livello di mappatura. Inoltre, se c'è una modifica di mappatura, questa dovrebbe essere gestita nel livello di mappatura e quindi il livello di costruzione dovrebbe gestire senza problemi la modifica poiché sta solo creando la query dagli oggetti mappati in precedenza.

Ad esempio, supponiamo di aver aggiunto un'altra tabella di join nel database. Il livello di mappatura dovrebbe gestire ciò che gestirà automaticamente un'altra condizione di join. La costruzione effettiva del join non dovrebbe essere diversa, ma invece di 1 condizione di join, ora ho 2.

For each (Condition in JoinConditions)....

Questo rende anche ogni livello testabile e ogni strato ha una singola responsabilità.

Vorrei avvertire che ne ho visto alcuni e diventano intrinsecamente complessi. Inizia con alcune operazioni comuni e inizia con una semplice logica prima di crearne una più complessa / astratta.

    
risposta data 14.03.2016 - 16:47
fonte
0

Serve fondamentalmente un meccanismo di creazione di query o un mapper dal tuo UI Layer al DB.

Dato che stai già utilizzando un ottimo ORM, dovrai solo costruire una parte di mappatura, NHibernate si prenderà cura della creazione di query.

Per la parte di mappatura, è necessario ottenere tutti i parametri specifici dell'interfaccia utente e inserire la logica del generatore di query, infine restituire i risultati. In qualsiasi modifica all'interfaccia utente è necessario eseguire questa operazione da zero.

Per la parte relativa alle query, ti consiglio di leggere questo articolo su NHibernate QueryOver Tecniche di costruzione di query

Modifica a causa del commento:

Il codice specifico dell'interfaccia utente dovrebbe essere sul livello dell'interfaccia utente, il codice specifico per i dati dovrebbe essere su un repository o un livello DAL. Ma come potrebbero comunicare questi due strati? Questa è la parte difficile.

Uso spesso un livello aggiuntivo non solo per le mie entità (DB) e le interfacce, ma anche modelli solo per scopi di comunicazione, che saranno comuni per tutti i miei progetti (UI, Business, Servizio ecc.). Ma considera la possibilità di mantenere solo i tuoi progetti (UI, Business, Service ecc.) In quei particolari progetti.

Quindi, crea un modello comune nella mappatura dell'interfaccia fpr delle entità fpr in DAL / Repository, inseriscilo nel tuo livello UI (l'ho chiamato mappatura prima), poi invia al livello DAL / Repository. Con queste informazioni è possibile creare la query, eseguire e inviare i risultati al livello dell'interfaccia utente.

Spero che questo approccio ti aiuti.

    
risposta data 14.03.2016 - 17:09
fonte

Leggi altre domande sui tag