NHibernate e la preview/update dei dati

mercoledì 24 giugno 2009 - 08.02

alfred1234 Profilo | Newbie

Ragazzi una domanda. Tempo fa mi sono imbattuto in un progetto creato utilizzando la tecnologia ORM e in particolare con NHibernate x C#. Durante la stesura del progetto però vedevo che il caricamento delle pagine era mostruosamente grande in quanto Nhibernate caricava gli oggetti comprese tutte le relazioni e man mano che il database si riempiva andava sempre peggio. Per esempio se avevo una classe di nome Mammifero e due altre classi di nome Delfino e Balena e queste a loro volta avevano delle sotto classi, se io dovevo far vedere in un gridview l'elenco dei mammiferi accadeva che + i mammiferi aumentavano e + era il tempo di caricamento dell'oggetto dell'istanza Mammifero, in quanto si caricava anche i dati dei delfini e delle balene (ciò che a me non serviva, è vero che è sempre comodo averli, però per una preview non occorrono). Per risolvere il tempo di caricamento sono passato alle query semplici e quindi ai DataSet. (da 30 secondi si è passati a poco meno di 2 secondi). Un'altra cosiderazione. Mi sono trovato anche, in questo progetto, a dover eseguire una serie di passi. Per esempio c'erano una serie di form da seguire in successione e in base a ciò che si selezionava in un form faceva cambiare di stato un Mammifero. Con Nhibernate è vero che cambiando una proprietà dell'oggetto e facendo il SaveorUpdate ti faceva lui direttamente la query di UPDATE nel database, ma ciò a volte per grossi progetti non va bene in quanto non hai il controllo totale su quello che sta facendo perchè è demandato tutto al traduttore di Nhibernate. Con questo voglio dire che magari alcuni campi di Mammifero non dovevano essere modificati (magari in qualche punto sconosciuto del codice lo fa però non ne siamo a conoscenza) e se invece si utilizza una QUERY del tipo UPDATE MAMMIFERO SET CAMPO1='A' WHERE ID=1 tu sai sicuramente che verrà modificato il campo1 e non il campo2.

Voi che ne pensate? E' utile utilizzare un ORM(in questo caso Nhibernate) per la preview dei dati?e per l'update?
Grazie mille

supix Profilo | Newbie

Ciao Alfred.

Per quanto riguarda il problema delle prestazioni, usare un ORM in generale migliora le cose.

Il problema al quale credo tu ti riferisca dipende dal fatto che, nel caricare un oggetto, finisci per portarti dietro anche tutti gli oggetti ad esso collegati attraverso il grafo delle associazioni. Per esempio, caricando un cliente, ti porti dietro tutte le sue fatture, quindi tutti gli articoli collegati, e forse gli ordini, i listini, i pagamenti, ecc. ecc. Questo problema si risolve attivando una funzionalità di cui ogni buon ORM (come NHibernate) è dotato. Si chiama "Lazy Loading". Con riferimento all'esempio precedente, funziona così: se carichi un cliente, al momento il cui l'ORM "incontra" la collezione delle fatture, non la carica, ma la sostituisce con un proxy, cioè con un oggetto che ha la stessa interfaccia della collezione (metodo count, metodo add, funzione d'accesso agli elementi, ecc.) ma è in realtà un suo surrogato, e quindi tu neanche te ne accorgi. Solo al momento in cui avrai effettivamente bisogno di accedere ad una proprietà della collezione (e solo se ne avrai bisogno), il proxy (che contiene in sé tutto il codice utile per scatenare le query necessarie al caricamento di questi oggetti) scatenerà le Select sul DB. Attivando il lazy loading dunque (in NHibernate 2.0+ è attivato di default) hai la possibilità di caricare quindi solo gli oggetti di cui hai effettivamente bisogno e nient'altro.

Relativamente al controllo di cosa aggiorni in una update, ancora una volta, un buon ORM (come, ehm..., NHibernate) in certe condizioni è in grado di capire quali colonne di un DB aggiornare su una chiamata a Update(), evitando inutili trasferimenti di dati da/verso il DBMS.

A parte questi due aspetti un ORM offre molto di più di questo. Provo a fare un veloce riassunto.

-) Caching dei dati. Quando carichi un oggetto, e durante la vita del programma provi a ricaricarlo, puoi chiedere all'ORM di non andare nuovamente sul DB, ma di darti la versione che è in cache. Naturalmente questo comportamento è configurabile ed hai la massima libertà nel decidere quali oggetti devono andare in cache, nonché la politica di gestione la cache. Immagina quante query puoi risparmiare grazie a questo comportamento.

