Io sinceramente ogni tanto mi pento di essere così “goloso” di conoscenza… perchè già il nostro lavoro è, se vogliamo, una fonte infinita di possibilità se poi ci mettiamo ad ascoltare le pulci nell’orecchio allora siamo proprio rovinati.
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?
Cose a cui siamo abituati con i database tradizionali e a cui giustamente non avrebbe senso rinunciare.
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 :-)
Tralasciando per ora il “ObjectManager” sui cui tornerò per capire quali sono i problemi vediamo di affrontare i concetti base per familiarizzare con db4o. A prima vista è di una semplicità disarmante, aggiungete una reference a:
  • Db4objects.Db4o.dll;
  • Db4objects.Db4o.Linq.dll (e qui slurp…);
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 );
    }
}
“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:
  • 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;
Lo snippet di codice di poco fa offre molti spunti di riflessione:
  • 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 IDb4oLinqQuery From( this IObjectContainer db )
    {
        return db.Cast();
    }
}
Possiamo 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:
Ok, ma dietro le quinte cosa succede? Abbiamo creato un’istanza della classe Person, abbiamo aperto una connessione al nostro database e chiamando il metodo Store( Object ) chiediamo all’engine di db4o di salvare l’oggetto, in realtà vedremo che chiediamo di salvare il grafo… quindi molto ma molto di più ;-) dopo di che facciamo una query con linq alla ricerca di tutti gli oggetti Person il cui FirstName inizi con la lettera “M”.
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:
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 );
    }
}
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”.
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:
using( 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 );
    }
}
altro non facciamo che passare un Predicate, Predicate non Expression, al metodo generico Query specificando con T quale è il tipo che stiamo interrogando.
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;
Comunque quello che possiamo fare è:
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 );
    }
}
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.
Complichiamoci un attimo la vita, altrimenti che gusto c’è ;-) questa è la classe Person nel suo insieme:
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 );
    }
}
Ci sono alcune cose interessanti:
  • è “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;
Osservate adesso questo snippet:
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 ); } }
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ì:
image
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?
db4o offre un supporto parziale, e vedremo che non ci si può fare molto, per gli scenari disconnessi: ogni oggetto (record) in db4o ha un id univoco, quello che possiamo fare è semplicemente questo:
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();
}
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.
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…
Il consiglio è quello quindi di fare l’operazione in maniera manuale, quindi caricare l’oggetto fare la copia delle modifiche e risalvarlo. Ci sono svariati thread a riguardo sui forum di db4o con svariate soluzioni, più o meno funanboliche, molto usano caratteristiche di java a me sconosciute, molto simili ad un uso massiccio di Reflection, quindi non ho idea se siano applicabili al mondo .net o se ci siano alternative.
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:
var query = db.From<Person>()
    .Where( p => p.FirstName.StartsWith( "M" ) && p.Addresses.Count > 2 );
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.
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