Java & PostgreSQL: da JDBC a Pl/Java (passando per org.postgresql.Driver) Ing. Luca Ferrari, PhD [email protected] Italian PostgreSQL Users Group ITPug Synopsis Questa presentazione vuole fungere da punto di riferimento per gli sviluppatori Java che vogliano scrivere applicazioni capaci di gestire i dati contenuti in un cluster PostgreSQL. In questa presentazione verranno mostrate le principali tecniche per la connettività Java verso PostgreSQL (tramite JDBC), il funzionamento interno del driver JDBC ufficiale nonché come “estendere” il database PostgreSQL con tecnologia Java tramite Pl/Java. Gli esempi di applicazioni/database qui riportati sono a puro scopo didattico. Java & PostgreSQL – Luca Ferrari 2 di 182 Database didattico CREATE TABLE corso ( corsopk serial NOT NULL, ­­ Chiave surrogata della tabella corso. corsoid character varying(10), ­­ Chiave reale della tabella corso. descrizione text, ­­ Descrizione del corso requisiti text, ­­ Elenco dei requisiti richiesti data date, ­­ Data in cui si tiene il corso n_ore integer, ­­ Numero di ore del corso. materiale oid, ­­ Materiale didattico distribuito con il corso ts timestamp with time zone DEFAULT ('now'::text)::timestamp, CONSTRAINT corso_chiave_surrogata PRIMARY KEY (corsopk), CONSTRAINT corso_chiave_reale UNIQUE (corsoid), CONSTRAINT corso_n_ore_check CHECK (n_ore > 0 AND n_ore < 8) ) Java & PostgreSQL – Luca Ferrari 3 di 182 Database didattico CREATE TABLE partecipante ( partecipantepk serial NOT NULL, ­­ Chiave surrogata della tabella nome character varying(20), ­­ Nome del partecipante. cognome character varying(20), ts timestamp with time zone DEFAULT ('now'::text)::timestamp, partecipanteid character varying(30), CONSTRAINT partecipante_chiave_surrogata PRIMARY KEY (partecipantepk), CONSTRAINT partecipante_chiave_reale UNIQUE (partecipanteid) ) Java & PostgreSQL – Luca Ferrari 4 di 182 Database didattico CREATE TABLE j_corso_partecipante ( j_corso_partecipante_pk serial NOT NULL, ­­ Chiave surrogata corsopk integer NOT NULL, partecipantepk integer NOT NULL, CONSTRAINT j_corso_partecipante_chiave_surrogata PRIMARY KEY (j_corso_partecipante_pk), CONSTRAINT j_corso_partecipante_corsopk FOREIGN KEY (corsopk) REFERENCES corso (corsopk) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION, CONSTRAINT j_corso_partecipante_partecipantepk FOREIGN KEY (partecipantepk) REFERENCES partecipante (partecipantepk) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION ) Java & PostgreSQL – Luca Ferrari 5 di 182 JDBC: applicazioni client Java & PostgreSQL – Luca Ferrari 6 di 182 JDBC JDBC (Java DataBase Connectivity) è un'API a livello SQL, ossia consente di inglobare comandi SQL nelle proprie applicazioni Java. JDBC fornisce un'unica API CLI per l'accesso a database relazioni eterogenei. L'applicazione non necessita di adattamenti qualora il motore database cambi! Applica il famoso concetto “Write Once, Run Everywhere” alla connettività database. Si noti che l'idea non è nuovissima: ODBC si propone già come API unificata. Non è indispensabile utilizzare JDBC! Java & PostgreSQL – Luca Ferrari 7 di 182 Application & Driver APIs JDBC può essere divisa in due parti fondamentali: Application API e Driver API. Le Application API comprendono tutti gli elementi che un'applicazione Java usa per la connessione ad un database. Le Driver API forniscono la base per la scrittura di un driver specifico per un determinato motore relazionale (es. PostgreSQL). Grazie alla suddivisione in due parti, JDBC consente la scrittura di applicazioni portabili e database-independent. All'URL All'URL http://developers.sun.com/product/jdbc/drivers http://developers.sun.com/product/jdbc/drivers èèpossibile possibileverificare verificarel'esistenza l'esistenzadidiun undriver driverJDBC JDBC per ogni motore relazionale conosciuto. per ogni motore relazionale conosciuto. Java & PostgreSQL – Luca Ferrari 8 di 182 Application API vs Driver API Java Application Application ApplicationAPI API Driver DriverAPI API Database Engine Java & PostgreSQL – Luca Ferrari 9 di 182 Livelli JDBC Le Driver API riconoscono quattro modalità di implementazione differenti, chiamate livelli o type dei driver JDBC: type 1 (JDBC-ODBC bridge): viene sfruttato un accesso ODBC (che deve esistere!). ● type 2 (Native API Drivers): il driver richiama, tramite JNI, codice nativo (es. C/C++) per la connettività. ● type 3 (Network Drivers): il driver si collega (via TCP/IP) ad un componente lato server che si interfaccia a sua volta con il database server. ● type 4 (Pure Java): il driver si collega direttamente al database server e comunica utilizzando un protocollo opportuno. ● Java & PostgreSQL – Luca Ferrari 10 di 182 Livelli JDBC: schema riassuntivo JDBC Type 1 ODBC Driver JDBC Type 2 Native Driver Database Engine Java Application JDBC Type 3 Translator (e.g., local JDBC type 2) JDBC Type 4 Java & PostgreSQL – Luca Ferrari 11 di 182 JDBC API Java & PostgreSQL – Luca Ferrari 12 di 182 Classi principali JDBC Le principali classi/interfaccie della libreria JDBC (java.sql.* e javax.sql.*) sono: DriverManager: rappresenta un repository di istanze di Driver per ogni database server. Ci si rivolge a questa classe per creare una connessione ad uno specifico database (tramite un URL). Driver: rappresenta il componente incaricato di stabilire la connessione con il database server. Può essere caricato all'interno del programma (es. tramite reflection) o specificando la proprietà jdbc.drivers nel momento di invocazione della JVM: java ­Djdbc.drivers=org.postgresql.Driver myApp Java & PostgreSQL – Luca Ferrari 13 di 182 Classi principali JDBC (2) Connection: rappresenta una connessione al server database. Tramite di essa è possibile interagire con il database stesso. Statement: implementa una query SQL di qualunque tipo; viene usato per l'esecuzione di un comando sul database server. Si specializza in due sotto-interfaccie: PreparedStatement: una query SQL precompilata e dotata di wrapping dei parametri. CallableStatement: una query SQL che coinvolge una stored procedure. Java & PostgreSQL – Luca Ferrari 14 di 182 Classi principali JDBC (3) ResultSet: implementazione di un cursore SQL. Contiene un riferimento alla riga dei risultati dall'esecuzione di un comando SQL. Disponde di una serie di metodi per il posizionamento rispetto alle righe e per l'estrazione dei valori di ogni colonna. ResultSetMetaData: informazioni aggiuntive su un ResultSet, quali nomi e numero delle colonne, nome dello schema corrente, etc. DatabaseMetaData: fornisce informazioni aggiuntive sul database stesso, quali versione, stored procedures, foreign keys, etc. SQLException: classe base di ogni eccezione JDBC. Java & PostgreSQL – Luca Ferrari 15 di 182 Tipica applicazione JDBC Caricamento Caricamentodel del driver necessario driver necessario Istanziazione Driver Connessione Connessionealal database database (Connection) (Connection) Registrazione presso DriverMagaer [while ResultSet.next()] <INVIO> Lettura Letturadel del ResultSet ResultSet Simile all'esecuzione del comando psql -h host -U utente db Simile alla scrittura di una query sulla linea di comando di psql Esecuzione Esecuzionedello dello Statement Statement Creazione Creazionedidiuno uno Statement Statement Chiusra Chiusrarisorse risorseassociate associateaa ResultSet ResultSet ee Statement Statement Chiusura Chiusura connessione connessione (Connection) (Connection) Java & PostgreSQL – Luca Ferrari 16 di 182 Classi accessorie La libreria fornisce una serie di classi aggiuntive per il mapping fra i tipi SQL e i tipi Java (Types), la gestione di Large Object Data (Clob, Blob), date (Date), timestamp (Timestamp) e array (Array). Come estensioni sono presenti anche le classi per la gestione dei data source (DataSource), dei row set (RowSet) e dei relativi eventi (e.g., RowSetEvent, StatetementEvent). Java & PostgreSQL – Luca Ferrari 17 di 182 Connessione al database public static void main(String[] args) throws Exception{ try{ // classe del driver String driverName = "org.postgresql.Driver"; // url di connessione String databaseURL = "jdbc:postgresql://localhost/pgdaydb"; // caricamento della classe del driver Class driverClass = Class.forName(driverName); // creazione dell'istanza del driver Driver driver = (Driver) driverClass.newInstance(); Java & PostgreSQL – Luca Ferrari 18 di 182 Connessione al database // // // // a questo punto il driver è registrato presso il DriverManager (il driver di PostgreSQL si registra automaticamente al DriverManager appena la sua classe viene caricata) // essendo il driver caricato e registrato posso // ottenere una connessione al database Connection connection = DriverManager.getConnection(databaseURL, "luca", // username "xxx" // password ); // una volta ottenuta una connessione al database // e' possibile interagire con esso per via di // oggetti di tipo Statement Java & PostgreSQL – Luca Ferrari 19 di 182 Creazione di uno Statement // creazione di una query e di uno statement // da usare (la query mostra i corsi // che contengono java nella descrizione // e i relativi iscritti) String query = "SELECT c.corsoid, c.descrizione, c.data, c.n_ore, p.nome, p.cognome " + " FROM (corso c LEFT JOIN j_corso_partecipante j ON c.corsopk = j.corsopk) " + " LEFT JOIN partecipante p ON p.partecipantepk = j.partecipantepk " + " WHERE c.descrizione ilike '%java%' "; // notare che lo Statement non e' ancora associato // a nessuna query Statement statement = connection.createStatement(); // effettuo la query ResultSet rs = statement.executeQuery(query); Java & PostgreSQL – Luca Ferrari 20 di 182 Analisi dei risultati: ResultSet // visualizzo i dati ottenuti dalla query: l'oggetto // ResultSet (rs) rappresenta un cursore dei risultati // che può essere scorso. while( rs.next() ){ System.out.println("######"); // posso ottenere i dati di una riga chiamando i // metodi getXXX (XXX = tipo di dato) e specificando // il numero della colonna (partendo da 1) o il suo // nome simbolico System.out.println("Id-Corso e descrizione: " + rs.getString(1) + " " + rs.getString(2)); System.out.println("Data e numero ore: " + rs.getDate(3) + " " + rs.getInt(4)); System.out.println("Iscritto: " + rs.getString("nome") + " " + rs.getString("cognome")); } Java & PostgreSQL – Luca Ferrari 21 di 182 Fine del programma e gestione delle eccezioni // chiusura della connessione connection.close(); } }catch(SQLException exception){ // eccezione SQL (problema nella query, errore del // server, etc.) }catch(ClassNotFoundException ex){ // classe del driver JDBC non trovata }catch(InstantiationException ex){ // errore di reflection (classe driver non // istanziabile) }catch(IllegalAccessException ex){ // errore di reflection (classe driver non // accessibile) } } Java & PostgreSQL – Luca Ferrari 22 di 182 Esecuzione Riassunto: ● Caricamento del driver PostgreSQL (necessario solo all'avvio del thread principale dell'applicazione); ● Creazione di una connessione specificando l'URL di connessione; ● Creazione di uno Statement; ● Esecuzione dello Statement; ● Lettura dei risultati tramite il ResultSet; ● Chiusura delle risorse. Java & PostgreSQL – Luca Ferrari 23 di 182 Come fa il DriverManager a scegliere il Driver opportuno? Il DriverManager stabilisce quale sia il Driver da utilizzare per una connessione sulla base dell'URL di connessione (ogni driver accetta il proprio subprotocol): protocol:subprotocol://HOSTNAME/DB_NAME HOSTNAME: specifica l'indirizzo IP/nome dell'host ove è in esecuzione il database. DB_NAME: nome del database cui ci si intende connettere. jdbc:postgres://localhost/pgdaydb protocol: specifica il dominio di appartenenza di questo URL. Indica che l'URL si riferisce ad una connessione database (JDBC). subprotocol: specifica il driver che si deve utilizzare. Viene utilizzato da DriverManager per fare il lookup dell'istanza di Driver da restituire per gestire la connessione al database. Java & PostgreSQL – Luca Ferrari 24 di 182 Considerazioni sul caricamento del driver Il caricamento e l'istanziazione del driver JDBC può avvenire attraverso reflection o direttamente: // reflection Class driverClass = Class.forName(“org.postgresql.Driver”); Driver driverInstance = driverClass.newInstance(); // istanziazione diretta Driver driverInstance = new org.postgresql.Driver(); I driver hanno sempre un costruttore di default senza argomenti, I driver hanno sempre un costruttore di default senza argomenti, e quindi sono sempre istanziabili mediante reflection. e quindi sono sempre istanziabili mediante reflection. Java & PostgreSQL – Luca Ferrari 25 di 182 Considerazioni sul ResultSet Un ResultSet rappresenta un cursore sui risultati di una query. Il posizionamento fra le righe del cursore avviene tramite metodi specifici, quali next() e previous(). Ogni ResultSet è legato allo Statement che lo ha generato; se lo Statement viene alterato il ResultSet è automaticamente invalidato! Esistono metodi per recuperare il valore di ogni colonna secondo il tipo di appartenenza (String, int, Date, ...): <tipo_Java> get<tipo_Java>(int colonna) <tipo_Java>get<tipo_Java>(String nomeColonna) Ogni tipo SQL viene mappato in un tipo Java (e viceversa) mediante una specifica tabella di conversione (http://java.sun.com/j2se/1.3/docs/guide/jdbc/getstart/mapping. html). Tale tabella può essere estesa dall'utente per mappare i tipi user defined. SiSipresti prestiattenzione attenzionealalfatto fattoche, che,contrariamente contrariamentealla allalogica logica degli array, le colonne sono numerate partendo da 1! degli array, le colonne sono numerate partendo da 1! Java & PostgreSQL – Luca Ferrari 26 di 182 ResultSet: errori frequenti Si richiama getXXX(..) con un indice di colonna errato (maggiore) rispetto al numero di colonne estratte dalla query. Si richiama getXXX(..) prima di aver chiamato almeno una volta next(). Java & PostgreSQL – Luca Ferrari 27 di 182 Esecuzione di un INSERT/UPDATE/DELETE // creazione di una query e di uno statement // da usare per inserire un nuovo corso String query = "INSERT INTO corso(corsoid,descrizione) VALUES('Java_adv', 'Corso avanzato su Java/JDBC') "; Statement statement = connection.createStatement(); // chiedo al server di eseguire la query e // di fornirmi il numero di righe "toccate" // tutte le query che modificano i dati vanno eseguite // attraverso il metodo executeUpdate(..) int rows = statement.executeUpdate(query); if( rows > 0) System.out.println("Inserimento effettuato con successo:" + rows + " righe inserite"); // chiusura della connessione connection.close(); Java & PostgreSQL – Luca Ferrari 28 di 182 Esecuzione Riassunto: ● Caricamento del driver PostgreSQL (necessario solo all'avvio del thread principale dell'applicazione); ● Creazione di una connessione specificando l'URL di connessione; ● Creazione di uno Statement; ● Esecuzione dello Statement; ● Lettura del valore di ritorno dell'esecuzione dello Statement, tale valore indica il numero di righe toccate dalla query; ● Chiusura delle risorse. Java & PostgreSQL – Luca Ferrari 29 di 182 Compilazione del driver Java & PostgreSQL – Luca Ferrari 30 di 182 Driver JDBC per PostgreSQL PostgreSQL fornisce un driver JDBC conforme ai livelli 2,3 e 4. Il sito web del progetto è http://jdbc.postgresql.org, dal quale è possibile scaricare il driver (in formato binario o sorgente), navigare la documentazione Javadoc della parte public delle API. E' presente una mailing list specifica: pgsql­[email protected]; gli attuali mantainer del Driver sono Dave Cramer, Kris Jurka, Oliver Jowett. Java & PostgreSQL – Luca Ferrari 31 di 182 Decompressione dei sorgenti I sorgenti vengono rilasciati sotto forma di archivio compresso (tarball): postgresql­jdbc­8.2­504.src.tgz E' possibile decomprire programma tar: i sorgenti usando il tar xzvf postgresql­jdbc­8.2­504.src.tgz ottenendo una directory che contiene l'albero dei sorgenti ls ­l postgresql­jdbc­8.2­50 ­rw­r­­r­­ 1 luca luca 259 2006­12­01 13:02 build.properties ­rw­r­­r­­ 1 luca luca 21469 2006­11­29 05:00 build.xml drwxr­xr­x 2 luca luca 4096 2006­12­01 13:22 doc ­rw­r­­r­­ 1 luca luca 1542 2005­01­11 09:25 LICENSE drwxr­xr­x 3 luca luca 4096 2006­12­01 13:22 org ­rw­r­­r­­ 1 luca luca 3769 2004­10­23 00:24 README ­rwxr­xr­x 1 luca luca 495 2004­11­07 23:15 update­translations.sh Java & PostgreSQL – Luca Ferrari 32 di 182 Compilazione: requisiti Per compilare il driver JDBC è necessario utilizzare Apache Ant (http://ant.apache.org), uno strumento di compilazione automatico per Java. Ant è uno strumento concettualmente simile a make, ma ottimizzato per la gestione dei progetti Java. Esso stesso è scritto in Java, e questo ne facilita la fruibilità nello sviluppo di componenti Java. Ant necessita della presenza di un file di build (solitamente build.xml) che contiene i task del processo di compilazione. E' possibile specificare proprietà di compilazione mediante file di proprietà Java (file properties). Java & PostgreSQL – Luca Ferrari 33 di 182 Compilazione: build Il file di build del driver è build.xml, e può essere omesso nell'invocazione di ant: [luca@fluca:/sviluppo/java/contrib­src/postgresql­jdbc­8.2­504.src]$ ant Buildfile: build.xml all: prepare: check_versions: check_driver: driver: compile: [javac] Compiling 144 source files to /sviluppo/java/contrib­src/postgresql­jdbc­8.2­ 504.src/build [javac] Note: Some input files use or override a deprecated API. [javac] Note: Recompile with ­Xlint:deprecation for details. [javac] Note: Some input files use unchecked or unsafe operations. [javac] Note: Recompile with ­Xlint:unchecked for details. jar: [jar] Building jar: /sviluppo/java/contrib­src/postgresql­jdbc­8.2­ 504.src/jars/postgresql.jar BUILD SUCCESSFUL Total time: 5 seconds [luca@fluca:/sviluppo/java/contrib­src/postgresql­jdbc­8.2­504.src]$ Java & PostgreSQL – Luca Ferrari 34 di 182 Compilazione: risultato Al termine del processo di compilazione viene generato un file postgresql.jar nella sottodirectory jars: [luca@fluca:/sviluppo/java/contrib­src/postgresql­jdbc­8.2­504.src]$ ls ­l jars/ total 436 ­rw­r­­r­­ 1 luca luca 441337 2007­03­31 11:22 postgresql.jar [luca@fluca:/sviluppo/java/contrib­src/postgresql­jdbc­8.2­504.src]$ Il file jar prodotto deve essere incluso nel classpath corrente affinché i programmi Java possano caricare il driver. $ export CLASSPATH=`pwd`/jars/postgresql.jar:$CLASSPATH $ echo $CLASSPATH /sviluppo/java/contrib­src/postgresql­jdbc­8.2­504.src/jars/postgresql.jar: $ Java & PostgreSQL – Luca Ferrari 35 di 182 Compilazione: risultato (2) Un altro file importante viene generato: Driver.java $ ls ­l org/postgresql/Driver.java* ­rw­r­­r­­ 1 luca luca 29517 2007­03­31 11:21 org/postgresql/Driver.java ­rw­r­­r­­ 1 luca luca 29523 2005­11­24 07:18 org/postgresql/Driver.java.in $ Prima della compilazione infatti il solo file Driver.java.in è presente. Il file Driver.java viene generato dinamicamente durante il processo di build, a seconda del livello JDBC del driver. Java & PostgreSQL – Luca Ferrari 36 di 182 Compilazione in Eclipse Definire un nuovo progetto basato su un file Ant esistente. (1) (2) Selezionare il file di build dalla directory dei sorgenti; specificare le proprietà del progetto. Java & PostgreSQL – Luca Ferrari 37 di 182 Compilazione in Eclipse (2) (3) Ricercare il file build.xml tramite il modulo Ant di Eclipse. Java & PostgreSQL – Luca Ferrari 38 di 182 Compilazione in Eclipse (3) (4) Eseguendo il file di build il progetto viene compilato. Java & PostgreSQL – Luca Ferrari 39 di 182 JDBC 2, 3 o 4? Il driver JDBC di Postgresql supporta solo i livelli JDBC 2,3 e 4. L'albero dei sorgenti include i package per ogni versione JDBC. $ ls org/postgresql/ ­l ... drwxr­xr­x 3 luca luca 4096 2006­12­01 13:23 jdbc2 drwxr­xr­x 2 luca luca 4096 2006­12­01 13:23 jdbc3 drwxr­xr­x 2 luca luca 4096 2006­12­01 13:23 jdbc4 ... $ La decisione su quale livello di driver (org.postgresql.Driver) viene presa durante il processo di build in base alla versione della JVM. In particolare la versione viene stabilita sulla base della Java Virtual Machine installata sul sistema. Java & PostgreSQL – Luca Ferrari 40 di 182 Driver.java.in Il file org.postgresql.Driver.java.in è un file che viene tramutato nel driver Java vero e proprio durante il processo di build Ant. Al suo interno il file Driver.java.in contiene dei segnaposti per le classi JDBC del livello opportuno. Java & PostgreSQL – Luca Ferrari 41 di 182 Scelta del livello JDBC Il file contiene build.xml un target, check_versions, che controlla quale versione di JVM sia disponibile e, di conseguenza, quale versione di driver sia la più appropriata. A seconda del livello stabilito viene impostata a true la proprietà jdbcX. Java & PostgreSQL – Luca Ferrari 42 di 182 Scrittura del livello JDBC Il target driver si occupa della scrittura del file Driver sostituendo i marcaposto al suo interno con le classi del livello JDBC scelto. Si noti che viene utilizzata la funzionalità di filtro (filter) di Ant per la sostituzione dei marcaposto (token) con le classi concrete. Diverse altre classi sono specificate con lo stesso procedimento. Java & PostgreSQL – Luca Ferrari 43 di 182 Scrittura del livello JDBC (2) Viene fatto un controllo sulla proprietà jdbcX, impostata precedentemente dal target check_versions. (1) Java & PostgreSQL – Luca Ferrari 44 di 182 Scrittura del livello JDBC (3) (2) Vengono impostati i token marcaposto da sostituire nel file Driver.java.in; ogni token specificato in build.xml trova corrispondenza con @token@ in Driver.java.in Java & PostgreSQL – Luca Ferrari 45 di 182 Scrittura del livello JDBC (4) (3) Il file Driver.java.in viene copiato in Driver.java applicando le condizioni di filtro. Java & PostgreSQL – Luca Ferrari 46 di 182 JDBC: tipi di Statement Java & PostgreSQL – Luca Ferrari 47 di 182 PreparedStatement Un PreparedStatement è un oggetto che memorizza uno statement SQL precompilato, che può essere parametrizzato e che può eseguire più efficientemente di uno Statement normale. L'idea è di precompilare un comando SQL parziale e di completarlo successivamente, al momento in cui l'esecuzione sia necessaria. Lo statement precompilato può poi essere riutilizzato più volte, cambiando eventualmente i parametri di esecuzione. Un PreparedStatement è una sottointerfaccia di Statement! Essendo Essendouno unostatement statementparametrizzabile, parametrizzabile,PreparedStatement PreparedStatementfornisce forniscedei deimetodi metodi setXXX setXXXeegetXXX getXXXper perimpostare impostarei iparametri parametrididiesecuzione. esecuzione. Java & PostgreSQL – Luca Ferrari 48 di 182 Prepared Statement // creazione di una query di inserimento e dello // statement precompilato per la sua esecuzione String query = "INSERT INTO corso(corsoid,descrizione) VALUES(?,?) "; PreparedStatement statement = connection.prepareStatement(query); // inserisco alcuni corsi fornendo gli opportuni // parametri al server int rows = 0; for(int i = 0; i < 10; i++){ String corsoid = "corso_" + i; String descrizione = "Nessuna descrizione "; // impostazione dei parametri nello statement statement.setString(1, corsoid); statement.setString(2, descrizione); // eseguo lo statement (non devo specificare una query // poiché lo statement è già stato precompilato) rows += statement.executeUpdate(); } Java & PostgreSQL – Luca Ferrari 49 di 182 Considerazioni: PreparedStatement Un PreparedStatement utilizza una sintassi speciale per le query SQL: ogni carattere ? viene usato come un marcatore per un parametro che verrà specificato in seguito. In maniera duale ad un ResultSet vengono forniti dei metodi setXXX(numpar, value). Il vantaggio dei PreparedStatement è che possono essere memorizzati e precompilati dal database server, aumentado le prestazioni nel caso di cicli (l'ottimizzatore potrebbe però stabilire piani diversi a seconda della selettività dei parametri). Si Sipresti prestiattenzione attenzionealalfatto fattoche che i iparametri parametrisono sononumerati numeratipartendo partendoda da1!1! Java & PostgreSQL – Luca Ferrari 50 di 182 Statement vs PreparedStatement Statement: PreparedStatement: ✔ query statiche o poco variabili; ✔ query parametriche; ✔ nessuna necessità escaping dei parametri. ✔ query base da eseguire ciclicamente a seconda di parametri run-time; di ✔ escaping di parametri usati nella query stessa. L'utilizzo di PreparedStatement è preferibile in molte situazioni! Java & PostgreSQL – Luca Ferrari 51 di 182 CallableStatement L'interfaccia CallableStatement viene usata in maniera analoga ai PreparedStatement per l'invocazione di una Stored Procedure. La sintassi utilizzata per l'invocazione è la seguente: { call stored_procedure(?,?,...) } Viene specificato il nome della stored procedure da invocare, seguito dalla lista di eventuali parametri. La funzione viene invocata con il metodo executeQuery() che restituisce un ResultSet. Java & PostgreSQL – Luca Ferrari 52 di 182 JDBC: Utilizzo avanzato Java & PostgreSQL – Luca Ferrari 53 di 182 Esecuzione di comandi arbitrari Si supponga di voler creare una vista definita come segue: CREATE VIEW vw_partecipanti AS SELECT c.corsoID, c.descrizione, c.data, p.cognome, p.nome FROM partecipante p JOIN corso c ON c.corsoPK = p.corsoPK ORDER BY c.corsoID, p.cognome, p.nome Come si può notare l'esecuzione della query non produce alcun risultato (in termine di numero di righe modificate) pur alterando la struttura del database. Java & PostgreSQL – Luca Ferrari 54 di 182 Esecuzione di comandi arbitrari // creazione di uno statement Statement statement = connection.createStatement(); // preparazione della query String sql = "CREATE VIEW vw_partecipanti AS SELECT c.corsoID, c.descrizione, c.data, p.cognome, p.nome FROM partecipante p JOIN corso c ON c.corsoPK = p.corsoPK ORDER BY c.corsoID, p.cognome, p.nome"; int result = statement.executeUpdate(sql); System.out.println("Risultato di esecuzione: " + result); Java & PostgreSQL – Luca Ferrari 55 di 182 JDBC: creazione della procedura // creazione di uno statement Statement statement = connection.createStatement(); // preparazione della funzione StringBuffer buffer = new StringBuffer(1000); buffer.append( "CREATE OR REPLACE FUNCTION ...”); buffer.append(" RETURNS void AS $BODY$ DECLARE ...”); // chiamata della stored procedure int result = statement.executeUpdate(buffer.toString()); System.out.println("Risultato della creazione della procedura: " + result); Java & PostgreSQL – Luca Ferrari 56 di 182 JDBC: invocazione della procedura // invocazione della funzione String sql = "{ call f_nuovo_corso(?,?,?) }"; // preparazione della chiamata e impostazione dei parametri CallableStatement cStatement = connection.prepareCall(sql); cStatement.setInt(1, 2); cStatement.setString(2, "Corso2009"); Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.DAY_OF_MONTH, 7); calendar.set(Calendar.MONTH, 7); calendar.set(Calendar.YEAR, 2009); cStatement.setDate(3, new java.sql.Date(calendar.getTimeInMillis()) ); // invocazione della funzione ResultSet resultSet = cStatement.executeQuery(); // in questo caso non ho risultati.... Java & PostgreSQL – Luca Ferrari 57 di 182 CallableStatement L'interfaccia CallableStatement viene usata in maniera analoga ai PreparedStatement per l'invocazione di una Stored Procedure. La sintassi utilizzata per l'invocazione è la seguente: { call stored_procedure(?,?,...) } { ?= stored_procedure(?,?,...) } Viene specificato il nome della stored procedure da invocare, seguito dalla lista di eventuali parametri. L'invocazione avviene tramite il metodo executeQuery() che restituisce un ResultSet. E' possibile registrare parametri di input/output e ottenerne il valore direttamente dallo statement. Java & PostgreSQL – Luca Ferrari 58 di 182 Batch L'interfaccia Statement consente l'esecuzione batch di una serie di comandi SQL: public void addBatch(String sql) mediante tale metodo è possibile assegnare una serie di istruzioni SQL da eseguire sequenzialmente in un momento successivo. L'esecuzione del gruppo di comandi avviene mediante il metodo executeBatch(), che restituisce i singoli valori di ritorno dell'esecuzione di ogni comando. public int[] executeBatch() Java & PostgreSQL – Luca Ferrari 59 di 182 Batch: un primo esempio // creo uno statement Statement statement = connetion.createStatement(); // imposto i comandi da eseguire nel batch String sql = “INSERT INTO ...”; // aggiungo questa istruzione al batch statement.addBatch(sql); // nuova istruzione da aggiungere al batch sql = “INSERT INTO ...”; statement.addBatch(sql); ... // eseguo il batch int result[] = statement.executeBatch(); // controllo i risultati di ogni comando for(int i=0; i<result.length; i++) System.out.println(“Comando “+(i+1)+” risultato “+ result[i]); Java & PostgreSQL – Luca Ferrari 60 di 182 Batch & PreparedStatement L'utilizzo dei batch agevola i casi in cui siano presenti cicli. E' possibile utilizzare oggetti PreparedStatement al fine di unire i vantaggi di un'esecuzione batch e della parametrizzazione dei comandi SQL. // query parametri String sql = “UPDATE corso SET descrizione=? WHERE corsoID=?”; PreparedStatement statement = connection.prepareStatement(sql); for(int i=0; i< 10; i++){ statement.setString(1, descrizione[i]); statement.setString(2, id[i]); statement.addBatch(); } // esecuzione int result[] = statement.executeBacth(); Java & PostgreSQL – Luca Ferrari 61 di 182 Batch: considerazioni Solitamente l'utilizzo di un batch risulta più efficiente che l'esecuzione delle singole operazioni in modo separato (ottimizzazione da parte del driver). Si presti attenzione al fatto che un batch è differente da una transazione, tuttavia può essere usato per rendere più compatto (e quindi leggibile) il codice di una transazione. Java & PostgreSQL – Luca Ferrari 62 di 182 Scrollable ResultSet Uno scrollable ResultSet è un tipo particolare ResultSet che offre una funzionalità di scrolling. di E' possibile muoversi attraverso le righe in avanti e indietro, saltare righe, posizionarsi in modo relativo o assoluto, etc. Il tipo di scrolling deve essere specificato a livello di creazione dello Statement, specificando il tipo di scrolling (e di aggiornabilità) come parametri del metodo createStatement(..), in modo da consentire al database di gestire le risorse. IlIldriver driverPostgreSQL PostgreSQLutilizza utilizzainternamente internamenteun unVector Vectorper per contenere contenereleletuple tupledel delResultSet; ResultSet;mediante medianteun unindice indice riesce a garantire lo scrolling di tali tuple. riesce a garantire lo scrolling di tali tuple. Java & PostgreSQL – Luca Ferrari 63 di 182 Tipo di scrolling Il tipo di un ResultSet può essere specificato tramite tre valori predefiniti: ResultSet.TYPE_FORWARD_ONLY (1003) è il classico ResultSet che può essere consultato solo in avanti. ResultSet.TYPE_SCROLL_INSENSITIVE (1004) può essere consultato in entrambe le direzioni ma non riflette cambiamenti fatti al database mentre è aperto. ResultSet.TYPE_SCROLL_SENSITIVE (1005) può essere consultato in entrambe le direzioni e riflette ogni cambiamento apportato. Il tipo di scrolling può essere ricavato a tempo di esecuzione tramite il metodo int ResultSet.getType(); Java & PostgreSQL – Luca Ferrari 64 di 182 Posizionamento all'interno del ResultSet L'interfaccia ResultSet mette a disposizione diversi metodi di posizionamento: beforeFirst(), afertLast() restituiscono true se si ci si trova oltre la prima o l'ultima riga. next(), previous() muovono di una riga in avanti o indietro il cursore. absolute(int), relative(int) spostano il cursore alla riga specificata in modo assoluto o relativo (dalla posizione corrente). Un indice negativo considera lo spostamento all'indietro (es. -1 sposta all'ultima riga nel caso di posizionamento assoluto o alla riga precedente nel caso di posizionamento relativo). getRow() ritorna il numero di riga (valore assoluto) a cui si è posizionati correntemente. Java & PostgreSQL – Luca Ferrari 65 di 182 Esempio di scrollable ResultSet // ottengo il result set di una query ResultSet rs = statement.executeQuery(); while( rs.next() ){ // esecuzione in avanti (solo righe pari) int numero_riga = rs.getRow(); if( (numero_riga % 2) == 0 ){ System.out.println("######"); System.out.println("Id-Corso e descrizione: " + rs.getString(1) + " " + rs.getString(2)); } } // torno indietro e processo solo le righe dispari while( rs.previous() ){ int numero_riga = rs.getRow(); } if( (numero_riga % 2) != 0 ){...} Java & PostgreSQL – Luca Ferrari 66 di 182 ResultSet aggiornabili JDBC v2 permette di aggiornare (inserire, modificare, cancellare) le righe di una query usando direttamente il ResultSet corrispondente. In sostanza non si è obbligati a creare un comando di update/insert ma si può modificare il valore di una colonna direttamente operando sui risultati di una query. E' un tentativo di nascondere i dettagli SQL e di rendere il sistema più Object-Oriented. Java & PostgreSQL – Luca Ferrari 67 di 182 ResultSet aggiornabili (2) Per rendere tutto ciò possibile l'interfaccia ResultSet include una serie di metodi simili a quelli per la definizione dei parametri di un PreparedStatement. Ad esempio: updateString(int column, String value); updateInt(int column, int value); ... Esistono poi dei metodi speciali per aggiornare/inserire/cancellare e aggiornare i valori della riga corrente: updateRow(); insertRow(); deleteRow(); refresh(); Java & PostgreSQL – Luca Ferrari 68 di 182 Tipo di aggiornabilità Il tipo di un ResultSet, relativamente alla sua aggiornabilità, può essere specificato tramite due valori predefiniti: ResultSet.CONCUR_READ_ONLY (1007) è ResultSet che non può essere aggiornato. il classico ResultSet.CONCUR_UPDATABLE (1008) può essere aggiornato e quindi supporta inserimento, modifica e cancellazione di righe. Il tipo di aggiornabilità può essere ricavato a tempo di esecuzione tramite il metodo int ResultSet.getType(); Java & PostgreSQL – Luca Ferrari 69 di 182 Inserimento di una riga L'inserimento di una riga in un ResultSet è un'operazione leggermente più complessa rispetto all'aggiornamento. Occorre infatti istruire il ResultSet affinché prepari posto per i valori della riga che si vuole inserire, assegnare tali valori e confermare (salvare) l'inserimento della nuova riga. Se il ResultSet è di tipo scroll sensitive la riga inserita sarà visibile senza bisogno di aggiornamenti del ResultSet stesso. Java & PostgreSQL – Luca Ferrari 70 di 182 Inserimento di una riga: un approccio visuale La procedura di inserimento di una nuova riga è simile alla procedura usata in molti programmi visuali di accesso e modifica dei database: (1) viene ottenuto il ResultSet relativo ad una specifica query; (2) si passa oltre l'ultima riga, usando una riga vuota come spazio temporaneo per l'inserimento dei valori. Si usa il metodo moveToInsertRow(); (3) vengono salvati i valori inseriti. Si usa il metodo insertRow(). Java & PostgreSQL – Luca Ferrari 71 di 182 Inserimento di una riga in un ResultSet // processo il result set ed inserisco una nuova riga (max 4) // ad ogni riga pari (non significa DOPO ogni riga pari!) while (rs != null && rs.next()) { int numero_riga = rs.getRow(); if( ((numero_riga % 2) == 0) && inserite < 4 ){ // inserisco una nuova riga in fondo al result set rs.moveToInsertRow(); rs.updateString(1, "CorsoRS" + numero_riga); rs.updateString(2, "Prova di inserimento da ResultSet"); rs.insertRow(); // torno alla riga cui ero prima dell'inserimento rs.moveToCurrentRow(); inserite++; } String corsoID = rs.getString(1); String descrizione = rs.getString(2); System.out.println("Riga numero " + numero_riga + " ­ " + corsoID + " = " + descrizione); } Java & PostgreSQL – Luca Ferrari 72 di 182 JDBC: Transazioni Java & PostgreSQL – Luca Ferrari 73 di 182 JDBC e transazioni Il driver JDBC lavora in modalità auto-commit: ogni istruzione eseguita tramite uno Statement viene automaticamente confermata. E' come se si eseguissero transazioni monocomando. Per prendere il controllo sulla transazione e segnarne l'inizio e la fine (BEGIN-END) è necessario disabilitare l'auto-commit del driver e forzare un esplicito commit/rollback. Java & PostgreSQL – Luca Ferrari 74 di 182 Schema di funzionamento // disabilito auto commit connection.setAutoCommit(false); // effettuo del lavoro, INSERT, UPDATE, ecc. e tengo // traccia se devo fare un rollback ... rollback = true; // tutto fatto if( ! rollback ) connection.commit(); else connection.rollback(); connection.setAutoCommit(true); // ripristino auto-commit Java & PostgreSQL – Luca Ferrari 75 di 182 Transazioni: livello di isolamento E' possibile impostare il livello di isolamento di una transazione. I livelli di isolamento sono memorizzati come costanti (interi) nell'oggetto Connection e possono valere: Connection.TRANSACTION_READ_COMMITTED impedisce che ci siano dirty-reads, ma consente unrepeatable e phantom reads; Connection.TRANSACTION_READ_UNCOMMITTED non consente nessun tipo di dirty, unrepeatable e phantom reads; Connection.TRANSACTION_REPEATABLE_READ consente solo phantom reads; Connection.TRANSACTION_SERIALIZABLE sono serializzabili. Java & PostgreSQL – Luca Ferrari le transazioni 76 di 182 Transazioni: riassunto & considerazioni L'inizio di una transazione deve essere eplicitato a livello di connessione (Connection) disabilitando la modalità di auto-commit. Tutte le operazioni eseguite tramite uno Statement dopo aver disabilitato l'auto-commit fanno parte della transazione. La fine di una transazione deve forzare un esplicito commit o rollback sulla connessione. Una volta terminata la transazione è necessario riabilitare l'auto-commit, altrimenti le istruzioni succesive faranno parte di una nuova transazione (il driver non ri-abilita da solo l'auto-commit!). Java & PostgreSQL – Luca Ferrari 77 di 182 JDBC: DataSource Java & PostgreSQL – Luca Ferrari 78 di 182 DataSource Le API JDBC 2 introducono nel package javax.sql l'interfaccia DataSource che si comporta come un wrapper attorno alla connettività di un un database. In sostanza un DataSource è un'oggetto che contiene tutte le informazioni che servono per la connettività verso la sorgente dati (database) quali URL, username, password, auto-commit mode, etc. Lo scopo dei DataSource è quello di disaccoppiare l'impostazione dei parametri di connettività (tipicamente amministrativi) dal loro utilizzo (tipicamente applicativo). Diverse applicazioni possono condividere la stessa DataSource (ad esempio tramite JNDI); una modifica nel DataSource (ad esempio modifica all'URL) non necessita nessun intervento sulle applicazioni che lo utilizzano. Java & PostgreSQL – Luca Ferrari 79 di 182 ConnectionPoolDataSource Un tipo particolare di sorgente dati è il ConnectionPoolDataSource che consente di gestire le connessioni in pool. L'idea è quella di mantenere le connessioni al database in una cache, in modo da renderle disponibili a diversi componenti man mano che vengono richieste. Non venendo aperte/chiuse di continuo, le connessioni vengono erogate con tempi di risposta più bassi rispetto all'approccio classico. L'utilizzo delle sorgenti dati con pool richiede però qualche passaggio in più rispetto all'uso delle sorgenti dati semplici. Viene comunque garantito il disaccoppiamento fra la parte amministrativa e quella applicativa. Java & PostgreSQL – Luca Ferrari 80 di 182 DataSource & PostgreSQL I driver JDBC di PostgreSQL mettono a disposizione due tipi principali di DataSource (contenute nel package org.postgresql.ds): PGSimpleDataSource un DataSource senza alcun tipo di pooling. Implementa javax.sql.DataSource e dispone di un metodo getConnection() che fornisce la connessione al database. Ad esempio: DataSource dataSource = new PGSimpleDataSource(); ... Connection connection = dataSource.getConnection(); Java & PostgreSQL – Luca Ferrari 81 di 182 DataSource & PostgreSQL PGPoolingDataSource un DataSource con capacità di pooling delle connessioni. Implementa javax.sql.ConnectionPoolDataSource e mette a disposizione un metodo getPooledConnection() che fornisce un oggetto javax.sql.PooledConnection, al quale si può richiedere la connessione tramite getConnection(). Ad esempio: ConnectionPoolDataSource dataSource = new PGConnectionPoolDataSource(); ... PooledConnection pooledConnection = dataSource.getPooledConnection(); Connection connection = pooledConnection.getConnection(); Java & PostgreSQL – Luca Ferrari 82 di 182 Impostazione delle proprietà di una sorgente dati Le proprietà delle sorgenti dati vengono impostati tramite metodi set (e lette dai relativi metodi get secondo le specifiche Java Beans). Solitamente gli ambienti server mettono a disposizione dei file di configurazione (es. XML) che consentono di specificare le varie proprietà. L'ambiente container (es. Tomcat) si farà poi carico di leggere tale file di configurazione e di impostare le proprietà di connessione nella sorgente dati. serverName Indirizzo IP o nome del server database databaseName Nome del database cui collegarsi portNumber Porta a cui collegarsi tramite TCP/IP user Utente da usare per la connessione password Password per l'utente di cui sopra initialConnections Numero di connessioni da creare all'avvio maxConnections Massimo numero di connessioni istanziabili Java & PostgreSQL – Luca Ferrari 83 di 182 Esempio di uso di DataSource // creazione di un data source per PostgreSQL PGConnectionPoolDataSource dataSource = new org.postgresql.ds.PGConnectionPoolDataSource(); // impostazione dei parametri di connessione dataSource.setDatabaseName("pgdaydb"); dataSource.setUser("luca"); dataSource.setPassword(null); dataSource.setServerName("localhost"); dataSource.setDefaultAutoCommit(true); // ora il datasource andrebbe esportato e reso disponibile // ad altre applicazioni, ad esempio tramite JNDI ... // prelevo la sorgenti dati ConnectionPoolDataSource source = (ConnectionPoolDataSource) dataSource; // oppure // PooledConnection pooledConnection = // source.getPooledConnection(); Java & PostgreSQL – Luca Ferrari 84 di 182 Spring La libreria Spring fornisce un sottoinsieme di classi di utilità per la connessione a database e la realizzazione di DAO. <!­­ il template jdbc che deve essere inizializzato con una datasource ­­> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <constructor­arg> <ref bean="datasource" /> </constructor­arg> </bean> <!­­ inmpostazione della datasource ­­> <bean id="datasource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="org.postgresql.Driver" /> <property name="url" value="jdbc:postgresql://192.168.1.2/hrpmdb" /> <property name="username" value="hrpm" /> <property name="password" value="HrPm" /> </bean> Java & PostgreSQL – Luca Ferrari 85 di 182 Spring lato applicativo Lato applicativo occorre ottenere il jdbctemplate e usare uno dei suoi metodi per effettuare le query. // insert the address in the database this.jdbcTemplate.update( this.getInsertQuery(), toInsert.getPrimaryKey(), toInsert.getStreet(), toInsert.getCity(), toInsert.getCountry(), toInsert.getState(), toInsert.getPhone(), toInsert.getZipCode(), toInsert.getFax() ); Java & PostgreSQL – Luca Ferrari 86 di 182 Java Transaction API Java & PostgreSQL – Luca Ferrari 87 di 182 Java Transaction API (JTA) La Java Transaction API (JTA) è un insieme di interfaccie e classi che forniscono una astrazione verso un sistema di transazioni distribuite. Una transazione distribuita è una transazione che coinvolge più risorse (database) allo stesso momento. La necessità di mantenere coerenti le risorse (database) richiede protocolli appositi per la decisione di commit/rollback globale. La JTA si appoggia su una implementazione specifica del server, chiamata Java Transaction Service (JTS). Il JTS è responsabile di implementare il Transaction Manager, che è il componente che prende la decisione globale sul commit/rollback. Java & PostgreSQL – Luca Ferrari 88 di 182 Transazioni distribuite in Java Java Application User UserTransaction Transaction Interface Interface Transaction TransactionManager Manager JDBC JDBCDriver Driver Application Server Database Engine Java & PostgreSQL – Luca Ferrari Database Engine 89 di 182 Transazioni distribuite in Java L'interfaccia javax.transaction.UserTransaction consente di definire i confini di una transazione distribuita, ossia quale sezione di codice debba essere eseguito come transazione. La sua implementazione dipende dal container che si sta utilizzando. L'interfaccia javax.transaction.TransactionManager consente la gestione delle risorse di transazione al container. L'interfaccia javax.transaction.xa.XAResource rappresenta una risorsa XA da gestire. Java & PostgreSQL – Luca Ferrari 90 di 182 Transazioni distribuite: Commit a due fasi Il protocollo di commit a due fasi (Two Phase Commit) è un algoritmo distribuito che consente a tutti i nodi che partecipano alla transazione di accordarsi all'unisono per un commit o un rollback. (fase 1: pre-commit) Il coordinatore invia un query-to-commit a tutti i partecipanti. Ogni partecipante effettua le operazioni, aggiorna i propri log (undo e redo) e invia un messaggio di commit o rollback al coordinatore. (fase 2: commit) Quando il coordinatore ha tutti i messaggi dei partecipanti prende la decisione: se tutti hanno inviato un commit il coordinatore conferma il commit ai partecipanti che rendono permanenti le modifiche e inviano un acknowledgement. Il coordinatore chiude la transazione quando riceve tutti gli acknoweldgement. se almeno un partecipante ha inviato un rollback il coordinatore invia un rollback ai partecipanti, che annullano la loro transazione locale e confermano il rollback al coordinatore. Java & PostgreSQL – Luca Ferrari 91 di 182 Risorse X/Open CAE X/Open CAE specifica gli oggetti e le interfaccie da usare nelle transazioni distribuite (Distributed Transaction Processing: the XA specification): definisce cosa un gestore di risorse debba fare per supportare le transazioni distribuite. Il supporto XA del driver è fondamentale per consentire l'uso con transazioni distribuite. Il driver JDBC di PostgreSQL supporta XA dalla versione 8.1, anche se senza supporto per il transaction interleaving (capacità di usare la stessa connessione per differenti transazioni contemporanee). Java & PostgreSQL – Luca Ferrari 92 di 182 JTA & Driver JDBC Nonostante il Transaction Manager sia il componente più importante, il driver JDBC deve fornire supporto a XADatasource, XAConnection e XAResource. Le classi di più alto livello JTA, quali ad esempio UserTransaction e TransactionManager non sono nello scope di un driver JDBC quanto di un application server. Java & PostgreSQL – Luca Ferrari 93 di 182 Identificazione di una transazione Una transazione è identificata da un Transaction ID composto da tre parti fondamentali: format ID: specifica il naming schema delle transazioni (es. OSI CCR – Commitment, Concurrency e Recovery). branch qualifier: un branch rappresenta una singola richiesta ad un resource manager. global transaction id: un identificativo globale della transazione in tutto il sistema. L'implementazione di un transaction dipende dal container/sistema utilizzato. id (javax.transaction.xa.Xid) Java & PostgreSQL – Luca Ferrari 94 di 182 Creazione di uno Xid public class PgDayXid implements Xid{ // Il formatID specifica quale specifica si sta usando. // Se vale 0 allora si sta usando la specifica OSI­CCR // (OSI­Commitment, Concurrency and Recovery). // Se vale ­1 allora l'Xid non è valido, // mentre se è superiore a zero allora indica // una specifica proprietaria. protected int formatID = 0; // identificativo globale della transazione corrente. (0­64 bytes) protected byte[] globalTransactionId = null; // identificativo del ramo della transazione corrente. (0­64 bytes) protected byte[] branchQualifier = null; public byte[] getBranchQualifier() { return this.branchQualifier; } public int getFormatId() { return this.formatID; } public byte[] getGlobalTransactionId() { return this.globalTransactionId; } Java & PostgreSQL – Luca Ferrari 95 di 182 Creazione di uno Xid public void setGlobalTransactionId(byte[] gtid){ // tengo conto solo dei primi 64 bytes... if( gtid != null && gtid.length > 64 ){ System.arraycopy( gtid, 0, this.globalTransactionId, 0, 64 ); } else this.globalTransactionId = gtid; } public void setBranchQualifier(byte[] bq){ ... } public void setFormatId(int id){ ... } } Java & PostgreSQL – Luca Ferrari 96 di 182 Utilizzo di JTA public static void main(String[] args) throws Exception { // 1) creazione di un datasource agganciato al database PGXADataSource dataSource = new PGXADataSource(); dataSource.setDatabaseName("pgday"); // nome del database dataSource.setUser("luca"); // username dataSource.setPassword("fluca"); // password dell'utente dataSource.setServerName("localhost"); // indirizzo di connessione // del database // 2) ottengo una connessione al database System.out.println("Tentativo di connessione al database..."); XAConnection connection = dataSource.getXAConnection(); System.out.println("Connesso!"); // 3) ottengo una risorsa per proteggere la transazione XAResource resource = connection.getXAResource(); Java & PostgreSQL – Luca Ferrari 97 di 182 Utilizzo di JTA // 4) devo avere un id della transazione che sto per usare PgDayXid identifier = new PgDayXid(); identifier.setBranchQualifier( new byte[] {0x01, 0x02, 0x03, 0x04, 0x05}); identifier.setGlobalTransactionId( new byte[] {0x05, 0x04, 0x03, 0x02, 0x01}); identifier.setFormatId(100); // 5) eseguo la transazione try{ // 6) inizio della transazione resource.start(identifier, XAResource.TMNOFLAGS); // 7) esecuzione di operazioni JDBC Connection jdbcConnection = connection.getConnection(); jdbcConnection.setAutoCommit(false); // bug del driver! Statement statement = jdbcConnection.createStatement(); String sql = "INSERT INTO corso(corsoID, descrizione) VALUES('XA1', 'Corso su JTA')"; int inserted = statement.executeUpdate(sql); System.out.println("Sono state inserite " + inserted + " righe"); Java & PostgreSQL – Luca Ferrari 98 di 182 Utilizzo di JTA // 8) ho terminato la transazione resource.end(identifier, XAResource.TMSUCCESS); // 9) controllo se il transaction manager consente il commit // (fase di precommit) int commit = resource.prepare(identifier); System.out.println("Valore del prepare " + commit); if( commit == XAResource.XA_OK ) // commit definitivo resource.commit(identifier, // identificativo transazione false); // false = commit 2 fasi else resource.rollback(identifier); }catch(XAException e){ e.printStackTrace(); } } Java & PostgreSQL – Luca Ferrari 99 di 182 Sospensione di una transazione E' possibile sospendere una transazione distribuita per dare modo al processo corrente di eseguire operazioni locali. E' necessario terminare la transazione distribuita e avviarla nuovamente specificando come flag rispettivamente TMSUSPEND e TMRESUME. // 8) sospensione della transazione distribuita resource.end(identifier, XAResource.TMSUSPEND); // 8 bis) effettuo degli statement fuori dalla transazione // distribuita usando lo stesso oggetto Statement statement.executeUpdare(...); // 8 tris) riprendo la transazione distribuita resource.start(identifier, XAResource.TMRESUME); Java & PostgreSQL – Luca Ferrari 100 di 182 JDBC & SSL Java & PostgreSQL – Luca Ferrari 101 di 182 Connessioni SSL PostgreSQL supporta nativamente le connessioni tramite SSL/TLS da parte dei client (si veda il capitolo 16 del manuale). Solitamente il server PostgreSQL accetta sia connessioni in chiaro che cifrate sulla stessa porta, negoziando dinamicamente il tipo di connessione ricevuta. Una connessione in chiaro invia i pacchetti (es. le query e i risultati) senza cifrarli, e quindi questi sono osservabili sulla rete. Una connessione SSL utilizza uno schema di cifratura per rendere illeggibili da terzi i pacchetti in transito. Java & PostgreSQL – Luca Ferrari 102 di 182 Connessioni SSL: impostazioni del server Il server deve essere stato compilato con il supporto per SSL, e i certificati devono essere stati generati e si devono trovare nella directory $PGDATA. Java & PostgreSQL – Luca Ferrari 103 di 182 Connessioni SSL: test Per verificare il funzionamento del supporto SSL è possibile usare il client da riga di comando. Di default viene usata una connessione in chiaro La suite di cifratura è disponibile (connessione esplicita a localhost) Java & PostgreSQL – Luca Ferrari 104 di 182 Java & SSL Ci sono due modi principali di utilizzo di SSL nelle connessioni a PostgreSQL: connessione verificata: il certificato del server è stato correttamente importato nel keystore (personale o di sistema) di Java, ad indicare che il certificato è noto e corretto. In tal caso, il driver JDBC di PostgreSQL effettua i controlli necessari per validare il certificato ed evitare attacchi man-in-the-middle. connessione non verificata: non è possibile importare il certificato nel keystore, e quindi ci si fida al volo (con i rischi che ne derivano) del certificato presentato dal server. In questo caso occorre usare una socket factory messa a disposizione dal driver JDBC di PostgreSQL. Java & PostgreSQL – Luca Ferrari 105 di 182 Connessione verificata Il driver JDBC di PostgreSQL si occupa di tutta la parte di scambio di chiavi e connessione, ma occorre importare il certificato del server nel keystore: (1) il certificato del server deve essere tradotto in una forma che keytool può comprendere. A tal fine lo si converte come file DER (ASN1-DER; formato senza header) dal formato PEM (standard per openssl, presenta un header testuale). Java & PostgreSQL – Luca Ferrari 106 di 182 Connessione verificata (2) il certificato deve essere importato nel keystore (in questo caso privato dell'utente). Al certificato viene associata l'alias pgday, che rappresenta una sorta di username (in realtà è il proprietario del certificato). Il keystore generato, essendo usato per la verifica di un'identità e non strettamente per la cifratura, viene anche denominato trustore. Java & PostgreSQL – Luca Ferrari 107 di 182 Connessione verificata (3) l'URL di connessione deve contenere il parametro ssl=true, al fine di indicare al driver di usare la connessione SSL (viene usata la factory socket di default). Connection connection = DriverManager.getConnection( "jdbc:postgresql://localhost/pgday?loglevel=2&ssl=true", "luca", "fluca"); (4) occorre specificare come parametri alla JVM quale keystore usare e quale password. [luca@fluca:~]$ java ­Djavax.net.ssl.trustStore=$HOME/keystore ­Djavax.net.ssl.trustStorePassword=aglets it.pgday.lferrari.PgJDBC1 Java & PostgreSQL – Luca Ferrari 108 di 182 Connessione verificata La connessione impiega qualche secondo per essere convertita da normale a cifrata. Il traffico in circolazione non è più in chiaro. Java & PostgreSQL – Luca Ferrari 109 di 182 Connessione non verificata E' possible stabilire una connessione SSL senza aver verificato (importato) il certificato del server. A tal scopo è sufficiente impostare la factory di socket SSL come parametro della URL di connessione a org.postgresql.ssl.NonValidatingFactory. Non è più necessario specificare il keystore e la sua password. Connection connection = DriverManager.getConnection( "jdbc:postgresql://localhost/pgday?loglevel=2&ssl=true &sslfactory=org.postgresql.ssl.NonValidatingFactory", "luca", "fluca"); [luca@fluca:~]$ java it.pgday.lferrari.PgJDBC1 Java & PostgreSQL – Luca Ferrari 110 di 182 org.postgresql.Driver Java & PostgreSQL – Luca Ferrari 111 di 182 Registrazione del Driver PostgreSQL Il Driver si auto-registra in modo statico richiamando il metodo (static) registerDriver di DriverManager. La registrazione statica anziché nel costruttore del Driver stesso (come molti alti driver fanno) impedisce una registrazione multipla dello stesso driver all'interno della JVM. static { static { try{ try{ java.sql.DriverManager.registerDriver(new Driver()); java.sql.DriverManager.registerDriver(new Driver()); } } catch (SQLException e) catch (SQLException e) { e.printStackTrace(); } { e.printStackTrace(); } } // org.postgresql.Driver } // org.postgresql.Driver Java & PostgreSQL – Luca Ferrari 112 di 182 Come il DriverManager sceglie il Driver appropriato... private static Connection getConnection(String url, Properties info, private static Connection getConnection(String url, Properties info, ClassLoader callerCL) throws SQLException { ClassLoader callerCL) throws SQLException { java.util.Vector drivers = null; java.util.Vector drivers = null; ... ... synchronized (DriverManager.class){ synchronized (DriverManager.class){ drivers = readDrivers; drivers = readDrivers; } } ... ... SQLException reason = null; SQLException reason = null; for (int i = 0; i < drivers.size(); i++) { for (int i = 0; i < drivers.size(); i++) { DriverInfo di = (DriverInfo)drivers.elementAt(i); DriverInfo di = (DriverInfo)drivers.elementAt(i); Connection result = di.driver.connect(url, info); Connection result = di.driver.connect(url, info); if (result != null) { if (result != null) { class DriverInfo { return (result); class DriverInfo { return (result); Driver driver; } Driver driver; } Class driverClass; // codice di gestione Class driverClass; // codice di gestione String driverClassName; // errori ... String driverClassName; // errori ... } // java.sql.DriverManager }} } // java.sql.DriverManager ... ... }} // java.sql.DriverManager // java.sql.DriverManager Java & PostgreSQL – Luca Ferrari 113 di 182 connect(..) Il metodo connect(..) del driver PostgreSQL effettua la connessione effettiva al database: si verifica che l'URL di connessione sia gestibile ed appropriato tramite il metodo acceptsURL(..); se non è stato specificato un timeout di login viene tentata la connessione direttamente nel thread chiamante (metodo makeConnection(..)); se è stato specificato un timeout di login viene creato un nuovo thread (ConnectionThread), che cercherà di effettuare la connessione (metodo run()) mentre il thread chiamante resterà in attesa per un tempo pari al timeout specificato (metodo getResult(..)). Java & PostgreSQL – Luca Ferrari 114 di 182 connect(..) public java.sql.Connection connect(String url, Properties info) public java.sql.Connection connect(String url, Properties info) throws SQLException { throws SQLException { ... ... try { // se non ho nessun timeout tento la connessione try { // se non ho nessun timeout tento la connessione // direttamente (opzione di default) // direttamente (opzione di default) long timeout = timeout(props); long timeout = timeout(props); if (timeout <= 0) if (timeout <= 0) return makeConnection(url, props); return makeConnection(url, props); // tento la connessione tramite un altro thread // tento la connessione tramite un altro thread ConnectThread ct = new ConnectThread(url, props); ConnectThread ct = new ConnectThread(url, props); new Thread(ct, new Thread(ct, "PostgreSQL JDBC driver connection thread").start(); "PostgreSQL JDBC driver connection thread").start(); return ct.getResult(timeout); return ct.getResult(timeout); } catch (PSQLException ex1) { } catch (PSQLException ex1) { throw ex1; throw ex1; } catch (Exception ex2) { } catch (Exception ex2) { throw new PSQLException(...); throw new PSQLException(...); } } }} // org.postgresql.Driver // org.postgresql.Driver Java & PostgreSQL – Luca Ferrari 115 di 182 PGStream: un oggetto magico PGStream rappresenta un wrapper attorno connessione verso il database PostgreSQL. ad una Esso contiene un riferimento ad un oggetto java.net.Socket che rappresenta la connessione al database e agli stream per la lettura e la scrittura dei dati su tale socket (sottoclassi specifiche di OutpuStream e InputStream). PGStream mette a disposizione dei metodi di utilità per la scrittura/lettura di byte, interi (2,4 byte), char, stringhe, etc. che vengono usati per l'invio di informazioni secondo il protocollo di comunicazione V2 o V3. Java & PostgreSQL – Luca Ferrari 116 di 182 Interfacce particolari QueryExecutor: gestisce i messaggi specifici di una versione di protocollo (es. V3) per l'esecuzione di query. Una implmentazione è ad esempio org.postgresql.core.v3.QueryExecutorImpl. ResultHandler: gestisce la costruzione progressiva di ResultSet (aggiunge una tupla man mano che viene ricevuta dalla rete), warning ed errori. SimpleQuery: gestione dei dati di una query singola. Java & PostgreSQL – Luca Ferrari 117 di 182 Comunicazione FrontEnd-BackEnd La comunicazione fra il lato applicativo (FrontEnd – driver) e il server (BackEnd) avviene tramite l'uso di un protocollo a scambio di messaggi. Il protocollo, giunto alla sua terza revisione (PostgreSQL >= 7.4) viene comunemente denominato protocollo V3. Java & PostgreSQL – Luca Ferrari 118 di 182 Protocollo V3: formato dei messaggi Un messaggio è un pacchetto contenente diversi byte rappresentati l'informazione. message_type (1 byte) length (4 bytes) content (length – 4 bytes) il primo byte del messaggio identifica il tipo del messaggio stesso i successivi quattro byte specificano la lunghezza del messaggio stesso (incluso il dato di lunghezza stesso) i rimanenti byte rappresentano contenuto del messaggio Java & PostgreSQL – Luca Ferrari il 119 di 182 PreparedStatement vs Portal La fase di parse genera un PreparedStatement, ossia un oggetto che rappresenta l'interpretazione lessicale di una query. Tale oggetto subirà poi il bind dei parametri venendo promosso in un Portal, un oggetto pronto ad eseguire la query. Sostanzialmente un Portal è un handle per una query pronta da eseguire o che sta eseguendo, una sorta di cursore (che funziona anche per query non SELECT). Un portale puo' essere identificato da un nome, oppure rimanere unnamed se si usa il portale di default. Java & PostgreSQL – Luca Ferrari 120 di 182 Protocollo V3: Simple & Extended Query Il protocollo prevede due modalità principali di funzionamento: Simple Query: viene inviato un messaggio con la query e questa viene immediatamente eseguita; i risultati vengono poi inviati al frontend. Extended Query: viene inviata la query ma non viene eseguita immediatamente. In una fase successiva si fa il bind dei parametri della query, si esegue la query e si inviano i risultati al frontend. Simple Query : Statement = Extended Query : PreparedStatement Java & PostgreSQL – Luca Ferrari 121 di 182 Simple Query BackEnd FrontEnd Query contiene la string SQL della query da eseguire RowDescription indica il layout delle colonne ritornate dalla query RowData dati di una singola riga RowData RowData ReadyForQuery indica che il backend è pronto ad accettare una nuova query Java & PostgreSQL – Luca Ferrari 122 di 182 Extended Query BackEnd FrontEnd Parse (stringa SQL, [nome prepared statement, nome portale]) ParseComplete Bind (lista parametri, [nome prepared statement, nome portale]) BindComplete Execute ([nome portale]) RowData dati di una singola riga RowData RowData Sync sincronizzazione per errori ReadyForQuery il backend è pronto Java & PostgreSQL – Luca Ferrari 123 di 182 Query: overview Viene creato un oggetto Statement dal Connection corrente. Tipicamente si passa attraverso AbstractJdbc2Connection, che richiama il tipo di connessione corretto (es. Jdbc4Connection). Il metodo executeQuery(..) viene sempre richiamato su AbstractJdbc2Statement, che mediante QueryExecutor ottiene un oggetto SimpleQuery che implementa il protocollo di comunicazione FrontEnd e BackEnd opportuno (es. v3). Java & PostgreSQL – Luca Ferrari 124 di 182 Query: overview Il metodo execute(..) di AbstractJdbc2Statement accetta l'oggetto query da eseguire e richiama il metodo execute(..) sul QueryExecutor. Al metodo viene anche passato un oggetto ResultHandler che si occupa di gestire risultati (ResultSet), errori e warning. Il QueryExecutor invia i messaggi necessari al server, in particolare tramite il metodo sendQuery(..) e sendOneQuery(..) invia la query da eseguire in formato testuale. Java & PostgreSQL – Luca Ferrari 125 di 182 Query: overview Il QueryExecutor processa la risposta del server tramite processResult(..). Questo metodo utilizza l'oggetto PGStream per leggere i messaggi in arrivo dal server. Viene usato un meccanismo ad eventi: a seconda del messaggio letto viene richiamato un metodo opportuno sul ResultHandler. In questo modo l'handler può, ad esempio, memorizzare le tuple man mano che vengono lette. Si richiede al ResultHandler di restituire il ResultSet. Questo viene ritornato indietro lungo lo stack fino al lato applicativo. Java & PostgreSQL – Luca Ferrari 126 di 182 Creazione di uno Statement public java.sql.Statement createStatement() throws SQLException { public java.sql.Statement createStatement() throws SQLException { // crea uno statement per un Resultset classico // crea uno statement per un Resultset classico return createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, return createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_READ_ONLY); java.sql.ResultSet.CONCUR_READ_ONLY); }} // org.postgresql.jdbc2.AbstractJdbc2Connection // org.postgresql.jdbc2.AbstractJdbc2Connection public java.sql.Statement createStatement(int resultSetType, public java.sql.Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) int resultSetConcurrency, int resultSetHoldability) throws SQLException { throws SQLException { // creazione di uno statement di livello 4 // creazione di uno statement di livello 4 Jdbc4Statement s = new Jdbc4Statement(this, resultSetType, Jdbc4Statement s = new Jdbc4Statement(this, resultSetType, resultSetConcurrency, resultSetHoldability); resultSetConcurrency, resultSetHoldability); s.setPrepareThreshold(getPrepareThreshold()); s.setPrepareThreshold(getPrepareThreshold()); return s; return s; }} // org.postgresql.jdbc4.Jdbc4Connection // org.postgresql.jdbc4.Jdbc4Connection La creazione di uno Statement implica ancora una volta il passaggio fra le classi dei vari livelli JDBC. In particolare si parte dal livello 2 (il minimo) e si arriva fino al livello 4 creando un Jdbc4Statement. Java & PostgreSQL – Luca Ferrari 127 di 182 Esecuzione di una query public ResultSet executeQuery(String p_sql) throws SQLException { public ResultSet executeQuery(String p_sql) throws SQLException { // se ho una query preprata (PreparedStament) non processare // se ho una query preprata (PreparedStament) non processare // un'altra query SQL (p_sql) // un'altra query SQL (p_sql) if (preparedQuery != null) if (preparedQuery != null) throw new PSQLException(...); throw new PSQLException(...); // eseguo la query specificata // eseguo la query specificata if (!executeWithFlags(p_sql, 0)) if (!executeWithFlags(p_sql, 0)) throw new PSQLException(...); throw new PSQLException(...); // sono stati ritornati troppi result set in una sola volta // sono stati ritornati troppi result set in una sola volta if (result.getNext() != null) if (result.getNext() != null) throw new PSQLException(...); throw new PSQLException(...); return (ResultSet)result.getResultSet(); return (ResultSet)result.getResultSet(); }} // org.postgresql.jdbc2.AbstractJdbc2Statement // org.postgresql.jdbc2.AbstractJdbc2Statement Il metodo fondamentale per l'esecuzione di una query è executeWithFlags. Mediante tale metodo viene creato un oggetto SimpleQuery che si occuperà dello scambio di messaggi con il backend per il protocollo relativo. Java & PostgreSQL – Luca Ferrari 128 di 182 Esecuzione di una query public boolean executeWithFlags(String p_sql, int flags) public boolean executeWithFlags(String p_sql, int flags) throws SQLException { throws SQLException { // controlla se lo statement è chiuso (nel caso solleva // controlla se lo statement è chiuso (nel caso solleva // una eccezione) // una eccezione) checkClosed(); checkClosed(); // effettua alcune sostituzione nella query (es. escaping) // effettua alcune sostituzione nella query (es. escaping) p_sql = replaceProcessing(p_sql); p_sql = replaceProcessing(p_sql); Query simpleQuery = Query simpleQuery = connection.getQueryExecutor().createSimpleQuery(p_sql); connection.getQueryExecutor().createSimpleQuery(p_sql); execute(simpleQuery, null, QueryExecutor.QUERY_ONESHOT | flags); execute(simpleQuery, null, QueryExecutor.QUERY_ONESHOT | flags); this.lastSimpleQuery = simpleQuery; this.lastSimpleQuery = simpleQuery; // result è un tipo ResultWrapper, che contiene // result è un tipo ResultWrapper, che contiene // anche il ResultSet // anche il ResultSet return (result != null && result.getResultSet() != null); return (result != null && result.getResultSet() != null); }} // org.postgresql.jdbc2.AbstractJdbc2Statement // org.postgresql.jdbc2.AbstractJdbc2Statement public Query createSimpleQuery(String sql) { public Query createSimpleQuery(String sql) { return parseQuery(sql, false); return parseQuery(sql, false); }} // org.postgresql.core.v3.QueryExecutorImpl // org.postgresql.core.v3.QueryExecutorImpl Java & PostgreSQL – Luca Ferrari 129 di 182 Esecuzione di una query protected void execute(Query queryToExecute, protected void execute(Query queryToExecute, ParameterList queryParameters, int flags) throws SQLException { ParameterList queryParameters, int flags) throws SQLException { // chiusura di query precedenti e pulizia di warning ed errori // chiusura di query precedenti e pulizia di warning ed errori // che potevano provenire da query precedenti // che potevano provenire da query precedenti StatementResultHandler handler = new StatementResultHandler(); StatementResultHandler handler = new StatementResultHandler(); result = null; result = null; connection.getQueryExecutor().execute(queryToExecute, connection.getQueryExecutor().execute(queryToExecute, queryParameters, queryParameters, handler, handler, maxrows, maxrows, fetchSize, fetchSize, flags); flags); result = firstUnclosedResult = handler.getResults(); result = firstUnclosedResult = handler.getResults(); }} // org.postgresql.jdbc2.AbstractJdbc2Statement // org.postgresql.jdbc2.AbstractJdbc2Statement Si prepara un handler per il risultato ed eventuali errori e si passa la query e l'handler al metodo execute(..) del QueryExecutor (la sua implementazione per il protocollo V3) affinché gestisca i messaggi. Java & PostgreSQL – Luca Ferrari 130 di 182 Esecuzione di una query (lettura risultati) protected void processResults(ResultHandler handler, int flags) protected void processResults(ResultHandler handler, int flags) throws IOException { throws IOException { Vector tuples = new Vector(); Field[] fields = null; Vector tuples = new Vector(); Field[] fields = null; ... ... while (!endQuery) { while (!endQuery) { c = pgStream.ReceiveChar(); c = pgStream.ReceiveChar(); switch (c){ switch (c){ case 'D': // messaggio di tipo DataRow case 'D': // messaggio di tipo DataRow Object tuple = null; Object tuple = null; try { try { tuple = pgStream.ReceiveTupleV3(); tuple = pgStream.ReceiveTupleV3(); } catch(OutOfMemoryError oome) { ... } } catch(OutOfMemoryError oome) { ... } if (!noResults) if (!noResults) tuples.addElement(tuple); tuples.addElement(tuple); break; break; ... ... // org.postgresql.core.v3.QueryExecutorImpl // org.postgresql.core.v3.QueryExecutorImpl Java & PostgreSQL – Luca Ferrari 131 di 182 Esecuzione di una query (lettura risultati) case 'C': // command status (fine esecuzione execute) case 'C': // command status (fine esecuzione execute) String status = receiveCommandStatus(); String status = receiveCommandStatus(); ... ... if (fields != null || tuples != null) { if (fields != null || tuples != null) { handler.handleResultRows(currentQuery, fields, tuples, null); handler.handleResultRows(currentQuery, fields, tuples, null); fields = null; tuples = null; fields = null; tuples = null; } } else else interpretCommandStatus(status, handler); interpretCommandStatus(status, handler); ... ... break; break; case 'Z': // ready for query case 'Z': // ready for query receiveRFQ(); receiveRFQ(); endQuery = true; // termina il ciclo while endQuery = true; // termina il ciclo while // pulizia degli oggetti in memoria e delle code // pulizia degli oggetti in memoria e delle code ... ... break; break; ... ... } } } } }} // org.postgresql.core.v3.QueryExecutorImpl // org.postgresql.core.v3.QueryExecutorImpl Java & PostgreSQL – Luca Ferrari 132 di 182 Query: traffico di rete Analizzando il traffico di rete con uno sniffer è possibile vedere i singoli messaggi inviati da e per il server. Nell'esempio qui sopra si nota il messaggio di Parse, con la query inviata, seguito da vari messaggi uno dei quali Execute. Java & PostgreSQL – Luca Ferrari 133 di 182 Query: traffico di rete In questo caso si vede il messaggio RowDescription che precede una serie di messaggi DataRow, ciascuno con indicazione del numero, lunghezza e valore delle colonne. Java & PostgreSQL – Luca Ferrari 134 di 182 Query: traffico di rete Messaggio Parse contenente la query e il nome dello statement di riferimento sul server (null). Il server invia un RowDescription e di seguito tutte le righe tramite una serie di DataRow. Il server invia i messaggi di chiusura, fra i quali ReadyForQuery. Java & PostgreSQL – Luca Ferrari 135 di 182 Esecuzione di una query parametrica statement2.setInt(1, corsoPK); statement2.setInt(1, corsoPK); // lato applicativo // lato applicativo La fase di bind in memoria prevede che il parametro sia associato alla sua posiziona nella query (index), al suo valore (rappresentato come stringa) e al suo tipo (OID) affinché il BackEnd possa capire come trattare il dato stesso. I parametri sono contenuti in oggetti che implementano ParameterList a seconda del protocollo utilizzato (ad es. V3ParameterList). public void setInt(int parameterIndex, int x) throws SQLException { public void setInt(int parameterIndex, int x) throws SQLException { checkClosed(); checkClosed(); bindLiteral(parameterIndex, Integer.toString(x), Oid.INT4); bindLiteral(parameterIndex, Integer.toString(x), Oid.INT4); }} private void bindLiteral(int paramIndex,String s,int oid) private void bindLiteral(int paramIndex,String s,int oid) throws SQLException { throws SQLException { if(adjustIndex) if(adjustIndex) paramIndex­­; paramIndex­­; // registro il parametro in un contenitore V3ParameterList // registro il parametro in un contenitore V3ParameterList preparedParameters.setLiteralParameter(paramIndex, s, oid); preparedParameters.setLiteralParameter(paramIndex, s, oid); } // org.postgresql.jdbc2.AbstractJdbc2Statement } // org.postgresql.jdbc2.AbstractJdbc2Statement Java & PostgreSQL – Luca Ferrari 136 di 182 Esecuzione di una query parametrica Il metodo executeQuery(), invocato dopo il bind dei parametri, richiama il metodo executeWithFlags(..) già visto in precedenza e usato anche per query non parametriche. Ripercorrendo lo stack di chiamata si giunge a QueryExecutor.execute(..) che questa volta ha impostato i parametri della query. Da qui si passa poi a QueryExecutor.sendOneQuery(..) che provvede a sostituire ad ogni parametro (indicato con ?) un $n (con n valore della posizione partendo da 1). In coda alla query compare poi l'array degli OID dei tipi dei parametri. public java.sql.ResultSet executeQuery() throws SQLException { public java.sql.ResultSet executeQuery() throws SQLException { if (!executeWithFlags(0)) if (!executeWithFlags(0)) throw new PSQLException(...); throw new PSQLException(...); if (result.getNext() != null) if (result.getNext() != null) throw new PSQLException(...); throw new PSQLException(...); return (ResultSet) result.getResultSet(); return (ResultSet) result.getResultSet(); } // org.postgresql.jdbc2.AbstractJdbc2Statement } // org.postgresql.jdbc2.AbstractJdbc2Statement FE=> Parse(stmt=null,query="SELECT cognome, nome FE=> Parse(stmt=null,query="SELECT cognome, nome FROM partecipante WHERE corsoPK=$1",oids={23}) FROM partecipante WHERE corsoPK=$1",oids={23}) Java & PostgreSQL – Luca Ferrari 137 di 182 Esecuzione di una query parametrica private void sendBind(SimpleQuery query, SimpleParameterList params, private void sendBind(SimpleQuery query, SimpleParameterList params, Portal portal) throws IOException { Portal portal) throws IOException { // informazioni su portale e statement (se esistono) // informazioni su portale e statement (se esistono) String statementName = query.getStatementName(); String statementName = query.getStatementName(); byte[] encodedStatementName = query.getEncodedStatementName(); byte[] encodedStatementName = query.getEncodedStatementName(); byte[] encodedPortalName = (portal == null ? null : byte[] encodedPortalName = (portal == null ? null : portal.getEncodedPortalName()); portal.getEncodedPortalName()); ... ... pgStream.SendChar('B'); pgStream.SendChar('B'); // messaggio Bind // messaggio Bind pgStream.SendInteger4((int)encodedSize); // dimensione messaggio pgStream.SendInteger4((int)encodedSize); // dimensione messaggio if (encodedPortalName != null) // eventuale portale if (encodedPortalName != null) // eventuale portale pgStream.Send(encodedPortalName); pgStream.Send(encodedPortalName); pgStream.SendChar(0); pgStream.SendChar(0); if (encodedStatementName != null) if (encodedStatementName != null) pgStream.Send(encodedStatementName); pgStream.Send(encodedStatementName); // eventuale statement // eventuale statement pgStream.SendChar(0); pgStream.SendChar(0); ... ... // org.postgresql.core.v3.QueryExecutorImpl // org.postgresql.core.v3.QueryExecutorImpl Java & PostgreSQL – Luca Ferrari 138 di 182 Esecuzione di una query parametrica // invio tipo e numero dei parametri // invio tipo e numero dei parametri for (int i = 1; i <= params.getParameterCount(); ++i) for (int i = 1; i <= params.getParameterCount(); ++i) pgStream.SendInteger2(params.isBinary(i) ? 1 : 0); pgStream.SendInteger2(params.isBinary(i) ? 1 : 0); pgStream.SendInteger2(params.getParameterCount()); pgStream.SendInteger2(params.getParameterCount()); ... ... for (int i = 1; i <= params.getParameterCount(); ++i) { for (int i = 1; i <= params.getParameterCount(); ++i) { if (params.isNull(i)) if (params.isNull(i)) pgStream.SendInteger4( ­1); // dimensione ­1 => NULL pgStream.SendInteger4( ­1); // dimensione ­1 => NULL else { else { // dimensione del parametro // dimensione del parametro pgStream.SendInteger4(params.getV3Length(i)); pgStream.SendInteger4(params.getV3Length(i)); try{ // valore del parametro try{ // valore del parametro params.writeV3Value(i, pgStream); params.writeV3Value(i, pgStream); }catch (PGBindException be) { }catch (PGBindException be) { bindException = be; bindException = be; } } } } } } ... ... } // org.postgresql.core.v3.QueryExecutorImpl } // org.postgresql.core.v3.QueryExecutorImpl Java & PostgreSQL – Luca Ferrari 139 di 182 Esecuzione di INSERT/UPDATE I passi fondamentali sono simili a quanto visto in precedenza: si deve ottenere (AbstractJdbc2Connection) (AbstractJdbc2Statement); dall'oggetto uno Connection Statement si richiede allo Statement (AbstractJdbc2Statement) di eseguire la modifica mediante il metodo JDBC executeUpdate(String sql); si ottiene come valore di ritorno il numero di record modificati nella base di dati (command status) Il command status viene interpretato letteralmente, ossia dalla stringa di command status inviata dal BackEnd si estraggono i valori ritornati dal server. Java & PostgreSQL – Luca Ferrari 140 di 182 Esecuzione di un INSERT/UPDATE public int executeUpdate(String p_sql) throws SQLException { public int executeUpdate(String p_sql) throws SQLException { ... ... // eseguo la query tramite il QueryExecutor // eseguo la query tramite il QueryExecutor // non mi aspetto risultati (QUERY_NO_RESULTS) // non mi aspetto risultati (QUERY_NO_RESULTS) if (executeWithFlags(p_sql, QueryExecutor.QUERY_NO_RESULTS)) if (executeWithFlags(p_sql, QueryExecutor.QUERY_NO_RESULTS)) // non dovrei avere alcun risultato // non dovrei avere alcun risultato throw new PSQLException(...); throw new PSQLException(...); // restituisco il numero di record aggiornati/inseriti // restituisco il numero di record aggiornati/inseriti return getUpdateCount(); return getUpdateCount(); } // org.postgresql.jdbc2.AsbtractJdbc2Statement } // org.postgresql.jdbc2.AsbtractJdbc2Statement public boolean executeWithFlags(String p_sql, int flags) public boolean executeWithFlags(String p_sql, int flags) throws SQLException { throws SQLException { Query simpleQuery = Query simpleQuery = connection.getQueryExecutor().createSimpleQuery(p_sql); connection.getQueryExecutor().createSimpleQuery(p_sql); execute(simpleQuery, null, QueryExecutor.QUERY_ONESHOT | flags); execute(simpleQuery, null, QueryExecutor.QUERY_ONESHOT | flags); this.lastSimpleQuery = simpleQuery; this.lastSimpleQuery = simpleQuery; return (result != null && result.getResultSet() != null); return (result != null && result.getResultSet() != null); }} // org.postgresql.jdbc2.AsbtractJdbc2Statement // org.postgresql.jdbc2.AsbtractJdbc2Statement Java & PostgreSQL – Luca Ferrari 141 di 182 Esecuzione di un INSERT/UPDATE protected void processResults(ResultHandler handler, int flags) protected void processResults(ResultHandler handler, int flags) throws IOException { throws IOException { .... .... case 'C': // Command Status (end of Execute) case 'C': // Command Status (end of Execute) // ottiene la stringa risultato dell'esecuzione della query // ottiene la stringa risultato dell'esecuzione della query // ad esempio INSERT 0 1 oppure UPDATE 1 // ad esempio INSERT 0 1 oppure UPDATE 1 String status = receiveCommandStatus(); String status = receiveCommandStatus(); if (fields != null || tuples != null){ if (fields != null || tuples != null){ // qui c'e' un ResultSet, quindi una select // qui c'e' un ResultSet, quindi una select handler.handleResultRows(currentQuery, fields, tuples, null); handler.handleResultRows(currentQuery, fields, tuples, null); fields = null; tuples = null; fields = null; tuples = null; } } else { else { // qui non c'e' ResultSet, quindi una scrittura sul DB // qui non c'e' ResultSet, quindi una scrittura sul DB interpretCommandStatus(status, handler); interpretCommandStatus(status, handler); } } ... ... } // org.postgresql.core.v3.QueryExecutorImpl } // org.postgresql.core.v3.QueryExecutorImpl Java & PostgreSQL – Luca Ferrari 142 di 182 Pl/Java Java & PostgreSQL – Luca Ferrari 143 di 182 Installazione di Pl/Java Pljava si basa su un modulo del kernel denominato pljava.so; la versione base è compilata per architettura 32 bit e verso PostgreSQL 8.3. Per una corretta installazione su piattaforme e database differenti occorre: usare un JDK fra la versione 4 e 5 (non la 6 perché cambiano le API JDBC); installare gli header per lo sviluppo di PostgreSQL della versione corrente (ad esempio postgresql­server­dev­8.4); scaricare tramite CVS l'ultima versione dei sorgenti di Pl/Java; verificare che JAVA_HOME e il PATH puntino al compilatore Java corretto; lanciare la compilazione con il comando make. Java & PostgreSQL – Luca Ferrari 144 di 182 Installazione di Pl/Java Occorre poi apportare alcune modifiche a postgresql.conf affinché il server-cluster postmaster possa caricare l'estensione Java: dynamic_library_path = '$libdir:/sviluppo/java/pljava/org.postgresql.pljava/build/objs' custom_variable_classes = 'pljava' pljava.classpath='/sviluppo/java/pljava/org.postgresql.pljava/build /pljava.jar' Java & PostgreSQL – Luca Ferrari 145 di 182 Introduzione a Pl/Java Pl/Java è una estensione di PostgreSQL per supportare il linguaggio Java come linguaggio interno. Pl/Java è simile, concettualmente, ad altri Pl/xxx come ad esempio Pl/Perl. La differenza principale con altri linguaggi è che, essendo Java compilato, non è possibile scrivere direttamente codice Java all'interno del server, bensì si deve istruire il server per richiamare del codice Java in formato bytecode. In altre parole usare Pl/Java significa sempre: Scrivere del codice Java. Scrivere del codice SQL per richiamare Java. Java & PostgreSQL – Luca Ferrari 146 di 182 JNI? Il backend PostgreSQL non è scritto in Java! Occorre quindi trovare un modo per far dialogare un pezzo di codice Java con il backend. Ci sono due soluzioni possibili: RPC JNI Pl/Java si basa su JNI per svariate ragioni, principalmente: Piu' semplice ed efficiente Il codice Java risiede sul server, quindi è locale al backend (non c'è necessità di usare chiamate remote) Java & PostgreSQL – Luca Ferrari 147 di 182 1 backend = 1 JVM ? // backend.c static void initializeJavaVM(void){ ... jstat = JNI_createVM(&s_javaVM, &vm_args); ... } Nel file backend.c, che inizializza il collegamento fra il backend vero e proprio e Java, si ha che per ogni connessione viene avviata una JVM tramite JNI. In sostanza si ha una JVM per ogni connessione utente. Ciò è coerente con il design di PostgreSQL che prevede un processo (backend appunto) per ogni connessione utente, inoltre garantisce la protezione dello spazio utente tramite l'astrazione dei processi e infine consente di gestire la priorità dei processi stessi tramite gli strumenti del sistema operativo (ps, nice, ...). Java & PostgreSQL – Luca Ferrari 148 di 182 Installazione di Pl/Java PlJava viene fornito con uno strumento (Java) per l'installazione del supporto Java presso un determinato database: deploy.jar. Occorre eseguire questo programma Java (che richiede la presenza del driver JDBC PostgreSQL) per installare il supporto dinamico a Java nel database: java org.postgresql.pljava.deploy.Deployer ­install ­database hrpmdb ­user postgres ­password postgres Il programma provvede a creare le funzioni handler e lo schema speciale sqlj nel database. Java & PostgreSQL – Luca Ferrari 149 di 182 Vedere cosa succede dietro le quinte E' bene abilitare il livello di log_min_messages a debug3 per ottenere un po' di informazioni circa l'utilizzo di Java nel server. Nei log si troveranno messaggi circa la creazione dell'istanza della JVM e il caricamento delle classi. 2010­06­01 13:55:03 CEST DEBUG: find_in_dynamic_libpath: trying "/usr/lib/postgresql/8.4/lib/pljava" 2010­06­01 13:55:03 CEST DEBUG: find_in_dynamic_libpath: trying "/sviluppo/java/pljava/org.postgresql.pljava/build/objs/pljava" 2010­06­01 13:55:03 CEST DEBUG: find_in_dynamic_libpath: trying "/usr/lib/postgresql/8.4/lib/pljava.so" 2010­06­01 13:55:03 CEST DEBUG: find_in_dynamic_libpath: trying "/sviluppo/java/pljava/org.postgresql.pljava/build/objs/pljava.so" 2010­06­01 13:55:03 CEST DEBUG: Using integer_datetimes 2010­06­01 13:55:03 CEST DEBUG: Added JVM option string "­ Djava.class.path=/sviluppo/java/pljava/org.postgresql.pljava/build" 2010­06­01 13:55:03 CEST DEBUG: Added JVM option string "­Dsqlj.defaultconnection=jdbc:default:connection" 2010­06­01 13:55:03 CEST DEBUG: Added JVM option string "vfprintf" 2010­06­01 13:55:03 CEST DEBUG: Added JVM option string "­Xrs" 2010­06­01 13:55:03 CEST DEBUG: Creating JavaVM 2010­06­01 13:55:03 CEST DEBUG: JavaVM created 2010­06­01 13:55:03 CEST DEBUG: Getting Backend class pljava.jar Java & PostgreSQL – Luca Ferrari 150 di 182 Un primo esempio di funzione CREATE FUNCTION getsysprop(VARCHAR) RETURNS VARCHAR AS 'java.lang.System.getProperty' LANGUAGE java; Java & PostgreSQL – Luca Ferrari 151 di 182 Scrivere funzioni PL/Java Ogni metodo invocabile in PostgreSQL deve essere un metodo statico (i parametri devono corrispondere a quelli passati alla funzione SQL). Ogni funzione Java deve essere invocata tramite una funzione SQL. Le classi devono essere contenute in un file jar caricato e impostato nel classpath del database. Il classpath viene amministratori. gestito solo dagli utenti ATTENZIONE: se vengono riportate eccezioni di sicurezza significa che la classe è presente nel classpath del processo postmaster e quindi viene caricata senza passare dal motore database! Java & PostgreSQL – Luca Ferrari 152 di 182 Un secondo esempio di funzione Java & PostgreSQL – Luca Ferrari 153 di 182 Parametri e tipi di ritorno: un esempio Java & PostgreSQL – Luca Ferrari 154 di 182 NULL vs null Il tipo NULL del linguaggio SQL viene tradotto nel tipo null di Java. Però per SQL è lecito avere NULL anche dove c'è uno scalare, mentre per Java no! Di conseguenza le funzioni che lavorino con scalari (in ingresso/uscita) e che debbano gestire tipi NULL (SQL) devono usare le classi wrapper. Java & PostgreSQL – Luca Ferrari 155 di 182 Funzioni che restituiscono un SETOF Pl/Java richiede che una funzione che ritorna un SETOF (lato SQL) restituisca un Iterator (lato Java). Non verranno accettati altri tipi di ritorno! E' comunque possibile usare un qualunque tipo di dato/struttura che implementi l'interfaccia Iterator. Java & PostgreSQL – Luca Ferrari 156 di 182 Creazione di Trigger Il codice Java dipende dalle librerie Pl/Java Ogni funzione trigger non restituisce nulla (void) e accetta come parametro un oggetto TriggerData con le informazioni sull'invocazione del Trigger. Tramite TriggerData è possibile selezionare il result set new oppure old e su questi agire (old è in sola lettura). Java & PostgreSQL – Luca Ferrari 157 di 182 Esempio di Trigger Si vuole creare un trigger che modifichi una stringa di testo con le metainformazioni sul trigger stesso. Java & PostgreSQL – Luca Ferrari 158 di 182 Esempio di Trigger Java & PostgreSQL – Luca Ferrari 159 di 182 Esempio piu' complesso di Trigger Si supponga di voler tenere traccia del numero globali di invocazioni della funzione trigger, del numero di update di una riga e di impedire le cancellazioni delle righe che sono state aggiornate un numero pari di volte. Java & PostgreSQL – Luca Ferrari 160 di 182 Codice della funzione Trigger public class Trigger { public static int globalCounter = 0; public static void triggerJavaMethod( TriggerData triggerData ) throws SQLException{ // e' un trigger per update? if( triggerData.isFiredByUpdate() ){ // prelevo il result set nuovo ResultSet newRS = triggerData.getNew(); // prelevo il vecchio result set ResultSet oldRS = triggerData.getOld(); // inserisco il contatore globale newRS.updateInt("global_counter", ++globalCounter); // incremento il contatore di update newRS.updateInt("update_counter", oldRS.getInt("update_counter") + 1); } else if( triggerData.isFiredByDelete() ){ ResultSet oldRS = triggerData.getOld(); if( ( oldRS.getInt("update_counter") % 2 ) == 0 ) throw new TriggerException(triggerData, "Tupla non .."); } } Non esiste ancora un metodo efficace per gestire l'abort di un trigger! Lanciare una eccezione non funziona appieno: blocca la transazione nel backend! Java & PostgreSQL – Luca Ferrari 161 di 182 Esecuzione del Trigger Java & PostgreSQL – Luca Ferrari 162 di 182 Maneggiare tipi complessi I tipi complessi devono essere trattati tramite un ResultSet aggiornabile. Si rinuncia al paradigma OOP per passare ad un paradigma in stile record-based. E' comodo usare un ResultSetGenerator: La funzione (lato SQL) richiama un metodo (lato Java) statico che restituisce un oggetto che implementa l'interfaccia ResultSetProvider. ResultSetProvider contiene due metodi: assignRowValues(..) che permette l'update di una cella (riga/colonna) nel ResultSet e close() usato per rilasciare le risorse. ResultSetProvider.assignRowValues(..) restituisce false se non ci sono altre righe da processare, true se ancora una riga deve essere inserita nel ResultSet. Java & PostgreSQL – Luca Ferrari 163 di 182 Maneggiare tipi complessi: generare delle tuple public class RowGenerator implements ResultSetProvider { private static int NUM_ROWS = 10; private static int NUM_COLS = 5; public boolean assignRowValues(ResultSet rs, int rowNumber) throws SQLException { if( rowNumber <= NUM_ROWS ){ for( int j = 1; j <= NUM_COLS; j++ ) rs.updateString(j, "Riga " + rowNumber + " Colonna " + j); return true; } else return false; } public void close() throws SQLException { System.out.println("Chiusura del row set provider"); } public static ResultSetProvider generateRows(){ return new RowGenerator(); } } Java & PostgreSQL – Luca Ferrari 164 di 182 Maneggiare tipi complessi: invocazioni SQL Java & PostgreSQL – Luca Ferrari 165 di 182 JDBC & Pl/Java =~ SQL MED E' possibile chiamare JDBC da Pl/Java, in modo da poter interrogare in modo riflessivo il database al quale si è connessi. In questo modo si possono ottenere (e restituire) i dati appartenenti ad altre tabelle/relazioni. Java & PostgreSQL – Luca Ferrari 166 di 182 JDBC & Pl/Java =~ SQL MED public class RowGenerator implements ResultSetHandle { private String databaseName; private String username; private String password; private String tableName; public RowGenerator(String database, String username, String password, String table ){ this.databaseName = database; this.username = username; this.password = password; this.tableName = table; } // funzione usata lato SQL public static ResultSetHandle generateRows(String database, String username, String password, String table){ return new RowGenerator( database, username, password, table ); } Java & PostgreSQL – Luca Ferrari 167 di 182 JDBC & Pl/Java = SQL MED // funzione usata per ottenere le tuple public ResultSet getResultSet() throws SQLException { org.postgresql.Driver driver = new org.postgresql.Driver(); Connection connection = DriverManager.getConnection( this.databaseName, this.username, this.password ); Statement statement = connection.createStatement(); return statement.executeQuery(" SELECT * FROM "+ this.tableName ); } } Se la funzione viene creata come linguaggio untrusted allora è possibile usare la connessione verso ogni database, e quindi implementare un SQL MED completo. ATTENZIONE: i driver devono trovarsi nel classpath! Java & PostgreSQL – Luca Ferrari 168 di 182 Utilizzo dei SavePoint public static void executeInsert(int counter) throws SQLException{ Connection connection = DriverManager.getConnection("jdbc:default:connection"); Statement statement = connection.createStatement(); Savepoint save = null; // effettuo 10 inserimenti for( int i = 0; i < counter; i++ ){ statement.execute("INSERT INTO java_table(nome) VALUES('inserimento­" + i + "')"); if( i >= (counter / 2) && save == null ) save = connection.setSavepoint(); } // rollback connection.rollback( save ); } Java & PostgreSQL – Luca Ferrari 169 di 182 SavePoint: risultato Java & PostgreSQL – Luca Ferrari 170 di 182 Listener private static Logger logger = Logger.getAnonymousLogger(); public static void executeInsert(int counter) throws SQLException{ Connection connection = DriverManager.getConnection("jdbc:default:connection"); Session session = SessionManager.current(); session.addSavepointListener( new SavepointListener() { public void onStart(Session arg0, Savepoint arg1, Savepoint arg2) throws SQLException { logger.info("Savepoint START " + arg0 + " savepoints " + arg1 + " " + arg2); } public void onCommit(Session arg0, Savepoint arg1, Savepoint arg2) throws SQLException { logger.info("Savepoint COMMIT" + arg0 + " savepoints " + arg1 + " " + arg2); } public void onAbort(Session arg0, Savepoint arg1, Savepoint arg2) throws SQLException { logger.info("Savepoint ABORT " + arg0 + " savepoints " + arg1 + " " + arg2); } }); Java & PostgreSQL – Luca Ferrari 171 di 182 Listener E' possibile agganciare dei listener per le transazioni (TransactionListener) e per la gestione dei SavePoint (sotto-transazioni) (SavePointListener). Java & PostgreSQL – Luca Ferrari 172 di 182 JDBC & Pl/Java =~ SQL MED public class RowGenerator implements ResultSetHandle { private String databaseName; private String username; private String password; private String tableName; public RowGenerator(String database, String username, String password, String table ){ this.databaseName = database; this.username = username; this.password = password; this.tableName = table; } public static ResultSetHandle generateRows(String database, String username, String password, String table){ return new RowGenerator( database, username, password, table ); } public ResultSet getResultSet() throws SQLException { Connection connection = DriverManager.getConnection( this.databaseName, this.username, this.password ); Statement statement = connection.createStatement(); return statement.executeQuery(" SELECT * FROM "+ this.tableName ); } } Java & PostgreSQL – Luca Ferrari 173 di 182 Singleton e PooledObjects Pl/Java consente la gestione di oggetti in un pool, ovvero un recipiente di oggetti che possono essere riciclati per scopi futuri. In un certo senso questo aiuta nel paradigma dei singleton. Ogni oggetto che deve essere gestibile da un pool deve implementare l'interfaccia PooledObject e implementare i metodi per la attivazione e disattivazione. Il costruttore dell'oggetto deve accettare il pool di appartenenza (ObjectPool) sul quale puo' operare. Java & PostgreSQL – Luca Ferrari 174 di 182 Esempio di PooledObject public class Pooled implements PooledObject{ private long creationTimeMillis; private ObjectPool myPool; public Pooled( ObjectPool myPool ){ super(); this.creationTimeMillis = Calendar.getInstance().getTimeInMillis(); this.myPool = myPool; } public void activate() throws SQLException { System.out.println("Oggetto <" + creationTimeMillis + "> attivato!"); } public void passivate() throws SQLException { System.out.println("Oggetto <" + creationTimeMillis + "> disattivato!"); } Java & PostgreSQL – Luca Ferrari 175 di 182 Esempio di PooledObject public void remove() { System.out.println("Oggetto <" + creationTimeMillis + "> rimosso!"); } public static void createPooledObject() throws SQLException{ // ottengo la sessione corrente Session session = SessionManager.current(); // ottengo il pool di oggetti ObjectPool pool = session.getObjectPool( Pooled.class ); // ottengo una istanza dal pool Pooled myPooled = (Pooled) pool.activateInstance(); // ci faccio qualcosa.... // poi lo reinserisco nel pool pool.passivateInstance(myPooled); } } Java & PostgreSQL – Luca Ferrari 176 di 182 Esempio di PooledObject Se si invoca ripetutamente la funzione di generazione dell'oggetto si nota che è sempre lo stesso oggetto ad essere usato. Se l'oggetto non viene reinserito nel pool, allora un nuovo viene creato e usato. Java & PostgreSQL – Luca Ferrari 177 di 182 Multithreading Problema: Java è un linguaggio che supporta il multithreading, ma il backend PostgreSQL è un singolo processo senza supporto ai thread. Il supporto al multithreading di Java deve essere comunque garantito! Se si pensa al backend come ad un ennesimo thread, allora tutti i thread (Java e di backend) devono sincronizzarsi in modo coerente: si utilizza un singolo lock! Viene definito un oggetto particolare, Backend.THREADLOCK sul quale tutti si sincronizzano. Java & PostgreSQL – Luca Ferrari il 178 di 182 Backend.THREADLOCK Java & PostgreSQL – Luca Ferrari 179 di 182 Funzionamento del lock L'idea è semplice: il lock funziona come un mutex. Il processo backend detiene il lock dall'inizio. Quando il processo chiama una funzione Java (ad esempio perché scatta un trigger) rilascia il lock. La funzione Java acquisisce il lock. Quando la funzione Java termina il backend riacquisisce il lock. Così facendo il lock garantisce che ci sia sempre e solo un flusso di esecuzione: o il backend è in attesa della terminazione di Java oppure Java è in attesa della terminazione di un flusso di backend. Java & PostgreSQL – Luca Ferrari 180 di 182 Funzionamento del lock Backend Backend Java Java SELECT example1(); Esecuzione di una query risultati <fine> Java & PostgreSQL – Luca Ferrari 181 di 182 Utilizzo del lock Tutti i metodi della classe Portal.java (e delle classi che rappresentano oggetti del backend) utilizzano il lock come prima istruzione al fine di ottenere il lock dal backend. Ovviamente il lock va acquisito qualora si debba procedere con un metodo nativo. public int fetch(boolean forward, int count) throws SQLException { synchronized(Backend.THREADLOCK) { return _fetch(m_pointer, System.identityHashCode( Thread.currentThread()), forward, count); } Java & PostgreSQL – Luca Ferrari 182 di 182