-) Gestione della concorrenza. Se due utenti di un programma caricano uno stesso cliente in una finestra, uno modifica l'indirizzo, l'altro il numero di telefono, e poi entrambi salvano, cosa fa il programma? Se fa "vincere" il secondo salvataggio, la modifica all'indirizzo andrà persa, e (cosa ancor più grave) nessuno se ne accorgerà. Se impedisce al secondo di caricare e/o modificare e/o salvare finché il primo non abbia salvato, magari il secondo un po' se la prende (mentre il primo è a prenderer il caffè con il cliente visualizzato sulla sua finestra). E se la prende ancora di più quando il primo (tornando dal caffè) si accorge che la sua applicazione è andata in crash ed il cliente è rimasto bloccato. Con un buon ORM (NHibernate?) il problema della concorrenza può essere gestito nativamente, poiché l'ORM può essere configurato per rilevare i conflitti e ti permette di farvi fronte (per es. il secondo riceve un messaggio:- "Guarda che mentre facevi le tue modifiche, qualcun altro ha modificato il tuo record. Vuoi vedere la versione modificata e provvedere? Vuoi salvare prepotentemente la tua versione? Vuoi abbandonare le modifiche?").

-) Portabilità del DBMS. Non è bello scrivere un'applicazione che può girare su Oracle, ma anche su MySql, ma anche su SqlServer, ma anche su Firebird, ma anche su Sqlite, ma anche su Interbase...?
Ok, ok... Molti prima o poi arrivano (arriviamo) alla conclusione che è sempre meglio scrivere solo query standard, evitare stored proc e trigger, e le varie "cose strane" che ogni DBMS ti permette di fare.
Ma quelle maledette virgolette perché non funzionano su MySql?!?@??
E soprattutto... chi ha detto "data"???
Un buon ORM (...) ti permette davvero di cambiare una stringa di connessione e (senza neanche ricompilare) di rivolgerti ad un DBMS differente. E quando un cliente ti chiede "Che DBMS usi?", come è bello poter rispondere tranquillo:- "Che DBMS vuoi?".

-) Navigazione del grafo delle dipendenze. Ho un cliente. Cambio il suo indirizzo di spedizione, elimino l'ultimo articolo dal suo ultimo preventivo ed imposto la sua data di accettazione alla data odierna. Poi copio il preventivo in un nuovo DDT ed in una nuova fattura. Poi scrivo SaveOrUpdate(Cliente) e l'ORM (se è buono, come NHibernate) mi fa tutte le Update, le Insert, le Delete da solo, tranquillo e senza che io neanche me ne accorga. Sempre che non abbia deciso di abilitare il sistema di logging (perché sono un po' malfidato), ed allora me lo vado a leggere nel file se ho tempo e voglia. Poi dopo scrivo: LoadCliente(25) e trovo una bella istanza di cliente del quale, senza fare altro, posso andare subito a vedere le fatture, e poi gli articoli, e poi i listini, e poi gli ultimi acquisti. Ed l'ORM spruzza Select di qua e di là senza che io sappia niente di niente (ma il log sì, invece).

-) Unit of Work. Nel cliente che ho aggiornato prima, mica crederai che ogni query è inviata separatamente dalle altre? Ebbene no. Tutte le modifiche vengono impacchettate ed eseguite in un solo "giro di rete", alla fine di tutte le modifiche. Con buona pace dei tempi di risposta dell'applicazione.

-) Controllo nell'accesso ai dati. E come faccio a garantire che, neanche in presenza di un errore, neanche se volessi, potrei fare una modifica in tabella su un preventivo già accettato? Beh, potrei dire che un PreventivoAccettato è un discendente (eredita da) di un Preventivo. E che PreventivoAccettato è una classe "read-only" per il database. E l'ORM, incorruttibile, vigila per me.

E qui mi fermo. E non ti parlo di modularità, di trasparenza dei dati, di persistence agnosticism...

E ti dico anche che un ORM è un po' una rivoluzione nel modo di pensare all'architettura un'applicazione. Penso agli oggetti, e non alle tabelle. Penso alle proprietà, e non alle colonne. Penso alle aggregazioni, e non alle chiavi esterne. Inizio un'applicazione dal diagramma delle classi, e non dal grafo delle relazioni. Vedo il DB come un pezzo di implementazione, e non come il modello. Poi dico: "Tu, piccolo oggetto. Vatti a salvare sul DB. E ci vediamo dopo. Forse.". Ed il buon ORM, prende l'oggetto, e tutto ciò che è collegato ad esso, e lo custodisce per me. Senza che io debba preoccuparmi di dove ho dimenticato la virgola in quella query e perché ho sbagliato il nome del campo in quell'altra query. E senza che mi arrabbi perché avrei tanta voglia di aggiungere il flag "Simpatico" al cliente... ma poi mi tocca andare a modificare tutte le query. Meglio di no.
Ed ho tanto tempo in più per pensare al mio mondo fatto solo di oggetti.

