giovedì 30 giugno 2011

Gestire le unita' di misura in PostgreSQL

Uno dei problemi di molti, se non tutti, sistemi gestionali e' quello di gestire diverse unita' di misura e le conversioni fra di esse. E' possibile creare nel database una serie di funzioni per la gestione delle unita' di misura e della loro conversione.
Prima di tutto occorre fare alcune assunzioni: (1) le unita' di misura sono legate ad un contesto (o dominio), che identifica a quale settore merciologico (o addirittura per quale articolo) esse hanno validita'. Se e' vero infatti che kg e km hanno sempre quel significato, scatole, pallet, blister, ecc. hanno configurazioni e parametri di conversione differenti a seconda della categoria merceologica e del loro dominio di applicazione.
Un'altra assunzione e' che la conversione fra le unita' di misura sia sempre lineare, ossia basata da un fattore moltiplicativo (o diviso) e da nessuno spiazzamento. Infine si suppone che per ogni dominio venga definita una unita' di misura "root" (principale) alla quale si fara' riferimento per le conversioni; tale unita' puo' cambiare nel tempo, ma ogni categoria ne deve avere una specifica. Tipicamente l'unita' root e' quella di produzione, mentre le altre unita' sono quelle di acquisto/vendita.

Si passa alla definizione di una tabella che conterra' i dati di tutte le unita' di misura definite:

CREATE TABLE units
(
  unitspk serial NOT NULL,
  unit text NOT NULL,
  domain text NOT NULL,
  scaling_factor real NOT NULL DEFAULT 1,
  root_for_domain boolean NOT NULL DEFAULT false,
  references_to_root integer,
  CONSTRAINT units_surrogate_primarykey PRIMARY KEY (unitspk),
  CONSTRAINT unit_to_unit FOREIGN KEY (references_to_root)
      REFERENCES units (unitspk) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT unique_units_domain_check UNIQUE (unit, domain)
)

In sostanza per ogni unita' di misura si defnisce il suo codice identificativo (es kg) e il suo dominio, nonché il fattore di conversione verso una root del dominio.
L'idea è quella di poter definire delle unita' di misura mediante delle funzioni come segue:

-- creazione di una unita' di base
select f_create_root_unit( 'pesi', 'g', 0);
-- creazione di altre unita'
select f_add_unit( 'pesi', 'kg', 1000, 'g' );
select f_add_unit( 'pesi', 'hg', 0.1, 'kg' );


Quello che si vede è la creazione dell'unita' base per i pesi: il grammo. In seguito si creano delle unita' facendo riferimento ad altre unita', quindi ad esempio il 'kg' corrisponde a 1000 grammi, mentre il 'hg' corrisponde a 0.1 'kg'.
Infine si possono definire delle funzioni di conversione come segue:

-- conversione verso root
select f_convert_units( 'pesi', 'kg', 30, 'g' );
       -- fornisce in uscita 30000 (30 kg = 30000 g)

-- conversione da root
select f_convert_units( 'pesi', 'g', 5, 'hg' );
       -- fornisce in uscita 0.05 (5 g = 0.05 hg)

-- conversioni fra due unita' non root
select f_convert_units( 'pesi', 'hg', 99, 'kg' );
       -- fornisce in uscita 9.9 (99 hg = 9.9 kg)
select f_convert_units( 'pesi', 'kg',  88, 'hg' );
       -- fornisce in uscita 880 (88 kg = 880 hg)

che molto semplicemente effettuano la conversione di una data quantita', ad esempio 30 kg in grammi.
Il "trucco" dietro alle conversioni e' dato dal contenuto della tabella che presenta i fattori di moltiplicazione/divisione e i riferimenti increciati fra una unita' e la sua root:

select unit, domain, scaling_factor from units;
 unit | domain | scaling_factor
------+--------+----------------
 g    | pesi   |              1
 kg   | pesi   |           1000
 hg   | pesi   |            100
(3 rows)

