Articoli Manifesto Tools Links Canali Libri Contatti ?
Database

SmartSync for Java

Abstract
Il driver jdbc dispone di buone capacità di reflection. Vediamo come servircene per implementare un algoritmo universale di sincronizzazione di due database.
Data di stesura: 16/11/2002
Data di pubblicazione: 16/11/2002
Ultima modifica: 04/04/2006
di Giovanni Giorgi Discuti sul forum   Stampa

Introduzione

Quando si sviluppa una applicazione web, è necessario porre molta attenzione agli aspetti legati alla sicurezza.

Spesso è il responsabile della sicurezza a dettare le regole. Supponiamo di voler scrivere una applicazione web per la pubblicazione di articoli:

[Figura 1]

Figura 1: Schema delle macchine

DBFRU è il database di fruizione, non protetto dal firewall.
DBAPP è il database di approvazione, protetto sulla sottorete interna.
Sulla macchina W è installata l'applicazione di fruizione, e sulla macchina E è installato un editor di articoli.

L'architettura illustrata prevede un utente ROUSER in sola lettura, usato dalla macchina web W, che legge gli articoli da DBFRU.

L'editor, presente sulla macchina E è in grado di scrivere sia su DBAPP che su DBFRU.

La macchina W non può accedere a DBAPP perché il firewall blocca ogni connessione.

Il DBFRU è un database veloce nelle operazioni di lettura, per esempio MySql[2]. Il sistemista può decidere di avere N macchine web ognuna con il suo database, per migliorare le performance. Per semplicità supporremo che ci sia un solo database.

DBAPP è un database transazionale in grado di gestire complessi vincoli che non vengono mantenuti su DBAPP. L'applicazione E deve essere in grado di copiare il contenuto di DBAPP su DBFRU, in modo anche incrementale.

Per semplificare la trattazione faremo alcune ipotesi di comodo:

  1. Gli schemi dei due database sono identici
  2. Vengono usati solo i tipi base SQL. Inoltre l'aggiornamento è solo in un senso, da DBAPP a DBPRU.
  3. Benché sia responsabilità del DBA assicurarsi che gli schemi dei due database siano identici, si richiede un controllo applicativo per evitare pasticci.

SmartSync