Le belle cose descritte semplicisticamente prima, sono per la verità meccanismi _molto_ complessi (anche in un buon ORM) e bisogna saperli gestire bene, per non rischiare di perdere il controllo. O per non rischiare che l'applicazione che prima impiega 2 secondi, dopo impiega misteriosamente 30 secondi.
Maa... questo non è forse un invito a nozze per un informatico?
Perché non ci siamo fermati alla programmazione in assembly che è semplice e più-veloce-non-si-può?
Perché le variabili? e poi gli if e i while? e poi le procedure? e poi gli oggetti? e poi le virtual machine? e poi gli ORB? e poi i web services? e poi i .NET framework?
E perché si sente parlare sempre più di questi strani oggetti di nome ORM tanto che la Microsoft sta cercando di integrarne uno perfino nel bellissimo linguaggio C#? (a dire il vero senza grande successo, e meno male).

Senza voler necessariamente mitizzare. Un ORM è più o meno una pezza (forse una bella pezza, ma pur sempre pezza) al peccato originale del "disadattamento di impedenza" tra il paradigma relazionale e la programmazione orientata agli oggetti. Più di uno dice "ricominciamo tutto daccapo", buttiamo via i R-DBMS, pensiamo alla persistenza a partire direttamente dalla programmazione orientata agli oggetti, ed avremo vita più semplice.

Non so se riuscirò a vederlo. E intanto mi diverto con gli ORM.

Direi che la soluzione al tuo problema non può essere altra che prendere un bel po' di coraggio e tuffarsi nello studio di cosa sia davvero un buon ORM (che ne dici di NHibernate?). Solo così potrai decidere consapevolmente se un ORM fa per te, oppure non puoi fare a meno di progettare daccapo il "CRUD" per ogni nuova applicazione che crei.

Consiglio: "NHibernate in Action" è un ottimo testo per imparare uno tra i tanti (!?!) ORM disponibili. Il primo capitolo, che espande ottimamente i temi ai quali ho accennato, dovrebbe essere illuminante per la tua scelta futura. Il web, poi, pullula di informazioni ed esperienze su NHibernate.

In bocca al lupo.

Marcello Esposito.

alfred1234 Profilo | Newbie

Grazie mille per la tua lunga ed esaustiva risposta... :D cmq una sola cosa..Supponiamo di avere un gridview che mi fa un riepilogo dei clienti e nelle colonne ci metto i mesi tipo così
Clienti | GENNAIO 2009 | FEBBRAIO 2009.....
naturalmente l'entità cliente è in relazione con le fatture e sotto la colonna Gennaio 2009 voglio fare la somma degli importi del cliente nel mese di gennaio.
Allora eseguendo una query normalissima farei tante belle sotto query del tipo
SELECT cl.Nome, cl.Cognome,
(Select SUM(ft.costo) FROM Fatture ft where ft.id_cliente=cl.id and mese=1 and Anno=2009) as costoGennaio,
(Select SUM(ft.costo) FROM Fatture ft where ft.id_cliente=cl.id and mese=2 and Anno=2009) as costoFebbraio
FROM Cliente cl

tempo di esecuzione di tale query veramente molto ma molto basso (su 300 000 record).
mentre se lo dovessi fare in Nhibernate farei così (penso che ci siano altri modi pure + efficienti)
Prendo la IList<Cliente>, la carico e la metto come dataSource del gridview. Nell'evento RowDataBound casto il dataItem e quindi ogni cliente avrà una sua bella IList<Fattura> e per ogn fattura calcolo poi la somma e la metto nella colonna giusta. Tutto questo da riga di codice.
Il tempo di esecuzione di ciò non è dei migliori ( se i clienti sono molti i cicli sono parecchi).
Credo che esistono altri metodi migliori a questo.

Altro problemino da non sottovalutare..un giorno il cliente che ti ha commissionato il lavoro ti chiede..ma le fatture non si calcolano così..ma per esempio ai costi devi togliere il 20% di iva (è na cazzata però è giusto per capire che occorre fare una modifica alla logica di calcolo delle fatture).
Bene con la query tu modificheresti la stored procedure...e passi solo la stored procedure (le pagine aspx no...) quindi il cliente non si accorgerà della modifica che tu stai facendo e non stai interrompendo il suo lavoro. Mentre con Nhibernate dovresti andare a cambiare il codice interno che sta nel rowDataBound (oppure nell'hbm) e ciò vuol dire passare la caretlla bin...con la relativa interruzione del lavoro del proprio cliente.

Con questo non voglio dire che NHibernate fa veramente schifo anzi, io ho intenzione di approfondirlo perchè credo che sia un'ottimo strumento. quello che voglio dire è che magari non è adatto a tutte le soluzione WEB <-> DataBase.
Tu che ne pensi?

Grazie ancora per la risposta di prima :)