Come scritto sopra l'unità scelta come root potrebbe cambiare nel tempo, per questo la funzione f_create_root_unit accetta tre parametri: il terzo e' appunto un fattore di aggiustamento dalla root corrente a quella nuova.
Se si vuole modificare la root corrente (grammi) in milligrammi, considerando che un milligrammo corrisponde ad un millesimo di grammi, si avrà:

-- cambio la unita' root da grammi a milligrammi, considerando
-- che un grammo = 1000 milligrammi
select * from f_create_root_unit( 'pesi', 'mg', 1000 );


e il sistema aggiusterà tutti i fattori di conversione:


select unit, domain, scaling_factor from units;
 unit | domain | scaling_factor
------+--------+----------------
 mg   | pesi   |              1
 g    | pesi   |           1000
 kg   | pesi   |          1e+06
 hg   | pesi   |         100000



Come si nota la root ha sempre un fattore di conversione 1 (oppure 0), mentre le altre unita' sono state aggiustate di conseguenza.
Le funzioni di conversione operano nel seguente modo:
- se l'unita' di partenza e' la radice allora si divide la quantia' per lo scaling factor dell'unita' destinazione;
- se l'unita' di destinazione e' la radice allora si moltiplica per lo scaling factor dell'unita' di partenza la quantia';
- se nessuna delle due e' la radice allora si rapportano i due scaling factor per trovare quello definitivo

Analogamente la creazione di una nuova root va prima alla ricerca di root pre-esistenti e aggiusta tutti gli scaling factor di conseguenza.
Di seguito sono presentate le funzioni plpgsql.
Si consideri la funzione di creazione di una unita' radice per un dato dominio:


CREATE OR REPLACE FUNCTION f_create_root_unit(working_domain text, new_unit text, new_scaling real)
  RETURNS boolean AS
$BODY$
DECLARE
    /* dichiarazione variabili */
    old_domain_root_unit        integer;
    old_domain_root_unit_text    text;
    current_root_pk            integer;
    current_unit_pk            integer;
    current_root_scaling_factor    real;
BEGIN

    -- controllo
    /* se si tenta di inserire la root corrente non si fa nulla */
    SELECT unitspk, unit
    INTO   old_domain_root_unit, old_domain_root_unit_text
    FROM   units
    WHERE  root_for_domain = true
    AND    domain          = working_domain;

    IF old_domain_root_unit IS NOT NULL AND old_domain_root_unit_text = new_unit
    THEN
        RAISE INFO 'Impossibile aggiungere questa um come root, esiste!';
        return false;
    END IF;

    -- controllo
    /* potrebbe esserci il caso di agigunta unita' esistente come root */
    SELECT unitspk, scaling_factor
    INTO   current_root_pk, current_root_scaling_factor
    FROM   units
    WHERE  domain          = working_domain
    AND    root_for_domain = false
    AND    unit            = new_unit;

    IF current_root_pk IS NOT NULL
    THEN
        UPDATE units
        SET    root_for_domain    = false,
               scaling_factor     = scaling_factor / current_root_scaling_factor,
               references_to_root = current_root_pk
        WHERE  unitspk <> current_root_pk
        AND    domain = working_domain;

        UPDATE units
        SET    root_for_domain    = true,
               references_to_root = NULL
        WHERE  unitspk = current_root_pk;

        RETURN true;

           
    END IF;
   
   

    /* se esiste il dominio devo trovarne la root */
    SELECT unitspk, unit
    INTO   old_domain_root_unit, old_domain_root_unit_text
    FROM   units
    WHERE  root_for_domain = true
    AND    domain = working_domain;

    RAISE INFO 'Root (precedente) del dominio % = % %', working_domain, old_domain_root_unit, old_domain_root_unit_text ;


   
    /* inserimento dei valori nella tupla */
    INSERT INTO units( domain, unit, root_for_domain)
    VALUES( working_domain, new_unit, true);

    -- ottengo la nuova root
    SELECT unitspk
    INTO   current_root_pk
    FROM   units
    WHERE  domain = working_domain
    AND    unit   = new_unit;

    FOR  current_unit_pk IN     SELECT unitspk
                    FROM units
                    WHERE domain          = working_domain
                    AND   unitspk         <> current_root_pk
                    LOOP

        RAISE INFO 'Aggiustamento entry % alla nuova root %', current_unit_pk, current_root_pk;
        UPDATE units
        SET    scaling_factor     = scaling_factor * new_scaling,
               references_to_root = current_root_pk,
               root_for_domain    = false
        WHERE  domain             = working_domain
        AND    unitspk            = current_unit_pk;
                   
    END LOOP;

    -- aggiusto tutti gli scaling factor per puntare alla nuova root
   

    RAISE INFO 'Aggiustamento nuova root % %', current_root_pk, new_scaling;

    -- tutto ok
    RETURN true;

