Come
altre persone orientate al mondo java, ad esempio Bruce
Tate e David Geary, ho dato un'occhiata ad un nuovo
framework web: Rails. Di particolare interesse secondo
il mio punto di vista è Active Record, il suo
strumento ORM (Object Relational Mapping). Siccome scegliere
una
tecnologia comporta sempre valutazioni che ne giustifichino
la spesa, ho scritto questo articolo per confrontarlo
con un altro diffuso strumento ORM: Hibernate. Riassumerò
le conoscenze che ho approfondito riguardo a Rails,
paragonandolo ad Hibernate, una tecnologia a me molto
familiare.
Premessa
Uno dei più recenti e attuali argomenti di discussione
tra gli sviluppatori è proprio la salita alla
ribalta di questo nuovo framework, Ruby on Rails. Rails
è un framework web basato sul modello MVC, concettualmente
simile a Struts o a Webwork, ma scritto nel linguaggio
di scripting Ruby invece che
in Java. Oltre alle normali funzionalità di un
framework web, offre un numero di tecnologie integrate,
come il generatore di codice e lo strumento ORM (Object
Relational Mapping), ActiveRecord. Combinando un certo
numero di strumenti in un singolo ed elegante framework
integrato, viene
dichiarato che lo sviluppo attraverso Rails è
"almeno 10 volte più veloce con Rails di
ciò che si può ottenere con un tipico
framework java".
Ora
tale dichiarazione, e nuova convenzione, può
far supporre che Rails possa essere il nuovo Salvatore
reincarnato oppure in alternativa una aberrante creatura
degli abissi più remoti. Quale sarà il
vostro giudizio probabilmente dipenderà dal fatto
che il vostro linguaggio di programmazione preferito
incominci con la lettera J o R.
Cosa
non vuole essere questo articolo
Questo non vuole essere un articolo che dichiara che
Hibernate è sicuramente il miglior prodotto sul
mercato e Rails è pessimo. All'apparenza Rails
sembra possedere alcuni concetti interessanti, ma di
norma sono molto sospettoso quando si parla di una produttività
aumentata di almeno 10 volte.
Inizialmente, il framework sembra essere molto veloce
specialmente se si sta implementando un database web
con un modello puramente CRUD.
Comunque è difficile vedere se la velocità
di sviluppo si mantiene sempre così elevata.
Considerazioni
generali
Basandomi sulla ricerca che ho compiuto, Rails sembra
essere molto solido in un modello singola tabella/singolo
oggetto. Questo caratteristica lo rende adatto a modelli
semplici con poche associazioni. L'assunzione che fa
riguardo ai database sono ragionevoli e semplici da
gestire per un progetto
che si costruisce dall'inizio. Hibernate brilla invece
quando il modello dell'oggetto risulta più complicato,
oppure quando ci si riferisce a database già
esistenti. Studiandolo maggiormente si capisce che è
più maturo e che possiede maggiori funzionalità.
Entrando nel dettaglio si può evidenziare quali
siano
le differenze più evidenti. Di seguito una lista
di concetti e domande a cui spero di dare una spiegazione
con questo articolo.
-
Basic Architecture Patterns Hibernate e Rails
utilizzano pattern
ORM completamente diversi. Che cosa implica?
- Essere
espliciti Hibernate e Rails definiscono la
mappatura in
modo diverso, Hibernate in modo esplicito e Rails
in modo
implicito. Cosa significa e quanto è importante?
- Associazioni
Entrambi supportano le associazioni, in che
modo?
- Modelli
a persistenza transitiva Come gestiscono gli
oggetti
persistenti?
- Linguaggio
delle Query In che modo si trovano gli oggetti?
- Configurazione
delle prestazioni Quali opzioni è necessario
gestire?
Entriamo
nei dettagli.
Differenze
sull'architettura di base
La differenza sostanziale tra Rails ActiveRecord ed
Hibernate è il pattern architetturale su cui
si basano. Rails, come ovvio, utilizza il pattern di
ActiveRecord, mentre Hibernate utilizza il pattern Data
Mapper/Identity Map e il pattern Map/Unit of Work. Da
tale conoscenza possiamo già intuire differenze
potenziali.
Pattern
Active Record
L'ActiveRecord è "un oggetto che racchiude
una riga in una tabella o in una vista di un database,
incapsulando un accesso al database e aggiungendo un
dominio logico a quel dato"[Fowler, 2003]. Ciò
significa che ActiveRecord possiede metodi di "classe"
per trovare istanze, ed ogni istanza è
responsabile del proprio salvataggio, aggiornamento
e cancellazione nel database. Risulta ben adattabile
per domini con modelli semplici, quelli cioè
in cui le tabelle riflettono con buona fedeltà
il modello del dominio. E' inoltre generalmente più
semplice del più potente, ma più complesso
pattern Data Mapper.
Pattern
Data Mapper
Il
pattern Data Mapper rappresenta "uno strato di
mappatura che sposta dati tra gli oggetti ed il database
mantenendoli indipendenti tra di loro e rispetto allo
stesso mappatore" [Fowler, 2003]. Sposta la responsabilità
della persistenza fuori dall'oggetto del dominio, e
solitamente utilizza una mappa di identità per
mantenere le relazioni tra l'oggetto del dominio stesso
ed il database. In aggiunta, spesso utilizza (e Hibernate
lo fa) una Unita di Lavoro (sessione) per mantenere
traccia di oggetti che vengono modificati e assicurare
la correttezza della persistenza.
Implicazioni
Generali dei Pattern
Dopo aver parlato delle differenze fondamentali, le
implicazioni che ne derivano dovrebbero apparire abbastanza
ovvie. L'ActiveRecord (Rails) risulta più facilmente
comprensibile e più semplice da utilizzare in
un primo momento, ma un suo utilizzo per funzionalità
maggiormente avanzate risulterà più complicato
se non addirittura impossibile. Il nocciolo quindi è
sapere se e quando un progetto oltrepasserà questa
linea di utilizzo. Diamo ora uno sguardo ad alcune specifiche
dei due framework. Per illustrare queste differenze
utilizzeremo del codice da un mio progetto esempio chiamato
"Progetto Deadwood".
Il
Vantaggio dell'essere espliciti
Qual è il vantaggio dell'essere espliciti? Una
delle "caratteristiche" chiave dell'ActiveRecord
di Ruby è data dalla non necessità di
esplicitare i campi di una classe, in quanto questi
vengono determinati dinamicamente in base alle colonne
del database. In questo modo se si ha una tabella "miners"
come la
seguente:
create
table miners (
id BIGINT NOT NULL AUTO_INCREMENT,
first_name VARCHAR(255),
last_name VARCHAR(255),
primary key (id)
)
La
corrispondente classe ruby (miner.rb) risulta essere:
class
Miner < ActiveRecord::Base
end
miner.first_name
= "Brom"
Invece, la classe Hibernate (Miner.java) che esplicita
i campi, getter/setter e i tag xdoclet risulta:
package
deadwood;
/**
* @hibernate.class table="miners"
*/
public class Miner {
private Long id;
private String firstName;
private String lastName;
/**
* @hibernate.id generator-class="native"
*/
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
/**
* @hibernate.property column="first_name"
*/
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName
= firstName;
}
/**
* @hibernate.property column="last_name"
*/
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName
= lastName; }
}
miner.setFirstName("Brom");
Come
ho menzionato in un precedente articolo, sia Rails che
Hibernate necessitano di specificare il nome del campo
una volta sola. I dettagli di Hibernate risiedono nel
codice, quelli di ActiveRecord nel database. In questo
modo quando navigando nel codice ruby ci si troverà
di fronte a questo
class
GoldClaim < ActiveRecord::Base
end
La
domanda sarà: quali campi possiede questo oggetto?
Si dovrà quindi utilizzare MySQL Front (o strumenti
equivalenti) per controllare nello schema del database.
Quando il numero dei modelli del dominio è piccolo,
questo non diventa un grosso problema. Ma quando un
progetto presenta più di 40 tabelle/50 oggetti
del dominio (come il progetto Hibernate su cui sto attualmente
lavorando) questa operazione risulterà maggiormente
dolorosa. In ultima analisi, anche se può non
essere in cima alle "vostre" priorità,
avere più dettagli nel codice, ne rende più
semplice la comprensione e l'eventuale modifica.
Associazioni
Nella sezione precedente, la classe Miner considerata
era composta da una singola tabella mappata su di una
singola tabella miners. I sistemi ORM supportano più
soluzioni che mappano tabelle associante a oggetti in
memoria. Su questo argomento Hibernate e Rails non presentano
differenze sostanziali: entrambi possono utilizzare
le più basilari strategie di mappatura. Di seguito
è inserita una lista non esaustiva di associazioni
supportate da entrambi gli strumenti, inclusa la corrispondente
convenzione di
nomenclatura Hibernate Rails quando necessaria.
- Molti
a uno/uno a molti appartiene_a/possiede_un
- Uno
a molti (set) possiede_molti
- Molti
a molti (set) possiede_e_appartiene_a_molti
- Ereditarietà
di singola tabella
- Componenti
(mappatura > 1 oggetto per tabella)
A
titolo di esempio comparativo, si consideri la relazione
molti a uno, ampliando così la parte I dell'esempio
del progetto Deadwood. Si aggiunga alla classe Miner
una associazione molti a uno con un oggetto GoldClaim.
Ciò comporta la presenza di una foreign key,
gold_claim_id nella tabella miners che fa riferimento
ad un record nella tabella gold_claims.
(Java)
public class Miner {
// Other fields/methods omitted
private GoldClaim goldClaim;
/**
* @hibernate.many-to-one column="gold_claim_id"
* cascade="save"
*/
public GoldClaim getGoldClaim() { return goldClaim;
}
public void setGoldClaim(GoldClaim goldClaim) {
this.goldClaim = goldClaim;
}
}
(Rails)
class Miner < ActiveRecord::Base
belongs_to :gold_claim
end
In
tale esempio non si trovano molte differenze sostanziali,
entrambi gli strumenti eseguono la medesima funzionalità.
Hibernate utilizza una mappatura esplicita per specificare
la colonna identificata dalla foreign key, come la Cascata,
l'attività seguente di cui parleremo dopo. Salvando
un oggetto Miner di conseguenza viene salvata la sua
associata GoldClaim, invece aggiornandola o cancellandola
non si influisce sull'oggetto associato.
Persistenza
transitiva
Applicazioni
differenti da quelle di esempio tendono a lavorare con
grafici estesi di oggetti connessi. Risulta importante
per una soluzione ORM trovare un modo per individuare
e sequenzializzare modifiche in oggetti in memoria nel
database, escludendo la necessità di salvare
manualmente ognuno di essi.
Hibernate provvede con una versione potente e flessibile
di questo tipo di persistenza dichiarativa a cascata.
Rails sembra offrire una versione limitata della persistenza,
basata sul tipo di associazione. Per esempio Rails sembra
emulare di default l'attività di Hibernate di
cascade="save" per l'associazione appartiene_a.
Vediamo infatti il seguente codice di Rails
miner
= Miner.new("name" => "Brom Garrott")
miner.gold_claim = GoldClaim.new( "name" =>
"Western Slope")
miner.save # This saves both the Miner and GoldClaim
objects
miner.destroy # Deletes only the miner row from the
database
Cancellando, Hibernate offre un numero di differenti
attività in cascata per tutti i tipi di associazione
fondendo così un alto livello di flessibilità.
Per esempio, settando cascade="all" permetterà
a GoldClaim il salvataggio, l'update e la cancellazione
in accordo col padre Miner, vediamo come
Miner
miner = new Miner();
miner.setGoldClaim(new GoldClaim());
session.save(miner); // Saves Miner and GoldClaim objects.
session.delete(miner); // Deletes both of them.
Per
essere onesti, si può permettere anche a Rails
di agire in questo modo, ma è necessario fornire
al codice tale funzionalità come parte del ciclo
di vita dell'oggetto Miner utilizzando una chiamata
di callback. Cambiare quindi anche una sola cosa è
pur sempre un cambiamento. Con Rails se si vuole fare
cascading funziona esclusivamente dal lato "has-one".
In questo modo se si vuole modificare il nome della
classe GoldClaim non si potrà.
miner
= Miner.find(@params['id'])
miner.gold_claim.name = "Eastern Slope"
miner.save
Questo
non aggiorna la gold_claim.name. Dalla direzione opposta
(has_one), invece funziona
class
GoldClaim < ActiveRecord::Base
has_one :miner
end
claim
= GoldClaim.find(@params['id'])
claim.miner.name = "Seth Bullock"
claim.save # Saves the miner's name
Usando
il cascade="save-update", è possible
ottenere questo beneficio per ogni associazione, non
considerando in quale tabella è presente la foreign
key. Hibernate non basa la persistenza transitiva al
di fuori del tipo di relazione, ma piuttosto sullo stile
cascade, che è più a grana fine e potente.
Di seguito vediamo come ognuno dei due framework trova
l'oggetto che abbiamo reso persistente.
Linguaggio
per le query
Quanto il numero di similitudini aumenta tra i due framework
quando si parla di linguaggio delle query, tanto si
discosta rapidamente parlando di potenzialità
e utilizzo. Rails utilizza essenzialmente SQL, lo standard
più conosciuto per manipolare dati col database.
In aggiunta, per garantire l'utilizzo di metodi di ricerca
dinamici, è presente quello che considero un
suo proprio "mini" linguaggio che permette
agli sviluppatori di scrivere query semplificate inventando
nuovi metodi. Comunque alla fine tutto rimane espresso
in termini di tabelle e colonne.
Hibernate
invece possiede un proprio linguaggio per le query orientato
agli oggetti (Hibernate Query Language - HQL), che risulta
deliberatamente molto simile a SQL. La differenza consiste
nel permettere ai programmatori di esprimere le proprie
query in termini di oggetti e campi al posto di tabelle
e colonne. Hibernate poi traduce le query così
scritte in SQL ottimizzandole per ogni tipo di database.
Ovviamente inventare un nuovo linguaggio per le query
è un compito gravoso, ma l'espressività
e la potenza di questo
linguaggio è comunque uno dei punti di forza
di Hibernate. Vediamo quindi alcuni esempi relativi
ai due framework.
Rails
Insta-Finders
Per
query semplici, come ad esempio "trovare x e y
in base ad una proprietà", Rails permette
di aggiungere dinamicamente metodi filtro che poi si
occupa di tradurre in SQL. Supponendo per esempio che
si voglia trovare minatori della tabella Miners basandosi
sulle proprietà nome e cognome, è necessario
scrivere qualcosa del tipo riportato in seguito.
@miners
= Miner.find_by_first_name_and_last_name("Elma",
"Garrott")
In
più, l'oggetto ActiveRecord fornisce metodi di
ricerca comuni che permettono di passare nella clausola
where, come ad esempio
#
Returns only the first record
@miner = Miner.find_first("first_name = ?",
"Elma")
#
Finds up to 10 miners older than 30, ordered by age.
@miners = Miner.find_all ["age > ?", 30],
"age ASC", 10
#
Like find all, but need complete SQL
@minersWithSqA = Miner.find_by_sql [
"SELECT m.*, g.square_area FROM gold_claims g,
miners m " +
" WHERE g.square_area = ? and m.gold_claim_id =
g.id", 1000]
Il
punto è che le classi di Rails possiedono campi
dinamici, e tutte le colonne ritornate nel resultset
vengono inglobate nell'oggetto Miner. Nell'ultima query,
l'oggetto Miner possiede un campo square_area che non
possiede normalmente. Ciò significa che è
necessario modificare la vista, come in questo modo
#
Normal association traversing
<%= miner.gold_claim.square_area
#
Altered query for @minersWithSqA
<%= miner.square_area %>
Eseguire
query su oggetti con HQL
Come
detto precedentemente, essere in grado di esprimersi
in termini di oggetti e colonne è molto efficace.
Mentre query semplici sono facilmente definite con Rails,
quando è necessario navigare attraverso gli oggetti
con SQL, può essere molto più conveniente
utilizzare HQL. Guardiamo un
esempio
//
Find first Miner by name
Query q = session.createQuery("from Miner m where
m.firstName = :name");
q.setParameter("name", "Elma");
Miner m = (Miner) q.setMaxResults(1).uniqueResult();
//
Finds up to 10 miners older than 30, ordered by age.
Integer age = new Integer(30);
Query q = session.createQuery(
"from Miner m where m.age > :age order by age
asc");
List miners = q.setParameter("age", age).setMaxResults(10).list();
//
Similar to join query above, but no need to manually
join
Query q = session.createQuery(
"from Miner m where m.goldClaim.squareArea = :area");
List minersWithSqA = q.setParameter("area",
new Integer(1000)).list()
Considerando
l'ultima query vediamo che l'oggetto Miner possiede
sempre gli stessi campi qualsiasi sia il modo in cui
esso venga trovato. Così a differenza di quello
che avviene con le query di Rails, attraverso una vista
JSP, è possibile ancora accedere all'associazione
in modo tradizionale, in
questo modo:
${miner.goldClaim.squareArea}
<%-- Traverse fields normally --%>
Dopo aver visto le tecniche base per recuperare gli
oggetti, spostiamo l'attenzione su come rendere questo
processo più veloce. La prossima sezione spiega
il significato della configurazione dei parametri di
performance.
Configurazione
dei parametri di performance
Oltre a mappare oggetti su tabelle, una soluzione ORM
che possa definirsi robusta deve fornire soluzioni per
gestire l'ottimizzazione delle query. Uno dei rischi
che si possono avere lavorando con strumenti ORM è
quello di prelevare troppi dati dal database. Questo
accade in quanto è molto semplice
prelevare diverse migliaia di record con query SQL,
basandoci su un semplice statement come "from Miner".
Comuni strategie ORM per ovviare a questo problema includono
il Lazy (pigro) fetching, outer join fetching e caching.
Rails
è molto molto pigro
Ciò
che si intende dicendo che Rails è pigro è
che quando si vuole prelevare un oggetto, esso si blocca
su tale oggetto e non preleva altri dati da altre tabelle
fino alla richiesta dell'associazione, e questo comportamento
ovvia al problema di caricare troppi dati che non servono.
Sia Rails che Hibernate
supportano tale scelta, ma Hibernate permette inoltre
di poter discriminare su quali associazioni operare
tale scelta. Nell'esempio seguente si può vedere
come operare con Rails
@miner
= Miner.find(1) # select * from miners where id = 1
@claim = @miner.gold_claim # select * from gold_claim
where id = 1
Ciò
ci porta ad una delle più grandi falle degli
strumenti ORM, e cioè pensare che caricare dati
in maniera "pigra" sia sempre una buona decisione
da prendere. In realtà questa è una buona
scelta solo se non si ha bisogno di certi dati. Altrimenti
si potrebbero utilizzare migliaia di query per ottenere
lo
stesso risultato utilizzando una sola query. Questo
è il famigerato problema di N+1 select, dove
il reperimento di tutti gli oggetti richiede N select
+ la prima originale. Questo è un problema che
peggiora quando si ha a che fare con le collections.
Outer
Join e Fetching esplicito
Normalmente
uno dei modi migliori per aumentare le performance è
quello di limitare il numero di accessi al database.
Meglio avere una query molto lunga che più query
brevi. Hibernate possiede vari modi per gestire il problema
delle N+1 select. Le associazioni possono essere esplicitamente
etichettate nella selezione con outer join (atrraverso
outer-join="true") quindi è possibile
aggiungere l'outer join allo statement HQL. Per esempio
/**
* @hibernate.many-to-one column="gold_claim_id"
* cascade="save-update" outer-join="true"
*/
public GoldClaim getGoldClaim() { return goldClaim;
}
//
This does one select and fetches both the Miner and
GoldClaim
// and maps them correctly.
Miner m = (Miner) session.load(Miner.class, new Long(1));
In aggiunta, quando si selezionano liste oppure si ha
a che fare con
collezioni di associazioni è possibile utilizzare
un outer join esplicito,
come ad esempio
//
Issues a single select, instead of 1 + N (where N is
the # miners)
List list = session.find("from Miner m left join
fetch m.goldClaim");
L'incremento
di prestazione che si può avere in questo modo
può essere notevole. Rails d'altra parte soffre
molto il problema delle N+1 select e presenta delle
limitazioni nella soluzione di tale problema, a parte
lo scrivere esplicite join SQL riferite a query Piggy-back.
Il problema nasce dal fatto che Rails mappando tutti
i campi sull'oggetto Miner fa si che si perdano gli
oggetti associati, ciò comporta la necessità
di modificare le viste e il modo di lavorare con il
modello del dominio. In più la query si complica,
in particolar modo se ci sono più associazioni
da prelevare. La query @minersWithSqA
che è stata scritta precedentemente è
un esempio di query Piggy Back. Altro problema è
dato dal fatto che i campi addizionali risultano essere
di tipo stringa, perdendo in questo modo la propria
tipologia originale. E le cose peggiorano aggiungendo
progressivamente ulteriori associazioni.
Caching
Anche
se l'operazione di caching non sempre risulta essere
un valido aiuto, Hibernate presenta un enorme vantaggio
potenziale a riguardo. Fornisce infatti diversi livelli
di caching, incluso un livello di sessione (unità
di lavoro) e un opzionale secondo livello di cache.
Di norma si utilizza il primo
livello di cache per prevenire riferimenti circolari
e multipli accessi al database per lo stesso oggetto.
Utilizzando un secondo livello di cache si permette
allo stato del database di rimanere in memoria. Ciò
è utile principalmente per letture frequenti
e dati con riferimenti. Non presenta opzioni per l'operazione
di caching a livello di database. (Nonostante supporti
il caching per il livello web).
Conclusioni
Anche se non sono stati coperti tutti i possibili argomenti
di discussione, sono state analizzate alcune delle differenze
più sostanziali tra i due framework. In questo
modo si dovrebbe avere una conoscenza di base sulle
caratteristiche dei sue framework. Sono stati trattati
i pattern di architettura di base che costituiscono
le fondamenta di Rails e Hibernate, come pure il modo
in cui l'esplicazione si applichi alle classi di base
di entrambi i framework. Per quanto riguarda la associazioni,
esistono altri modi per la
mappatura che sono possibili con sistemi ORM, ma ci
si è occupati di quelli più utilizzati
dagli sviluppatori.
A
parte i basilari, Hibernate presenta alcuni tipi di
mappatura aggiuntivi (ne ho documentato un catalogo
completo di esempi con più di 20 tipi di mappatura
in Hibernate Quickly), inclusi differenti strategie
di ereditarietà, tipi definiti, e mappe di tipi
semplici o entità. Con Rails è più
semplice specificare e utilizzare le associazioni rispetto
ad Hibernate, ma queste presentano meno funzionalità
e quindi risultano meno utili. Se consideriamo esempi
semplici come singole tabelle con poche associazioni
e con un sistema di nomenclatura corretto Rails si comporta
molto bene, ma quando i modelli si complicano Hibernate
rappresenta sicuramente una scelta più efficace.
Rails
e Hibernate sono molto diversi quando il discorso si
sposta sul linguaggio delle query. Anche se non è
possibile fare una comparazione esaustiva dei loro linguaggio
per le query, di norma una select per singoli oggetti/tabelle
è più rapida e semplice con Rails, mentre
quando vengono introdotte query su tabelle in relazione
tra loro Hibernate funziona meglio. Rails utilizza SQL
che è un linguaggio familiare per la maggior
parte dei programmatori, mentre Hibernate utilizza HQL
un linguaggio di query orientato agli oggetti che i
programmatori invece normalmente devono imparare. Hibernate
offre più opportunità di configurazione,
in accordo con i necessari meccanismi ORM come il prelevamento
di dati tramite outer join, prelevamento pigro configurabile
e un secondo livello di cache. Tutto ciò dimostra
che Rails è adatto a progetti di piccole dimensioni,
ma il suo strato ORM necessita di un numero di funzionalità
essenziali per renderlo scalabile nei confronti di progetti
di grandi dimensioni.
Riferimenti
The Art of .war - Patrick's Weblog
Hibernate - Project Homepage
Ruby on Rails - Project Homepage
Patterns of Enterprise Application Architecture - By
Martin Fowler
Getting Rolling with Rails - By Curt Hibbs
Patrick
Peak è co-autore di Hibernate Quickly. Nonostante
si senta abbastanza a disagio nel riferirsi a se stesso
in terza persona, Patrick Peak è attualmente
Chief Technology Officer di Browsermedia, uno sviluppatore
e progettista web in Bethesda, MD. Il suo attuale impiego
riguarda l'utilizzo di risorse open source come
fondamenti per lo sviluppo rapido di software per sistemi
web. Ha utilizzato su numerosi progetti la maggior parte
dei framework open source più popolari e usati,
inclusi Struts, WebWork, Hibernate and Spring. Uno dei
suoi attuali progetti è quello di impacchettare
Hibernate Quickly che verrà pubblicato da Manning
Publiching nel Maggio del 2005. Il suo tempo libero
lo trascorre con partite di calcio, mountain bike e
kickball.
|