venerdì 14 marzo 2008

Gestione di sequenze alfanumeriche

Quando si lavora con applicativi interfacciati a database relazionali si ha spesso la necessità di far generare all'applicativo o al database stesso dei codici sequenziali. Molti RDBMS consentono di creare sequenze automatiche tramite delle sequence (tabelle speciali il cui valore cambia ad ogni lettura) o campi auto-increment. Il problema è che le sequenze così generate sono numeriche, mentre spesso sono necessarie sequenze alfanumeriche (ad esempio articolo001, articolo002, articolo003,...). Il problema non è sempre risolvivibile direttamente dal lato database, e quindi deve essere affrontato molto spesso dal lato applicativo.

In questo articolo viene descritta una possibile soluzione Java, capace di adattarsi a diversi database e contesti applicativi. Successivamente, viene illustrata una possibile soluzione lato database implementata su PostgreSQL.
Entrambe le soluzioni si appoggiano ad una sequenza lato database per la generazione della parte numerica del codice. Il codice qui illustrato è a scopo puramente didattico.

Soluzione Java

L'idea è quella di costruire una serie di classi che consentano di ottenere il prossimo valore alfanumerico di una sequenza. Per fare questo occorre anzitutto predisporre un tipo Sequence che possa essere usato per gestire i vari tipi di sequenza che, lato applicativo, si vogliono gestire (es. alfanumerici, numerici, ecc.):

public interface Sequence { }
Come si può notare l'interfaccia Sequence non definisce nessun metodo in particolare, lasciando alle sue concrete implementazioni la definizione dei tipi di dato da gestire. In altre parole, l'interfaccia Sequence è un semplice segnaposto (tag) per gestire una serie di tipi concreti. Una possibile estensione della Sequence è quella che consente di gestire le sequenze alfanumeriche:

public interface StringSequence extends Sequence {

/**
* Returns the next value of the sequence. This value could come from a database sequence (in such case
* this method will simply query the database) or can be calculated by the program.
* @return the sequence string.
*/
public String nextValue();


/**
* Sets up this sequence. Setting up a sequence means initialize it to run the nextValue() method.
* @param sequencePrefix a prefix to place in the beginning of the string
* @param sequenceName the name of the sequence in the database (if supported) or null if no sequence must
* be queried (or the database does not support it).
*/
public void setUp(String sequencePrefix, String sequenceName);
}

L'interfaccia StringSequence prevede due metodi fondamentali: setUp(..) che serve ad inizializzare il generatore di codice, e nextValue() che fornisce il prossimo valore dalla sequenza. In particolare, il metodo setUp(..) accetta come parametri un prefisso da appendere alle stringhe generate ad ogni chiamata a nextValue() e il nome della sequenza numerica (lato database) da usare.

Una possibile implementazione concreta del generatore di stringhe può essere la seguente:

public class PostgresqlSequence implements StringSequence, SerialSequence {

/**
* Each sequence generated by this genertor will have this prefix.
*/
protected String sequencePrefix = "articolo-";


/**
* The table to query to get the next sequence value.
*/
protected String sequenceTable = "";


/**
* Builds a sequence generator with the specified sequence table and the specified prefix.
* @param sequenceName the name of the sequence on the database to query
* @param sequencePrefix the prefix of the sequence generated
*/
protected PostgresqlSequence(String sequenceName, String sequencePrefix){
super();
this.setUp(sequencePrefix, sequenceName);
}


/**
* Sets up the sequence generator.
*/
public void setUp(String prefix, String sequenceName){
// avoid to store a null sequence name
if( prefix == null )
this.sequencePrefix = "";
else
this.sequencePrefix = this.sequencePrefix;

// store the sequence name
this.sequenceTable = sequenceName;
}


/**
* Returns the sequence key string as union of the string fixed in this class and the number that comes
* from the database sequence.
*/
public String nextValue() {
String ret = null;
Statement st = null;
ResultSet rs = null;

try{
/* connect to the database with your own code */
Connection connection = // database connection
st = connection.createStatement();
rs = st.executeQuery("SELECT nextval('" + this.sequenceTable + "')");
if( rs!= null && rs.next() ){
int seqVal = rs.getInt(1);
ret = this.sequencePrefix + seqVal;
}
else
ret = null;

// close database resources
rs.close();
st.close();

}catch(SQLException e){
// handle errors
return null;
}

return ret;

}
}