END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;
ALTER FUNCTION f_create_root_unit(text, text, real) OWNER TO postgres;

Di seguito la piu' semplice funzione di aggiunta di una unita' di misura:


CREATE OR REPLACE FUNCTION f_add_unit(working_domain text, new_unit text, scaling real, references_to_unit text)
  RETURNS boolean AS
$BODY$
DECLARE
    /* dichiarazione variabili */
    current_domain_root_unit    integer;
    current_domain_root_unit_text   text;
    current_domain            text;
    converting_function_name        text;
    function_query                  text;
BEGIN

    /* 1) trovo la root del dominio */
    SELECT unitspk, unit
    INTO   current_domain_root_unit, current_domain_root_unit_text
    FROM   units
    WHERE  root_for_domain = true
    AND    domain = working_domain;


    /* se non mi sto riferendo alla root devo calcolare
       il valore di scaling */
    IF current_domain_root_unit_text <> references_to_unit
    THEN
        SELECT scaling_factor * scaling
        INTO   scaling
        FROM   units
        WHERE  domain = working_domain
        AND    unit   = references_to_unit;
    END IF;
   

    RAISE INFO 'Root del dominio % = % %', working_domain, current_domain_root_unit, current_domain_root_unit_text ;


    /* 2) inserisco valori nella tupla */
    INSERT INTO units( unit, domain, scaling_factor, references_to_root )
    VALUES( new_unit, working_domain, scaling, current_domain_root_unit );

    -- tutto ok
    RETURN true;

END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;
ALTER FUNCTION f_add_unit(text, text, real, text) OWNER TO postgres;


Infine la funzione di conversione fra due unita' di misura:

CREATE OR REPLACE FUNCTION f_convert_units(working_domain text, from_unit text, quantity real, to_unit text)
  RETURNS real AS
$BODY$
DECLARE
    /* dichiarazione variabili */
    from_unit_record    units%rowtype;
    to_unit_record        units%rowtype;
    linear_multiplier    real;
BEGIN

    /* trovo le chiavi delle unita' di partenza e arrivo */
    SELECT *
    INTO   from_unit_record
    FROM   units
    WHERE  unit = from_unit
    AND    domain = working_domain;

    SELECT *
    INTO   to_unit_record
    FROM   units
    WHERE  unit = to_unit
    AND    domain = working_domain;




    -- se l'unita' di destinazion e' root il calcolo lineare e' corretto, altrimenti
    -- lo inverto
    IF to_unit_record.root_for_domain = false AND from_unit_record.root_for_domain = true
    THEN
        SELECT 1 / (to_unit_record.scaling_factor)
        INTO   linear_multiplier;
        RAISE INFO 'Conversione verso root %', linear_multiplier;

   
    ELSIF to_unit_record.root_for_domain = true AND from_unit_record.root_for_domain = false
    THEN
        SELECT 1 * (from_unit_record.scaling_factor )
        INTO   linear_multiplier;
        RAISE INFO 'Conversione da root %', linear_multiplier;

    ELSIF to_unit_record.root_for_domain = false AND from_unit_record.root_for_domain = false
    THEN
        -- conversione fra due unita' intermedie, si puo' fare agevolmente se si riferiscono
        -- alla stessa root
        IF from_unit_record.references_to_root = to_unit_record.references_to_root
        THEN
            SELECT ( (from_unit_record.scaling_factor ) / (to_unit_record.scaling_factor ) )
            INTO   linear_multiplier;
        END IF;
   
    END IF;

    -- tutto fatto
    RETURN quantity  * linear_multiplier;

