Curiosity killed the cat.
In viaggio verso Predappio per il workshop UGIdotNet il capo butta li una frase del tipo: Mi chiedo come mai i database ad oggetti non prendano piede. (citazione molto vaga ma la memoria alla mia età ormai fa acqua…).
Quindi… è domenica mattina presto, l’ufficio è aperto, c’è una giornata meravigliosa e la dolce metà è ancora a pelle d’orso nel letto… quindi quale momento migliore per soddisfare un po’ di sana curiosità?
db4o
E’ da anni che questo nome mi gira in testa, direi sin dall’epoca in cui per la prima volta ho sentito parlare di Tamino…
Che cosa è: db4o è un database ad oggetti, dove il paradigma relazionale scompare per essere completamente sostituito dal concetto di domain model anche nel “db”; La definizione è certamente molto casareccia e su Wikipedia ne trovate una molto più seria ;-)
Prima di dare uno sguardo a che cosa fa e come lo fa ci sono le domande, come ogni cosa che vuole rivoluzionare il modello a cui siamo fortemente abituati. In questo caso le prime cose che mi sono chiesto sono:
- Prestazioni?
- Scalabilità orizzontale?
- Replica?
- Variazioni dello schema?
- Indicizzazione e ricerca?
- Constraint sui dati?
- Gestione della concorrenza?
- Transazioni?
- Amministrazione?
n.d.r.: Direi non da poco… pienamente supportato sul Compact Framework 3.5.
Quindi… essenziale per partire è la sola class library di db4o; db4o è OpenSource e rilasciato con licenza GPL, c’è anche una versione commerciale con supporto etc, etc.. ci sono poi i tool di amministrazione in 2 versioni:
- commerciale con integrazione nell’ambiente di sviluppo;
- free e stand alone, completamente scritto in java. Una chiavica… le cose che non vanno al primo colpo a me personalmente fanno roteare i ma*oni al volo :-)
- Db4objects.Db4o.dll;
- Db4objects.Db4o.Linq.dll (e qui slurp…);
“Person” è una classe tradizionalissima, completamente POCO, senza nessun requisito di sorta, proprio nessuno. Per uso avanzato e per sfruttare tutte le caratteristiche di db4o in realtà POCO si perde un po’ per strada, ma non lo vedo come un problema. Db4oFactory è l’entry point per accedere ad un database di db4o, nello specifico OpenFile se non trova il file lo crea. Le modalità di accesso sono essenzialmente 2:var person = new Person(); person.FirstName = "Mauro"; person.LastName = "Servienti"; person.BornDate = new DateTime( 1973, 1, 10 ); Console.WriteLine( person ); using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { db.Store( person ); var query = db.Cast<Person>() .Where( p => p.FirstName.StartsWith( "M" ) );foreach( var p in query ) { Console.WriteLine( p ); }
}
- Single User, quella dell’esempio, che non è realmente single user ma va poco oltre;
- Client-Server: simile al tradizionale accesso che siamo abituati a fare ad un rdbms in rete;
- Il più evidente: pieno supporto per Linq e per gli Expression tree: quell’extension method Cast
(pessima scelta di naming) infatti non è il Cast di IEnumerable ma bensì l’entry point per dire al motore di costruzione delle query quale è la “tabella master” su cui volete operare, nei miei esempi mi sono già fatto un extension method decisamente più parlante:
static class ObjectContainerExtensions { public static IDb4oLinqQueryPossiamo quindi fare un paragone molto spannometrico e dire che l’istanza di IObjectContainer ritornata dalla OpenFile() è paragonabile al concetto di DataContext di Linq2Sql/EF ed in effetti ogni IObjectContainer rappresenta in tutto e per tutto una Unit of Work con, secondo concetto importante:From ( this IObjectContainer db ) { return db.Cast (); } }
- Identity Map: un IObjectContainer implementa quindi i pattern UnitOfWork e Identity Map;
Questo ci porta a introdurre le modalità di query supportate da db4o:
QBE: QueryByExample
Una primissima, e direi per la nostra mentalità inusuale, modalità di query è la QueryByExample:
In soldoni db4o ci permette di passargli un “template” dell’oggetto che stiamo cercando e se valorizziamo alcune proprietà dell’oggetto questi valori verranno usati come filtro per la ricerca, se la proprietà non sono valorizzare, o sono valorizzate al valore di default per il tipo della proprietà, allora db4o le considererà come “wild card”.using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { IObjectSet result = db.QueryByExample( new Person() { FirstName = "Mauro" } ); foreach( Object obj in result ) { Console.WriteLine( obj ); } }
Native Query (NQ)
Un’altra modalità di query è la Native Query, quella nativa da quel che ho capito… Il concetto di Native Query per noi dotnettinani è di una semplicità disarmante:
altro non facciamo che passare un Predicateusing( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { var result = db.Query<Person>( p => p.FirstName == "Mauro" ); foreach( Object obj in result ) { Console.WriteLine( obj ); } }
SODA: Simple Object Database Access
Tutto diventa gasato… :-) la documentazione dice che SODA è l’API interna del motore di query di db4o, è presente per 2 motivi:
- Retrocompatibilità;
- Costruzione dinamica delle query: problema direi superato con gli ExpressionTree, per i linguaggi che li supportano;
Chiediamo all’engine di creare una nuova query, impostiamo un Constrain sul tipo che stiamo cercando, quindi gli diciamo cosa stiamo cercando e con quale valore, infine eseguiamo la query. La nota interessante qui è la sintassi usata per riferirsi alla proprietà FirstName: quella strana sintassi è dovuta al fatto che nella mia classe Person sto utilizzando le Auto Implemented Property di C# 3.0, se avessi avuto una proprietà “tradizionale” implementata quindi con il backing field li avrei dovuto specificare il nome del backing field. Questo ci fa scoprire un interessante internal su come db4o memorizzi gli oggetti.using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { var query = db.Query(); query.Constrain( typeof( Person ) ); query.Descend( "k__BackingField" ) .Constrain( "Mauro" ); var result = query.Execute(); foreach( Object obj in result ) { Console.WriteLine( obj ); } }
Complichiamoci un attimo la vita, altrimenti che gusto c’è ;-) questa è la classe Person nel suo insieme:
Ci sono alcune cose interessanti:sealed class Person { public String FirstName { get; set; } public String LastName { get; set; } public DateTime BornDate { get; set; } public Int32 Age { get { return ( ( Int32 )( DateTime.Now.Date - this.BornDate ).TotalDays / 365 ); } } public override string ToString() { return String.Format( "{0} {1}, {2}", this.FirstName, this.LastName, this.Age ); } }
- è “sealed”;
- Non ha nessun membro virtual, e non potrebbe è sealed ;-);
- Non è pubblica;
- E’ effettivamente POCO, non c’è proprio nulla che ci faccia pensare che abbia qualcosa a che fare con un db;
Notate le chiamate un po’ barbine al metodo Store()? L’output di questo snippet è curioso, in realtà se ci pensiamo è più che giusto, ma al primo colpo lascia un po’ così:var person = new Person(); person.FirstName = "Mauro"; person.LastName = "Servienti"; person.BornDate = new DateTime( 1973, 1, 10 ); using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { db.Store( person ); db.Store( person );
Console.WriteLine( "Double Store, Count: {0}", db.From<Person>().Count() ); } using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { db.Store( person );
Console.WriteLine( "Single Store, Count: {0}", db.From<Person>().Count() ); } using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { Console.WriteLine( "Count: {0}", db.From<Person>().Count() ); var query = db.From<Person>() .Where( p => p.FirstName.StartsWith( "M" ) ); foreach( var p in query ) { Console.WriteLine( p ); } }
L’oggetto/istanza Person viene infatti salvata 2 volte… male direte voi, ma è evidente che non è possibile che non ci abbiano pensato… :-)
Cosa succede? stiamo semplicemente simulando il concetto di “disconnesso”, che nota di redazione non è pienamente supportato da db4o, nello snippet di cui sopra facciamo alcune cose interessanti:
- Creiamo una singola istanza della classe Person;
- Apriamo un “data context” e salviamo la stessa istanza 2 volte: questo porta, giustamente ad una singola “insert”, la doppia Store viene semplicemente ignorata in questo esempio perchè l’istanza di Person è “known” ergo il data context si rendo conto che la doppia store è un “non sense”
- Chiudiamo il data context e ne apriamo un altro rifacciamo la store e “boom” a questo punto abbiamo 2 “record” nel nostro database… siamo in modalità disconnessa a questo punto e giustamente l’engine non ha la più pallida idea che Person sia la stessa di prima; cosa possiamo fare?
All’interno del primo data context recuperiamo un l’id univoco che db4o genera per ogni “record” quando apriamo il secondo data context chiamiamo il metodo Bind() passando la “nuova” reference e l’id, internamente l’engine “carica” l’istanza presente nel db e la rimpiazza in memoria (nell’Identity Map) con quello che gli stiamo dando noi.long id; using( var db = Db4oFactory.OpenFile( config, @"c:\temp\db4o_Sample.db4o" ) ) { db.Store( person ); db.Store( person ); Console.WriteLine( "Double Store, Count: {0}", db.From<Person>().Count() ); id = db.Ext().GetID( person ); db.Close(); } using( var db = Db4oFactory.OpenFile( config, @"c:\temp\db4o_Sample.db4o" ) ) { db.Ext().Bind( person, id ); db.Store( person ); Console.WriteLine( "Single Store, Count: {0}", db.From<Person>().Count() ); db.Close(); }
Ok, funziona… ma è pericoloso, ecco perchè non è consigliato. Immaginiamo uno scenario disconnesso un po’ più complesso:
- Servizio WCF che maschera lo storage;
- Applicazione client;
- Il client recupera un grafo complesso di oggetti;
- Vengono modificati solo alcuni oggetti;
- Siccome il client è intelligente rimanda al servizio solo la parte modificata;
- Il servizio chiama Bind() e… solo una parte viene rimpiazzata in memoria con la “fastidiosa” conseguenza che quando chiamate Store() all’engine arriva un grafo parziale, con un id esistente e il buon db4o, giustamente, cancella dal db quello che non gli arriva… ooops…
E’ comunque un problema aperto e vi invito a non fidarvi assolutamente delle conclusioni che ho tratto io perchè sono frutto di una mezz’ora di esperimenti e tentativi, nulla di più.
Direi che per ora vi ho tediato fin troppo, credo di non aver risposto a nessuna delle domande iniziali, ma credo anche che sarebbe stato impossibile farlo in solo 4 ore di prove dal primo download.
Giusto un hint… questo funziona perfettamente:
dimostrando che anche query complesse su grafi complessi funzionano, naturalmente okkio che la query (l’ExpressionTree) deve essere trasformabile dall’engine pena non una exception ma piuttosto il caricamento di tutto il grafo e l’analisi dello stesso in memoria per risolvere la query.var query = db.From<Person>() .Where( p => p.FirstName.StartsWith( "M" ) && p.Addresses.Count > 2 );
L’intenziona adesso sarebbe quella di provarlo sul campo in un piccolo progetto personale che è partito (è lontano ancora dall’essere un embrione) con Sql Compact e che ha anche la necessità di girare su un device Windows Mobile, quindi necessita della replica… vedremo.
.m