SmartSync[1] è una classe in grado di sincronizzare completamente una qualsiasi tabella.
Vediamo come è fatto il costruttore:
  1. public SmartSync(String tableName, Connection srcCon, 
  2.                  Connection destCon, Category logz) 
  3.                  throws SQLException { 
  4.     this.source=srcCon; 
  5.     this.dest=destCon; 
  6.     this.log = Category.getInstance(logz.getName()+".SmartSync."+tableName); 
  7.     this.targetTable=tableName; 
  8.  
  9.     this.universalSelect="SELECT * FROM "+ this.targetTable; 
  10.     ResultSet rs=source.createStatement().executeQuery(universalSelect); 
  11.     if(!rs.next()) { 
  12.         log.warn("Src Table is empty"); 
  13.  
  14.     ResultSetMetaData md=rs.getMetaData(); 
  15.     columns=md.getColumnCount(); 
  16.  
  17.     // Ottiene i tipi sorgente: 
  18.     log.info(this.targetTable+" <Source> COLUMNS: "+md.getColumnCount()); 
  19.     this.srcColType=getTypes(md); 
  20.  
  21.     // Fa lo stesso con la destinzione: 
  22.     ResultSet rdest=dest.createStatement().executeQuery(universalSelect); 
  23.     if(rdest.next()){ 
  24.         log.error("<Dest> Table isn't empty: Sync can fail!"); 
  25.  
  26.     ResultSetMetaData mdDest=rdest.getMetaData(); 
  27.     log.info(this.targetTable+" <Dest>   COLUMNS: "+mdDest.getColumnCount()); 
  28.     this.destColType=getTypes(mdDest); 
  29.  
  30.     rs.close(); rdest.close(); 
  31.     checkDestTypes(); 
  32.  
  33.     this.universalInsert="INSERT INTO "+this.targetTable +" VALUES ("; 
  34.     int i=columns; 
  35.     while(i>1){ 
  36.         this.universalInsert += "?,"; 
  37.         i--; 
  38.     this.universalInsert += " ?)"; 
SmartSync richiede in input tre parametri: il nome della tabella da processare, la connessione sorgente (es DBAPP) e quella di destinazione (es DBFRU).

Il costruttore inizializza le variabili d'istanza, e provvede anche ad eseguire una select sulla tabella sorgente verificando che non sia vuota (linee 4-14). Poi legge il ResultSetMetaData ed estrae i tipi delle colonne, servendosi del metodo getTypes (linee 15-20). La stessa cosa viene fatta per la tabella destinazione (righe 23-30).

In linea 33 la funzione checkDestTypes() controlla che i tipi sorgente e destinazione coincidano. In caso di errore viene lanciata una eccezione specifica (SyncException) che è una sottoclasse di SQLException.
Se tutto va a buon fine viene creato lo statement sql di insert (universalInsert).

Il metodo getTypes si serve del ResultSetMetaData per ottenere i tipi identificati da costanti intere. Per migliorare il debugging, ho introdotto anche una funzione universale (typeToString) che traduce questi tipi in stringhe:

  1. int [] getTypes(ResultSetMetaData md) throws SQLException { 
  2.     int colType[]; 
  3.     int cols=md.getColumnCount(); 
  4.  
  5.     colType=new int[cols]; 
  6.     int t=0; 
  7.     String msg="  Types:"; 
  8.     while(t<cols){ 
  9.         // Rec: le pos partono da 1, gli array da zero! 
  10.         colType[t]=md.getColumnType(t+1); 
  11.         msg+=typeToString(colType[t]) + " "; 
  12.         t++; 
  13.  
  14.     log.debug(msg); 
  15.     return colType; 
  16.  
  17. private String typeToString(int type) { 
  18.     switch(type) { 
  19.         case java.sql.Types.ARRAY:return "ARRAY"; 
  20.         case java.sql.Types.BIGINT: return "BIGINT"; 
  21.         case java.sql.Types.BINARY: return "BINARY"; 
  22.         case java.sql.Types.BIT: return "BIT"; 
  23.         case java.sql.Types.BLOB: return "BLOB"; 
  24.         case java.sql.Types.CHAR: return "CHAR"; 
  25.         case java.sql.Types.CLOB: return "CLOB"; 
  26.         case java.sql.Types.DATE: return "DATE"; 
  27.         case java.sql.Types.DECIMAL: return "DECIMAL"; 
  28.         case java.sql.Types.DISTINCT: return "DISTINCT"; 
  29.         case java.sql.Types.DOUBLE: return "DOUBLE"; 
  30.         case java.sql.Types.FLOAT: return "FLOAT"; 
  31.         case java.sql.Types.INTEGER: return "INTEGER"; 
  32.         case java.sql.Types.JAVA_OBJECT: return "JAVA_OBJECT"; 
  33.         case java.sql.Types.LONGVARBINARY: return "LONGVARBINARY"; 
  34.         case java.sql.Types.LONGVARCHAR: return "LONGVARCHAR"; 
  35.         case java.sql.Types.NULL: return "NULL"; 
  36.         case java.sql.Types.NUMERIC: return "NUMERIC"; 
  37.         case java.sql.Types.OTHER: return "OTHER"; 
  38.         case java.sql.Types.REAL: return "REAL"; 
  39.         case java.sql.Types.REF: return "REF"; 
  40.         case java.sql.Types.SMALLINT: return "SMALLINT"; 
  41.         case java.sql.Types.STRUCT: return "STRUCT"; 
  42.         case java.sql.Types.TIME: return "TIME"; 
  43.         case java.sql.Types.TIMESTAMP: return "TIMESTAMP"; 
  44.         case java.sql.Types.TINYINT: return "TINYINT"; 
  45.         case java.sql.Types.VARBINARY: return "VARBINARY"; 
  46.         case java.sql.Types.VARCHAR : return "VARCHAR"; 
  47.         default: 
  48.             return"UNKNOWN_SQL_TYPE?"; 
  49.     } // switch 
Al ritorno dal costruttore il chiamante sa di avere un oggetto SmartSync che ha già validato la compatibilità dei tipi delle colonne tra i due database: per cui si può procedere tranquillamente alla sincronizzazione.

Invocando il metodo syncAll() si fa partire il processo di sincronizzazione:

  1. void syncAll() throws SQLException { 
  2.     log.debug("SmartSync->"+targetTable); 
  3.     // Carico e scarico! 
  4.     // Ora inizia il ciclo: 
  5.     int type, rowProcessed=0; 
  6.     Object objC; 
  7.     ResultSet rsSrc=this.source.createStatement().executeQuery(universalSelect); 
  8.     PreparedStatement insertStm=this.dest.prepareStatement(universalInsert); 
  9.     while(rsSrc.next()){ 
  10.  
  11.         // Read from Src and write to dest.... 
  12.         for(int i=1; i<= this.columns; i++){ 
  13.             objC=rsSrc.getObject(i); 
  14.             type=findType(i); 
  15.  
  16.             //log.debug("Processing COL:"+i+" Src:"+objC); 
  17.             if(objC!=null) { 
  18.                 insertStm.setObject(i,objC, type); 
  19.             }else{ 
  20.                 // Gli oggetti null vanno trattati diversamente. 
  21.                 insertStm.setNull(i, type); 
  22.         insertStm.execute(); 
  23.         rowProcessed++; 
Per prima cosa si estraggono i dati con una sola query (linea 7). Poi si inizia il ciclo di inserimento (linee 9-26). Per ogni inserimento si estraggono i valori di tutte le colonne (ciclo for in linee 12-23) Si ottimizza il ciclo servendosi di una PreparedStatement per effettuare le insert.

Purtroppo l'API del JDBC è allergica agli oggetti null, per cui è necessario porre attenzione quando manipola un tale oggetto. Quando si estrae (linea 13) un valore SQL NULL con l'API JDBC, esso è trasformato in un puntatore java null. Benché abbiano lo stesso nome, si tratta di due entità differenti. Per questa ragione le linee 17-22 si preoccupano di modificare il ciclo che popola la query di insert in modo appropriato. Se si tentasse di eseguire insertStm.setObject(i,null, type) si riceverebbe un errore a runtime.

Note d'uso

Siccome si tratta di una sincronizzazione applicativa, è più lenta di quella che si avrebbe se i due database riuscissero a parlarsi "da soli", per esempio scambiandosi dump compressi dei dati.

Per esempio, Oracle[4] è famoso per un tool, chiamato SQL*Loader che è efficientissimo nel caricamento di enormi quantità di dati da file di testo; purtroppo SQL*Loader funziona solo con Oracle e non è intuitivo da usare.

È importante notare che è responsabilità del client che usa SmartSync effettuare il commit() della transazione di destinazione. In caso di errore durante l'esecuzione di syncAll() il chiamante è in grado di eseguire un rollback() e di mantenere il database in uno stato consistente.

Sperimentalmente le performance di SmartSync sono soddisfacenti, a patto che la presenza di router/firewall/switch non rallenti troppo la connessione al DBFRU.

Sviluppi Futuri

Si è presentato qui solo un assaggio di quello che si può fare servendosi del driver JDBC.
In un prossimo articolo vedremo che è possibile costruire dei filtri per sincronizzare solo colonne che soddisfino particolari condizioni di where. Inoltre è possibile anche sviluppare una serie di classi di mapping che trasformino delle colonne in altre per migrare facilmente schemi differenti ma semanticamente compatibili. Potremmo chiamare questi componenti DataMutators.

Informazioni sull'autore

Giovanni Giorgi, classe 1974. Dopo il diploma di liceo Classico, si è laureato in Informatica nel febbraio 2000, e attualmente lavora nel campo del software finanziario (trading on line, soluzioni web).
Appassionato di linguaggi di programmazione, si interessa anche di politica e letteratura.

È possibile consultare l'elenco degli articoli scritti da Giovanni Giorgi.

Altri articoli sul tema Database.

Risorse

  1. Il sorgente del codice illustrato nell'articolo.
    http://www.siforge.org/articles/2002/11/smart-sync/src.jar (4Kb)
  2. MySql è un database Open Source con un discreto seguito. È famoso per la sua velocità ma anche per la carenza di molte features tipiche dei database relazionali.
    http://www.mysql.com
  3. PostgreSQL è un database relazionale open source, che supporta parecchie features al pari di database commerciali. Inoltre supporta caratteristiche avanzate, come una forma embrionale di OOP (ereditarietà tra tabelle, overloading sulle funzioni, operatori ed aggregatori).
    http://www.postgresql.com
  4. Il database più famoso.
    http://www.oracle.com/ip/deploy/database/oracle9i
  5. Il server SQL di Microsoft. Gira solo su piattaforma PC x86.
    http://www.microsoft.com/sql
  6. Log4J è un tool evoluto per la gestione dei log con Java. È vivamente consigliato dall'Autore dell'articolo.
    http://jakarta.apache.org/log4j/docs/index.html
  7. Oracle Database Logging tools. Si tratta di un equivalente di log4j per il database Oracle.
    http://log4plsql.sourceforge.net
Discuti sul forum   Stampa

Cosa ne pensi di questo articolo?

Discussioni

Questo articolo o l'argomento ti ha interessato? Parliamone.