END;
$BODY$
  LANGUAGE plpgsql VOLATILE
  COST 100;
ALTER FUNCTION f_convert_units(text, text, real, text) OWNER TO postgres;

Ovviamente le funzioni proposte qui sono a puro scopo didattico/dimostrativo, ulteriori controlli di coerenza dovrebbero essere implementati.
Per le trasformazioni non lineari una possibile soluzione e' quella di creare al volo delle funzioni di conversione specifiche e di usare tali funzioni per ogni conversione.

Software As A Service (SAAS): considerazioni

L'evoluzione del WWW sta portando rapidamente lo sviluppo e il deployment delle applicazioni a cambiare: si va sempre di piu' verso una condizione di Software As A Service (SAAS). L'idea e' quella di non installare piu' presso il cliente lo stack software, di qualunque natura sia, ma di permettere al cliente di accedere remotamente ad una installazione software disponibile (in esclusiva) presso il fornitore. Per rendere piu' chiaro il concetto, la differenza e' fra installare un server di posta aziendale internamente all'azienda oppure registrare l'azienda presso un fornitore esterno di servizi di posta elettronica. E la stessa cosa puo' essere fatta per software gestionali, documentali, di backup, fino alla virtualizzazione di storage e server interi.
Eppure questa variazione, sicuramente vantaggiosa in molti casi, produce qualche piccola contro-indicazione sui fornitori.

Anzitutto il cliente si aspetta che il "servizio" (non il software) sia sempre disponibile, sempre on-line, sempre aggiornato, sempre presidiato. Non si e' piu' disposti ad attendere downtime o periodi di inattivita' per consentire gli upgrade di versione. Non avendo lo stack software in casa, il cliente non sente suo il software, e di conseguenza non percepisce il problema relativo agli aggiornamenti. Ad accentuare cio' il fatto che il fornitore non e' piu' fornitore di prodotti e servizi, ma solo di servizi. Se si vende un prodotto (es. automobile) ad un cliente, questo comprende l'importanza e la necessita' della manutenzione sul prodotto stesso. Ma se al cliente si vende un servizio (es. trasporto in autobus), questo non accettera' mai di avere dei blocchi a causa di mancata manutenzione da parte del fornitore. Seppur semplici, queste considerazioni spostano il carico di lavoro e la necessita' di investimenti per le infrastrutture sulle spalle del fornitore stesso.

Ma c'e' anche un'altra problematica, questa volta da parte degli sviluppatori. Il fatto di avere lo stack software in casa, disponibile, "vicino" e soprattutto il fatto di non avere piu' il veto del cliente, consente agli sviluppatori di fare deployment continui. Se da un lato questo rappresenta una occasione di aggiornamento continuo, dall'altro spinge gli sviluppatori a programmare con meno cura, poiche' sanno che potranno sempre correggere velocemente il problema. Inoltre le modifiche allo stack saranno spesso visibili a tutti i clienti, e spesso non saranno nemmeno necessarie al cliente stesso, che pero' si vedra' addebitati i canoni di manutenzione e aggiornamento.

Con tutto questo non voglio dire che il modello SAAS sia sbagliato, ma invito a riflettere chiunque intenda intraprendere questa strada a testa bassa.

martedì 28 giugno 2011

Git reflogs

Git e' il sistema per la gestione delle revisioni che preferisco e che uso per la maggior parte dei miei progetti, alternando al suo fratello Mercurial.
Come e' noto, la capacita' di Git di lavorare con intricate connessioni di commit e di avere una storia non lineare e' superba. Esiste tuttavia un punto ove anche Git memorizza la storia in modo lineare: i reflogs.
I reflogs sono liste di identificativi SHA1, gli stessi usati per identificare gli oggetti in Git (commit, tag, ...). Lo scopo dei reflogs e' di associare ad una azione effettuata su un branch, ad esempio un commit, un punto nella storia. Ogni volta che un branch viene aggiornato (commit, checkout, merge, tag, rebase) viene memorizzata un'entry nel reflog indicando il tipo di azione, l'hash SHA1 di partenza, ove ci si trovava rispetto ad HEAD (nel tempo) e il messaggio descrittivo dell'azione.

