Introduzione
Eccoci al terzo
(ultimo?) post dedicato ad NHibernate. Nel primo abbiamo fatto un'overview delle
classi da persistere, alcune delle quali legate da una relazione uno-a-molti,
nel secondo abbiamo visto come creare files di mapping corretti che possano fornire all'engine di persistenza
di NHibernate tutte le informazioni su ciascuna classe e come le
classe si relazionano fra loro.
Fino ad oggi, non abbiamo scritto una sola maledetta linea di codice. In
realtà, almeno per quello che riguarda me, scrivere codice C# che utilizzi
NHibernate per caricare/scrivere/cancellare i nostri oggetti è davvero una
banalità, perchè il fulcro, tutta la logica e molta della complessità è
inserita nei files di mapping. Fatti quelli, la stragrande parte del lavoro è
fatta. Nel post di oggi vediamo (finalmente) come scrivere un semplice
dataprovider che esponga un'interfaccia (3 metodi) per fare tutto quello che ci
serve.
La classe DataProvider<T>
La prima cosa che viene
in mente è: "Ok, faccio una classe con un metodo
LoadHockeyPlayer che accetta un ID e ritorna un'istanza di
HockeyPlayer". Ricordiamoci innanzitutto di una cosa: l'ID di cui
parlo è lo stesso ID che abbiamo specificato nel file di mapping. Rivediamolo un
attimo:
<id name='ID' column='ID' type='Int32' length='4' unsaved-value="0">
<generator class='identity' />
</id>
In questo caso, l'ID è il campo identity della tabella, ma avremmo potuto
utilizzare anche il campo Name, ad esempio. Per una mia
scelta, utilizzeremo un campo numero: l'identity. Quindi, potremmo pensare di
implementare un metodo LoadPlayer più o meno così:
public HockeyPlayer LoadPlayer(int ID)
{
HockeyPlayer ret;
// Dico ad NHibernate di caricare l'oggetto con ID = ID
return(ret);
}
Il metodo accetta un qualsiasi int, e ritorna tramite
NHibernate l'istanza corrispondente a questo ID. Se le classi coinvolte nel
nostro domain model fossero 50, dovremmo scrivere 50 metodi diversi, ognuno dei
quali con un nome diverso (LoadFault,
LoadSquad, LoadStadium, tanto per citarne
qualcuna a caso). La struttura del metodo sarà sempre la stessa: cambierà solo
il tipo ritornato. Duplicazione di codice a tutto
andare, senza pensare poi ai metodi per il salvataggio e la
cancellazione. Insomma, tutto fuorchè elegante, comodo e manutenibile.
In realtà con l'avvento di .NET 2.0 possiamo risolvere brillantemente la
questione con i generics. Tutti gli oggetti del mio domain model ereditano da
una classe astratta Entity. Tutte, comprese quindi l'oggetto
HockeyPlayer, Fault e così via. Vi state
chiedendo cosa c'entra questo? E' presto detto. Così facendo possiamo creare una
sola classe DataProvider generica che espone i 3 metodi Load, Save e Delete e
che verrà istanziata di volta in volta a seconda delle necessità. Vediamo il
codice.
public class DataProvider<T> where T : AbstractEntity
{
public static void Save(T oggetto) { }
public static T Load(int ID) { }
public static void Delete(T oggetto) { }
}
I tre metodi sono statici. Impongo che il tipo T derivi da
AbstractEntity, in modo tale che forziamo il data provider a
lavorare solo con i tipi appartenenti al nostro domain model. Il resto credo che
sia piuttosto chiaro. Il metodo Save accetta come parametro
l'oggetto da salvare, di tipo T. Il metodo Load lo restituisce,
dato in input l'ID da caricare. Il metodo Delete infine
cancella un oggetto.
Come si utilizza? Nulla di particolare: sulla Windows Forms della mia sample
application ho inserito un Button per il caricamento, che non fa nient'altro
che:
private void btnLoad_Click(object sender, EventArgs e)
{
int id = 6;
player = DataProvider<HockeyPlayer>.Load(id);
SetDataBindings();
}
A scopo di test, carico sempre l'istanza con ID = 6. Brutto da vedere, ne
sono cosciente. Stessa cosa per gli altri due
metodi:
private void btnDelete_Click(object sender, EventArgs e)
{
DataProvider<HockeyPlayer>.Delete(player);
}
private void btnSave_Click(object sender, EventArgs e)
{
DataProvider<HockeyPlayer>.Save(player);
}
Ditemi che è complicato, e vi ammazzo!
Sì, va bene, ma come diavolo fai?
Se vi state chiedendo
questo, continuate a leggere. Fino ad adesso, non abbiamo visto ancora nulla di
NHibernate, ho solo descritto a grandi linee come funziona il mio data provider.
Cosa implementa ciascuno dei metodi coinvolti, cioè il Load, il
Save ed il Delete?
La prima cosa da sapere è che NHibernate ha bisogno di una configurazione, in
buona parte letta dell'app.config associato all'eseguibile. Considerato che
trovate un miliardo di siti che vi fanno vedere questa cosa, preferisco parlare
d'altro. La mia classe DataProvider contiene tre field privati
che mantengono tale configurazione, più una variabile bool che
indica se il data provider è già stato inizializzato oppure no. Tali field
sono i seguenti:
static bool init = false;
static Configuration nhibernateConfiguration;
static ISessionFactory factory;
static ISession session;
All'ingresso di ciascun dei due metodi, controllo init: se
vale false, significa che è la prima volta che utilizzo
il data provider, e quindi ne devo settare la configurazione:
if (!init) initialize();
Il metodo initialize è un altro metodo statico privato che
inizializza la classe DataProvider:
#region Initialize
private static void initialize()
{
/* Inizializza, se necessario, l'engine
* di NHibernate */
nhibernateConfiguration = new Configuration();
/* Qui devo aggiungere l'assembly che contiene i
* files di mapping di NHibernate */
nhibernateConfiguration.AddAssembly("DataBindingSample");
nhibernateConfiguration.AddAssembly("HockeyObject");
factory = nhibernateConfiguration.BuildSessionFactory();
init = true;
}
#endregion
La parte interessante del metodo initialize a parer
mio è quella riguardante l'aggiunta dell'assembly (o degli assembly) che
contengono i files di mapping. In altre parole, sull'oggetto
nhibernateConfiguration chiamo il metodo
AddAssembly per poter dare all'engine la possibilità di
caricarsi tutti i files di mappings che abbiamo definito. Nota importante: quando comincerete a
lavorare con NHibernate, probabilmente incontrerete sulla vostra strada parecchi
errori, di svariati tipi. Se non vi succederà, significa che tutta la community
di NHibernate sparsa per il Web (compreso il mio piccolo contributo in questo
frangente) ha spiegato bene le cose: ve lo auguro di cuore. Nel caso
contrario, sappiate una cosa: alcuni tipi di errori li
troverete proprio sulla chiamata a AddAssembly, quando il files
di mapping ha qualcosa che non va a livello sintattico, oppure quando avete
scritto nomi di proprietà non valide che non appartengono allo schema di
NHiernate. Questi errori sono i più gravi, perchè
significa che il file di mapping non è valido e di conseguenza
non viene neppure caricato. Altri errori invece dipendono dagli oggetti, e
quindi appariranno solo quando necessario: se salvo un oggetto la cui proprietà
Denominazione è vuota, ma sul database il campo è marcato come
not-null, allora NHibernate ci avviserà solo durante il salvataggio
stesso.
Una volta che la classe Data Provider è stata inizializzata
correttamente, possiamo vedere l'implementazione di ciascuno dei 3 metodi di cui
finora abbiamo solo parlato. Partiamo dal metodo Load:
public static T Load(int ID)
{
if (!init) initialize();
session = factory.OpenSession();
T ret =(T) session.Load(typeof(T), ID);
session.Close();
return(ret);
}
Se init è true, apro una sessione,
istanzio un oggetto di tipo T, lo carico, chiudo la sessione e ritorno
l'oggetto. Lineare, semplice ed efficace. Cosa volere di più? Passiamo al metodo
Save:
public static void Save(T oggetto)
{
if (!init) initialize();
session = factory.OpenSession();
ITransaction transaction = session.BeginTransaction();
// Comunichiamo ad NHibernate che questo oggetto deve essere salvato
session.SaveOrUpdate(oggetto);
transaction.Commit();
// Commit dei cambiamenti al database e chiusura della ISession
session.Close();
}
Infine, il metodo Delete che chiude le danze:
public static void Delete(T oggetto)
{
if (!init) initialize();
session = factory.OpenSession();
// Comunichiamo ad NHibernate che questo oggetto deve essere salvato
session.Delete(oggetto);
// Commit dei cambiamenti al database e chiusura della ISession
session.Close();
}
Download della sample application
Il codice
aggiornato di questa piccola applicazione è scaricabile qui!