Come si può notare, il costruttore della classe richiama immediatamente il metodo setUp(..) fornendo come dati il nome della sequenza da interrogare e quello del prefisso da usare nella restituzione dei dati. Una volta inizializzata, l'istanza del generatore di codici provvede ad interrogare il database ad ogni chiamata di nextValue() e a restituire una stringa formata dalla composizione del valore restituito dalla sequenza del database e dal codice da usarsi come prefisso.

Un esempio di utilizzo del generatore di codici è il seguente:

StringSequence sequence = new PostgresqlSequence("test_pk_seq","articoli_");
String code = sequence.nextValue(); // produce qualche cosa del tipo articoli_763

Per rendere l'architettura sopra descritta maggiormente portabile, è possibile utilizzare una SequenceFactory che fornisca un riferimento ad un oggetto Sequence (in particolare StringSequence) in modo trasparente e a seconda della configurazione del sistema.


Soluzione PostgreSQL

L'idea è quella di creare una funzione che interroghi la sequenza opportuna e restituisca la concatenazione della stringa di prefisso con il valore della sequenza stessa.
Un primo modo può essere quello di creare una funzione specifica per ogni sequenza che debba essere interroga. Supponendo di avere una sequenza test_pk_seq è possibile utilizzare la seguente funzione plpgsql:



/**
* Esempio di uso:
* select next_test_alphanumeric_value('articolo-test');
* che ritorna un valore simile a
* articolo-test27
*/
CREATE OR REPLACE FUNCTION next_test_alphanumeric_value(prefix character varying)
RETURNS character varying
AS $BODY$
DECLARE
/* it will contain the next computed value */
next_key character varying;
BEGIN
raise debug 'Querying the test_pk_seq sequence';
SELECT prefix || nextval('test_pk_seq') INTO next_key;
return next_key;
END;


Come si può notare la funzione è molto semplice: viene accettato come parametro unico la stringa da usare come prefisso nella generazione del codice alfanumerico, e questo viene concatenato all'interrogazione della sequenza test_pk_seq. Questo metodo, seppur semplice, ha il forte svantaggio di non essere adattabile a sequenze differenti: occorrerà implementare una funzione specifica per ogni sequenza da interrogare.


Una soluzione migliore consente di specificare, oltre alla stringa di prefisso, anche il nome della sequenza da interrogare. In questo modo, con una sola funzione, è possibile ottenere i valori da più sequenze. L'idea è simile a quella vista in precedenza: si concatena la stringa di prefisso al valore ottenuto dalla sequenza. Essendo però questa volta la sequenza non nota a priori, è necessario costruire la query SQL dinamicamente:

/**
* Esempio di uso:
* select next_alphanumeric_value('articolo-','test_pk_seq');
* che ritorna un risultato simile a
* articolo-34
*/
CREATE OR REPLACE FUNCTION next_alphanumeric_value(prefix character varying, sequence_name character varying)
RETURNS character varying AS
$BODY$

DECLARE
/* it will contain the next computed value */
next_key character varying;

/* the query string dynamically built */
query_string character varying;

BEGIN
raise debug 'Sequence used %', sequence_name;
query_string := 'SELECT ' || quote_literal(prefix) || '|| nextval(' || quote_literal(sequence_name) || ');';
raise debug 'Dynamic query %', query_string;
execute query_string into next_key;
return next_key;
END;

$BODY$
LANGUAGE plpgsql VOLATILE;
ALTER FUNCTION next_alphanumeric_value(character varying, character varying) OWNER TO luca;
Come si può notare, viene anzitutto costruita una stringa che rappresenta la query da eseguire, ossia uno statement SELECT con la concatenazione della stringa di prefisso e del nextval della sequenza. Questa query viene poi fatta eseguire tramite il comando execute e il risultato viene memorizzato nella variabile ritornata dalla funzione. Si noti che, siccome la funzione nextval(..) si aspetta un argomento stringa, il nome della sequenza passato come parametro deve essere racchiuso da apici, e quindi si utilizza la funzione quote_literal(..) per ottenere la relativa stringa SQL.

Nessun commento: