L’utile costruttore

PL\SQL è un linguaggio procedurale ma è possibile adottare uno stile di scrittura orientato alla programmazione ad oggetti.

In un package PL\SQL è frequente dover definire un modello dati in memoria a supporto degli algoritmi di elaborazione.
Per far ciò, io personalmente preferisco utilizzare TYPE RECORD definiti all’interno del package stesso invece di TYPE OBJECT, perché ciò rende il package auto consistente.

Dato un TYPE RECORD è possibile definire uno o più RECORD di questo tipo di dato. L’inizializzazione di queste variabili può essere effettuati in punti diversi del codice nei modi più diversi, ma non si ha alcuna garanzia che il record venga inizializzato sempre in modo completo e corretto.
Questo rischio aumenta quando è necessario estendere il TYPE RECORD aggiungendo un attributo per esempio.
Il rischio è che, le procedure che utilizzano tali RECORD, risentano della inizializzazione incoerente di questi e che ciò produca errori a run-time.

Il concetto di costruttore è una soluzione efficace a questo problema: quando si usa una struttura dati, prima di usarla, è necessario avere garanzia che essa sia stata correttamente inizializzata.

Nel nostro caso l’idea è di considerare un TYPE RECORD l’equivalente di una CLASS e definire una FUNCTION che svolga il ruolo di costruttore.
Il costruttore accetta in ingresso tutta una serie di parametri che consentono di inizializzare il RECORD e restituisce il RECORD stesso.
La regola non scritta, si parla appunto di stile di programmazione, è che prima di usare RECORD di questo tipo si invoca sempre il suo costruttore.

SUBTYPE tItemId IS VARCHAR2(50);
SUBTYPE tCurrency IS NUMBER;

TYPE tItem IS RECORD(
   id     tItemId
  ,descr  VARCHAR2(255)
  ,price  tCurrency
  ,qty    NUMBER
  ,total  tCurrency
);
FUNCTION itemNew RETURN tItem IS
  this tItem :=NULL;
BEGIN
  this.qty   := 0;
  this.total := 0;
  RETURN this;
END;

FUNCTION itemNew(
   id IN tItemId
  ,descr IN VARCHAR2
  ,price  tCurrency
  ,qty    NUMBER
) RETURN tItem IS
  this tItem :=itemNew();
BEGIN
  this.id := id;
  this.descr := descr;
  this.price := price;
  this.qty := qty;
  RETURN this;
END;
DECLARE
  item tItem := itemNew();
BEGIN
  item := itemNew(id=>'ID123',descr=>'HAMMER',price=>1.3,qty=>2);
END;

Questo approccio può sembrare costoso a prima vista ma in realtà è semplice e facilmente ripetibile.

Il suo utilizzo rende il codice facilmente comprensibile.
Inoltre nel caso in cui si debba estendere il il TYPE RECORD, alterando il costruttore, avremo garanzia che l’intervento venga effettuato in modo coerente in tutti i punti del codice in cui il costruttore è invocato.

Il consiglio è di predisporre sempre un costruttore base non accetta alcun parametro e che inizializza tutti gli attributi ad un valore di default: nel gergo dei programmatori C un costruttore VOID.

Naturalmente sfruttando l’OVERLOADING è possibile definire una famiglia di costruttori che espongono parametri diversi in base alle varie esigenze applicative.

Un cronografo piccolo piccolo

A volta capita che una procedura PL\SQL sia lenta.
In questi casi per individuare le porzioni di codice lento è utile misurarne il tempo di esecuzione.
Per far ciò ho realizzato un package che implementa un cronografo.

CREATE OR REPLACE PACKAGE CRONO IS
   TYPE tTimer IS RECORD(
      s TIMESTAMP
     ,e TIMESTAMP
   );
 FUNCTION  new     RETURN tTimer;
   PROCEDURE stop    (this IN OUT NOCOPY tTimer);
   PROCEDURE reset   (this IN OUT NOCOPY tTimer);
   FUNCTION  time    (this IN OUT NOCOPY tTimer) RETURN VARCHAR2;
   FUNCTION  hour    (this IN OUT NOCOPY tTimer) RETURN NUMBER;
   FUNCTION  minute  (this IN OUT NOCOPY tTimer) RETURN NUMBER;
   FUNCTION  second  (this IN OUT NOCOPY tTimer) RETURN NUMBER;
 END;
CREATE OR REPLACE PACKAGE BODY CRONO IS
  
  FUNCTION  new RETURN tTimer IS
    this tTimer := NULL;
  BEGIN
    this.s := SYSTIMESTAMP;
    this.e := NULL;
    RETURN this;
  END;
  
  PROCEDURE starting(this IN OUT NOCOPY tTimer)IS
  BEGIN
    this.s := SYSTIMESTAMP;
  END;
  
  PROCEDURE stop (this IN OUT NOCOPY tTimer) IS 
  BEGIN
    this.e := SYSTIMESTAMP;
  END;
  
  PROCEDURE reset (this IN OUT NOCOPY tTimer) IS
  BEGIN
    this.s := SYSTIMESTAMP;
    this.e := NULL;
  END;

  FUNCTION  time (this IN OUT NOCOPY tTimer) RETURN VARCHAR2 IS
    ret VARCHAR2(32767):=NULL;
  BEGIN
    ret := this.e - this.s;
    
    ret := ret || ' ' || 'h:' || (extract(hour from this.e - this.s));
    ret := ret || ' ' || 'm:' || (extract(minute from this.e - this.s));
    ret := ret || ' ' || 's:' || (extract(second from this.e - this.s));
    
    RETURN ret;
  END;

  FUNCTION  hour (this IN OUT NOCOPY tTimer) RETURN NUMBER IS
    h NUMBER :=NULL;
  BEGIN
    h := (extract(hour from this.e - this.s));
    RETURN h;
  END;

  FUNCTION  minute (this IN OUT NOCOPY tTimer) RETURN NUMBER IS
    m NUMBER :=NULL;
  BEGIN
    m := (extract(minute from this.e - this.s));
    RETURN m;
  END;

  FUNCTION  second (this IN OUT NOCOPY tTimer) RETURN NUMBER IS
    s NUMBER :=NULL;
  BEGIN
    s := (extract(second from this.e - this.s));
    RETURN s;
  END; 
END;

Il suo utilizzo è molto semplice.
A seguire un esempio:

DECLARE
  t1 CRONO.tTimer; --Variabile di tipo cronometro

  --Codice che consuma tempo
  PROCEDURE NOP(cycle IN NUMBER)IS
  BEGIN
    FOR i in 1..cycle LOOP
      DECLARE
       c NUMBER := 0;
      BEGIN
        select 1 into c from dual;
      END;
    END LOOP;
  END;

BEGIN
  --Il cronometro t1 viene creato ed avviato
  t1 := CRONO.new();
  
  NOP(cycle=>1000);
   
  --Il cronometro t1 viene fermato
  CRONO.stop(t1);

  --Si stampa il tempo misurato dal cronometro t1
  DBMS_OUTPUT.PUT_LINE('Tempo trascorso A:' || CRONO.time(t1));

  --Si resetta il cronometro t1
  CRONO.reset(t1); 
  
  NOP(cycle=>500);
 
  --Il cronometro t1 viene fermato
  CRONO.stop(t1);
   
  --Si stampa il tempo misurato dal cronometro t1
  DBMS_OUTPUT.PUT_LINE('Tempo trascorso B:' || CRONO.time(t1));
 
END;