UDF e parametri NULL
Non usare NULL
È un errore abbastanza frequente utilizzare NULL per scopi diversi da quelli per cui è
stato concepito. Consideriamo, per esempio, una tabella anagrafica con la colonna SECONDO_NOME. In questa tabella
è scorretto utilizzare NULL, al posto di una stringa vuota, in corrispondenza di persone che mancano
del secondo nome: NULL indica una grandezza
sconosciuta, non un valore
mancante/
non assegnato o uno spazio
vuoto. Utilizzare NULL per l'attributo SECONDO_NOME
significa "questa persona probabilmente ha un secondo nome, ma non lo conosciamo" (
NON
significa "questa persona non ha un secondo nome").
Esempio
CREATE TABLE tab (
NOME VARCHAR(32) NOT NULL,
SECONDO_NOME VARCHAR(32) NOT NULL,
COGNOME VARCHAR(32) NOT NULL,
NOME_COMPLETO COMPUTED BY
(CapitalizeUDF(NOME || ' ' || SECONDO_NOME || ' ' || COGNOME)) );
Senza il vincolo NOT NULL, la concatenazione produrrebbe come risultato NULL in presenza di
un attributo nullo.
Non chiamare funzioni UDF con parametri NULL
Gran parte delle funzioni UDF sono del tipo
write null through, vale a dire che restituiscono
NULL nel caso di argomenti NULL. Se la situazione è questa, non è necessaria alcuna chiamata alla funzione perché
conosciamo già il valore che verrà calcolato.
Esempio 1
UPDATE tab
SET tipo_prodotto = LowerUDF (tipo_prodotto)
WHERE tipo_prodotto IS NOT NULL;
Esempio 2
CREATE TRIGGER tr FOR tab BEFORE UPDATE AS
BEGIN
IF (NEW.tipo_prodotto IS NOT NULL) THEN
NEW.tipo_prodotto = LowerUDF (NEW.tipo_prodotto);
END
Esempio 3
SELECT ID, Sin(angolo) as SINE FROM tab WHERE angolo IS NOT NULL
UNION ALL
SELECT ID, CAST(NULL AS DOUBLE PRECISION) FROM tab WHERE angolo IS NOT NULL;
Controllare i valori NULL nell'applicazione (client side)
Una domanda frequente nei newsgroup è
Come posso realizzare una funzione UDF che converta un valore
NULL in una stringa vuota? Mi serve per concatenare Nome+SecondoNome+Cognome. Questa operazione è,
di fatto, una
formattazione di una stringa, per cui è meglio eseguirla nell'applicazione, dove
è semplice identificare i NULL.
Esempio
Al posto di:
SELECT NOME || ' ' || SECONDO_NOME || ' ' || COGNOME FROM …;
selezionare separatamente le componenti:
SELECT NOME, SECONDO_NOME, COGNOME FROM …;
ed eseguire la formattazione (concatenazione) nell'applicazione (cosa che, fra l'altro, permette di
avere un unico spazio di separazione nel caso di secondo nome mancante).
Cosa succede chiamando una funzione UDF con un argomento NULL?
È semplice, la funzione UDF riceverà un valore zero. Per una stringa si tratterà di una
"stringa di lunghezza zero", per una data "data con offset zero" rispetto alla data di base (che
corrisponde al 1858-11-17)… Non potrete distinguere (all'interno della funzione UDF) se il
parametro sia zero o NULL. Talvolta questo comportamento risulta utile.
Esempio
CREATE TRIGGER tr FOR tab BEFORE UPDATE AS
BEGIN
NEW.NOME = TrimUDF (NEW.NOME);
NEW.COGNOME = TrimUDF (NEW.COGNOME);
…
/* l'effetto collaterale della chiamata a TrimUDF è la conversione da NULL a stringa vuota */
END
Passare argomenti diversi da stringhe convertendoli in stringa
Quando si ha bisogno di passare argomenti numerici (ovvero date, time, timestamp), è possibile scrivere
una funzione UDF che accetta stringhe come parametri in ingresso. In questo modo, zero sarà convertito
nella stringa '0', mentre NULL diverrà la stringa vuota ''.
Esempio
Questa funzione restituisce il valore assoluto di un numero o -1 nel caso di argomento scorretto o NULL.
function TEST_Abs (val: PChar): integer; cdecl;
begin
try
Result := Abs( StrToInt(val) );
except
Result := -1;
end;
end;
DECLARE EXTERNAL FUNCTION TEST_ABS
CSTRING(20)
RETURNS INTEGER BY VALUE
ENTRY_POINT 'TEST_Abs' MODULE_NAME 'UDF_Examples';
SELECT I, TEST_Abs(I) FROM TAB;
I TEST_ABS
======== ========
5 5
-5 5
0 0
<null> -1
Argomento composto da singolo carattere concatenato ad una stringa
Il risultato della concatenazione di una stringa con il valore NULL ritorna NULL. Dunque nel caso
di questa chiamata:
MyUDF ('*' || MyField)
un argomento di lunghezza zero indica NULL.
Esempio
Questa funzione restituisce la lunghezza della stringa in ingresso dopo averle applicato l'operazione
Trim, oppure -1 nel caso di argomento NULL:
function TEST_TrimLen (val: PChar): integer; cdecl;
begin
Result := -1;
if val[0] = #0 then Exit;
Inc(val); // Salta il carattere.
Result := Length(Trim(val));
end;
DECLARE EXTERNAL FUNCTION TEST_TRIMLEN
CSTRING(20)
RETURNS INTEGER BY VALUE
ENTRY_POINT 'TEST_TrimLen' MODULE_NAME 'UDF_Examples';
SELECT X || '<', TEST_TrimLen('#' || X) FROM TAB2;
TEST_TRIMLEN
=========== ============
ABCD< 4
EFGH < 4
XYZ < 3
< 0
<null> -1
Ricorso ad un parametro indicatore
È possibile utilizzare un parametro supplementare destinato ad identificare un argomento di
tipo NULL:
IF (IntegerVariable IS NOT NULL)
THEN Result = MyUDF(IntegerVariable, 1);
ELSE Result = MyUDF(IntegerVariable, 0);
oppure
MyUDF(IntegerVariable, 1+0*IntegerVariable)
In questa situazione è sufficiente controllare il secondo argomento della funzione: 0 indica NULL, 1
NOT NULL.
Parametri di tipo Blob
Identificare il valore NULL in un argomento di tipo Blob è semplice perché un Blob viene
passato alla funzione UDF come un puntatore ad una struttura. Se l'argomento è NULL, allora il
valore blob_handle della struttura sarà nullo. La dichiarazione di una struttura BLOB può essere reperita
nella
Developer's Guide o nei file
jrd/val.h o
include/ibase.h (solo
Firebird).
Esempio
Questa funzione ritorna la dimensione del blob o -1 nel caso di parametro NULL.
type
TBlob = record
GetSegment : Pointer;
BlobHandle : ^Integer;
SegmentCount : LongInt;
MaxSegmentLength : LongInt;
TotalSize : LongInt;
PutSegment : Pointer;
end;
PBlob = ^TBlob;
funtion TEST_BlobSize(inBlob: PBlob): integer; cdecl;
begin
Result := -1;
if (not Assigned(inBlob)) or
not Assigned(inBlob^.BlobHandle)) then Exit;
Result := inBlob^.TotalSize;
end;
DECLARE EXTERNAL FUNCTION TEST_BlobSize
BLOB
RETURNS INTEGER BY VALUE
ENTRY_POINT 'TEST_BlobSize' MODULE_NAME 'UDF_Examples';
CREATE TABLE TAB3 (I INTEGER, B BLOB);
INSERT INTO TAB3 (I,B) VALUES (1, 'abc');
INSERT INTO TAB3 (I,B) VALUES (2, '');
INSERT INTO TAB3 (I,B) VALUES (3, NULL);
/* L'esempio si riferisce a Firebird. */
/* Con InterBase può esser necessario utilizzare una funzione di conversione String->Blob. */
SELECT I, TEST_BlobSize(B), FROM TAB3;
I TEST_BLOBSIZE
======= =============
1 3
2 0
3 -1
Utilizzare i descrittori (descriptor) di Firebird
Di solito i parametri vengono passati alle funzioni UDF per riferimento. Firebird supporta un'ulteriore
modalità: il passaggio mediante descrittore che, fra le altre cose, permette una semplice identificazione
dei valori NULL (il meccanismo dei descrittori è presente in tutte le versioni di IB, però solamente in
Firebird sono stati risolti alcuni bug ed è stata specificata l'opportuna sintassi per le dichiarazioni).
La dichiarazione completa della struttura PARAMDSC si trova nel file include/ibase.h (solo Firebird).
Esempio
Questa funzione ritorna il valore assoluto o -1 in caso di argomento NULL.
const
DSC_null = 1;
type
ParamDsc = record
dsc_dtype : Byte;
dsc_scale : ShortInt;
dsc_length : Word;
dsc_sub_type : SmallInt;
dsc_flags : Word;
dsc_address : Pointer;
end;
PParamDsc = ^ParamDsc;
PInteger = ^Integer;
function TEST_Abs_Dsc (val :PParamDsc): integer; cdecl;
begin
Result := -1;
if (val=nil) or ((val^.dsc_flags and DSC_null) <> 0) then Exit;
// Questo è solo un esempio. Una funzione UDF reale dovrebbe controllare dsc_dtype.
Result := Abs( PInteger(val.dsc_address)^ );
end;
DECLARE EXTERNAL FUNCTION TEST_ABS_DSC
INTEGER BY DESCRIPTOR
RETURNS INTEGER BY VALUE
ENTRY_POINT 'TEST_Abs_Dsc' MODULE_NAME 'UDF_Examamples';
SELECT I, TEST_Abs_DSC(I) FROM TAB;
I TEST_ABS_DSC
======== ============
5 5
-5 5
0 0
<null> -1