Il seguente listato mostra una parte di reflog del progetto JFK:

$ git reflog
f16c3e0 HEAD@{0}: commit: Code clean up.
509b537 HEAD@{1}: commit: Inserted single includes for resources in the test goal.
c2d782b HEAD@{2}: checkout: moving from e3c164d49d5e50c42f90b9d142ef64ddc77c175c to master
e3c164d HEAD@{3}: checkout: moving from master to e3c164d49d5e50c42f90b9d142ef64ddc77c175c
c2d782b HEAD@{4}: commit (amend): Refactoring of the repository in order to use Maven for t
4c26949 HEAD@{5}: merge maven: Fast-forward
e3c164d HEAD@{6}: checkout: moving from maven to master
4c26949 HEAD@{7}: checkout: moving from e3c164d49d5e50c42f90b9d142ef64ddc77c175c to maven
e3c164d HEAD@{8}: checkout: moving from master to e3c164d49d5e50c42f90b9d142ef64ddc77c175c
e3c164d HEAD@{9}: checkout: moving from maven to master
4c26949 HEAD@{10}: checkout: moving from master to maven
e3c164d HEAD@{11}: checkout: moving from maven to master
4c26949 HEAD@{12}: commit: Refactoring of the repository in order to use Maven for the comp
e3c164d HEAD@{13}: checkout: moving from master to maven


Partendo dal fondo si ha che 13 modifiche prima dell'HEAD corrente (attenzione, le modifiche non sono per forza dei commit) si e' effettuato un checkout che ha portato HEAD a puntare dal ramo "master" a quello "maven". Analogamente si nota come "11 modifiche fa" si sia passati dal ramo "maven" a quello "master" per poi tornare nuovamente al ramo "maven" (10 modifiche fa).
L'hash riportato nella prima colonna indica a cosa puntava HEAD quando si e' svolta l'azione. Ad esempio nel caso della modifica 12 si aveva che HEAD puntava a 4c26949 (ramo "maven"); a seguito del cambio di ramo e successivo ritorno al ramo originario "maven" la HEAD e' tornata ad essere 4c26949. Questo perche' l'operazione 11 e' stata annullata dalla 10 che di fatto ha riportato la situazione di HEAD alla modifica 12. Analogamente la modifica 0 (HEAD corrente) punta alla HEAD corrente, come e' facile verificare con:

$ git log
commit f16c3e04a6220e5c246dda75832d3cb47890eb37


Riassumendo quindi si puo' dire che i reflog rappresentano un giornale delle azioni che hanno modificato la HEAD corrente (non importa su quale ramo si stia lavorando), e visto che ogni azione Git va a modificare la HEAD (commit, rebase, tag, branch, ...) si ha come risultato che i reflogs rappresentano la storia lineare del processo di sviluppo. In altre parole i reflogs sono il diario delle azioni svolte dagli sviluppatori sul repository, e volendo usare un brutto paragone sono una sorta di ".bash_history" del repository.

sabato 18 giugno 2011

Maven settings: repository path & proxy

La configurazione del processo di compilazione di Maven puo' essere modificata opportunamente mediante il file che si trova in $HOME/.m2/settings.xml. Inizialmente potrebbe essere necessario creare tale file, poiche' Maven non lo inizializza di default. Due personalizzazioni molto utili da inserire in tale file sono:
 
  • localRepository: configura il percorso del repository affinche' non sia, in default, in $HOME/.m2. Questo e' utile quando si vuole, ad esempio, tenere i jar scaricati su una partizione differente (magari condivisa).
  • proxy: consente la configurazione di un proxy, con eventuale autenticazione, per il download delle risorse.

