Untitled Document
   
 
Hibernate vs Rail - la sfida sulla persistenza
di Patrick Peak, traduzione di Carlo Possati

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.