supix Profilo | Newbie

Ciao Alfred.

La query che indichi puoi farla in NHibernate più o meno così:

IQuery c1 = session.CreateQuery(
@"select ft.Cliente.Nome, ft.Cliente.Cognome, ft.mese, ft.Anno, Sum(ft.costo)
from Fatture ft
group by ft.Cliente.Nome, ft.Cliente.Cognome, ft.mese, ft.Anno");

Dovrebbe andare veloce almeno come la tua, se non di più (poiché viene mappata su una query di raggruppamento).

Per il deploy, sono d'accordo con te: c'è bisogno di un minimo di down-time. Con uno script di 5 o 6 righe da far girare di notte, ad uffici chiusi, direi non più di 30 secondi.
Se il cliente si lamenta digli che io, che ho il CC su Fineco, una volta ogni due mesi circa resto per un intero week-end impossibilitato
a giocare in borsa per "aggiornamenti alle procedure".
Evita di dirgli che non gioco in borsa.

Al di là di questo, le nostre 2 soluzioni si distinguono per qualcosa di molto più profondo.
Nella tua architettura, i dati tirati fuori da una Select arrivano ad alimentare direttamente, quasi senza intermediazioni, il controllo grafico GridView.
La base dati, la logica e l'interfaccia grafica sono molto dipendenti tra loro. Pur se strati logicamente separati, nel tuo codice sono fisicamente fusi l'uno nell'altro.

Vantaggio: è un approccio semplice e veloce. Non per niente gli ambienti che lo consentono vengono annoverati come "Ambienti RAD (Rapid Application Development)". In circa 10 minuti metto su un'applicazione con una base dati "live".
Svantaggio: gli ambienti che consentono questo approccio vengono annoverati (anche) come ambienti di "rapid prototyping". E io un prototipo non lo metterei nella mia azienda a tenere traccia dei soldi che i miei clienti mi devono.

L'approccio del "tutto-in-uno" produce scarsa modularità, cioè scarsa indipendenza tra i layer logici elencati prima, cioè una modifica su un layer può avere effetti collaterali che si propagano attraverso gli altri layer. E purtroppo in un sistema complesso le modifiche/estensioni al codice sono all'ordine del giorno.
La inclinazione di un sistema software ad essere modificato/esteso può decretarne il successo (o la morte).

Attenzione però. Un "sistema complesso", ho detto. Il tuo approccio è certamente vantaggioso da adottare per piccole applicazioni, che resteranno sempre piccole (non vado con la Ferrari a fare la spesa sotto casa).
Quando il sistema diventa grande, la complessità però aumenta inesorabilmente. Per dominarla bisogna spaccare il mostro in piccoli pezzi ("divide et impera") e gestirne i pezzetti separatamente.

Un approccio del genere favorisce:

-) la riduzione/eliminazione di effetti collaterali "a lungo raggio";
-) la possibilità di stesura del codice in team (con esaltazione delle specifiche competenze individuali);
-) la manutenibilità futura del codice;
-) l'estensibilità del software;
-) la comprensibilità del software;
-) il raggiungimento di alti livelli di correttezza;
-) la modularità;
-) ...

In tre parole: la "qualità del software".

NHibernate, in un certo senso, ti costringe ad intraprendere un approccio meglio strutturato alla stesura del codice: la ben nota architettura "three-tier" è descritta proprio nell'illuminante primo capitolo del libro "NHibernate in Action". Insieme ad un sacco di buoni motivi per cui le architetture con meno tier non mi fanno dormire tranquillo.

Buon lavoro.

Marcello Esposito.

alfred1234 Profilo | Newbie

Grazie Marcello per le tue risposte complete ed esaustive.
Io mi trovo davanti a progetti aventi complessità medio/alta quindi la suddivisione come dicevi tu in + livelli è l'apporccio ideale. Il libro che mi indicavi lo sto iniziando a leggere x approfondire meglio la progettazione di futuri progetti.

X Ora grazie infinite a presto
Partecipa anche tu! Registrati!
Hai bisogno di aiuto ?
Perchè non ti registri subito?

Dopo esserti registrato potrai chiedere
aiuto sul nostro Forum oppure aiutare gli altri

Consulta le Stanze disponibili.

Registrati ora !
Copyright © dotNetHell.it 2002-2024
Running on Windows Server 2008 R2 Standard, SQL Server 2012 & ASP.NET 3.5