Un esempio di file che utilizza entrambe le funzioni di cui sopra e' il seguente:



 <settings>

  <localRepository>/sviluppo/java/jars</localRepository>


  <proxies>
   <proxy>
      <active>true</active>
      <protocol>http</protocol>
      <host>192.168.1.76</host>
      <port>8080</port>
      <username>luca</username>
      <password>password</password>
    </proxy>
  </proxies>

</settings>

mercoledì 15 giugno 2011

Maven & Log4J: dependency error

Maven e' un ottimo sistema di compilazione/configurazione di un progetto Java, e la scelta di usare degli artifacts che vengono scaricati e configurati automaticamente e' veramente eccellente. Mai mi sarei aspettato di ottenere un errore piuttosto criptico da un progetto molto semplice che richiedeva la presenza di log4j. Eseguendo il processo di compilazione ottenevo l'errore:

[ERROR] Failed to execute goal on project XYZ: Could not resolve dependencies for project XYZ:XYZ:jar:0.2-STABLE: The following artifacts could not be resolved: com.sun.jdmk:jmxtools:jar:1.2.1, com.sun.jmx:jmxri:jar:1.2.1: Could not transfer artifact com.sun.jdmk:jmxtools:jar:1.2.1 from/to java.net (https://maven-repository.dev.java.net/nonav/repository): No connector available to access repository java.net (https://maven-repository.dev.java.net/nonav/repository) of type legacy using the available factories WagonRepositoryConnectorFactory


Ebbene l'errore di cui sopra e' generato dall'impossibilita' di scaricare alcuni jar dal sito di java.net, ma a parte questo, essendo il mio progetto molto semplice, tali jar non erano nemmeno richiesti! Il problema e' nelle dipendenze di Log4J che dalla versione 1.2.15 hanno delle dipendenze sui suddetti package. Ma se tali package non vengono usati e' possibile escluderli dal processo di compilazione/download modificando il file pom come segue:

<dependency>
      <groupId>log4j</groupId>
      <artifactId>log4j</artifactId>
      <version>1.2.15</version>
      <scope>compile</scope>
<exclusions>
    <exclusion>
      <groupId>javax.mail</groupId>
      <artifactId>mail</artifactId>
    </exclusion>
    <exclusion>
      <groupId>javax.jms</groupId>
      <artifactId>jms</artifactId>
    </exclusion>
    <exclusion>
      <groupId>com.sun.jdmk</groupId>
      <artifactId>jmxtools</artifactId>
    </exclusion>
    <exclusion>
      <groupId>com.sun.jmx</groupId>
      <artifactId>jmxri</artifactId>
    </exclusion>
  </exclusions>

    </dependency>


Con questa modifica la compilazione di progetti che richiedono le funzionalita' "standard" di Log4J avverra' senza problemi.

martedì 14 giugno 2011

JFK update: Maven setup

After a few commits to make JFK available as an Apache Ant based project  I decided to switch to the more useful Apache Maven. Now JFK has a Maven pom.xml file that allows for compilation, downloading of dependencies and test execution.
To compile the project just type:
  
   mvn compile

and the system will start download dependencies and all required jars to compile the project. To run the tests just type:

    mvn test

Note that within this commit the repository layout has changed in order to respect the Maven structure; in particular:
  • -the "src" folder now has "main" for the JFK code and "test" for the test suite;
  • the "src/main/resource" folder is the one that contains the log4j and spring configuration XML files;
  • the above XML files have changed name: "jfk.log4j.xml" and "jfk.spring-beans.xml"


lunedì 13 giugno 2011

WhiteCat & Maven

In the last days I spent a few hours refactoring the WhiteCat source tree in order to be compliant with Apache Maven. Now it is possible to download the tree and have Maven to compile and run the test suite without having to worry about manually set up dependencies and jar files.

To compile the tree just type

mvn compile
while to run all the tests just run

mvn test

It is worth noting that this refactoring has allowed me to discovered a few bugs that have been fixed.