giovedì 24 luglio 2014

User Defined Types e Stored Procedures

gli User Defined Types sono uno dei modi che Oracle mette a disposizione per definire delle strutture dati custom che ci facilitino il lavoro in PL/SQL.
Si possono fare due tipi di UDT, come oggetto o come lista. Non mi è mai capitato di usare i tipi oggetto, perchè di solito il domain model è sempre definito nell'applicazione, ma sono comunque molto potenti, sebbene abbiano dei vincoli e delle restrizioni.
Vediamo invece come usare i tipi lista per passare liste di dati in una stored procedure, probabilmente il caso più comune che può capitare ad uno sviluppatore dotnet che si trovi a lavorare con Oracle.

Gli step da seguire sono pochi e abbastanza semplici:

1. Creiamo uno User Defined Type (UDT) in Oracle con una istruzione sql:

CREATE OR REPLACE TYPE MYINTTABLE AS TABLE OF NUMBER;  

In questo modo creiamo un UDT chiamato MYINTTABLE, di tipo TABLE, che può contenere dei NUMBER.

2. Creiamo ora una stored procedure di esempio:

CREATE OR REPLACE PROCEDURE TEST_UDT (PARAM MYINTTABLE,POUT SYS_REFCURSOR)
AS
BEGIN
  OPEN POUT FOR
  SELECT * FROM TABLE1 WHERE ID IN (SELECT * FROM TABLE(CAST(PARAM AS MYINTTABLE)));

END;

Come vedete la procedura accetta un parametro in input di tipo MYINTTABLE (il nostro UDT) e un parametro in output di tipo REFCURSOR.
Il codice PL/SQL non fa altro che riempire il cursore con una select star su una tabella di esempio, in questo caso TABLE1, con un campo ID sul quale vado a filtrare le righe prendendo i valori dalla variabile UDT passata in ingresso.

Immaginando di voler prendere le righe di TABLE1 con ID  1, 10, e 20 scrivendo un'istruzione sql "pura", dovrei scrivere

SELECT * FROM TABLE1 WHERE ID IN (1,10,20)

Se però voglio avere la possibilità di variare la lista di ID che filtrerà i miei risultati, posso sostituire la lista statica di numeri con

SELECT * FROM TABLE(CAST(PARAM AS MYINTTABLE))

che non è altro che una subquery che ripropone l'insieme di numeri sotto forma di tabella, proprio come una normale subquery.

3. Vediamo ora lato codice cosa ci serve. Intanto abbiamo bisogno di una classe .NET che mappi il mio UDT oracle, quindi creo una nuova classe c# ( o vb.net, in base alle preferenze), come segue:

using Oracle.DataAccess.Types;

[OracleCustomTypeMappingAttribute("MYINTTABLE")] 
public class UDTMappingOracle_MYINTTABLE_Type:IOracleArrayTypeFactory
 {
     public System.Array CreateArray(int numElems)
     {
         return new int[numElems - 1];
     }
     public System.Array CreateStatusArray(int numElems)
     {
         return null;
     }
 }

L'attributo OracleCustomTypeMappingAttribute indica al provider ODP.NET qual'è la classe da utilizzare per instanziare i giusti oggetti da inviare al client oracle, mentre l'implementazione dell'interfaccia IOracleArrayTypeFactory mette a disposizione i metodi per creare appunto la nostra lista di id da passare alla procedura.

4. E' giunto il momento di richiamare la nostra procedura, con un qualsiasi codice in grado di aprire una connessione ed eseguire un comando SQL passando dei parametri, ad esempio:

using Oracle.DataAccess.Client;
 
namespace OracleUDT
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
 
                //creo la nostra lista di id
                var listaId = new System.Collections.Generic.List<int>() { 11020 };
 
                //creo un adapter per praticità
                using (OracleDataAdapter adapter = new OracleDataAdapter("TEST_UDT""User Id=TEST;Password=TEST;Data Source=TESTDB;"))
                {
                    //indico al provider che voglio eseguire una stored procedure invece di una istruzione plain sql
                    adapter.SelectCommand.CommandType = System.Data.CommandType.StoredProcedure;
                    //creo il parametro di input
                    OracleParameter inParam = new OracleParameter("PARAM"OracleDbType.Array, System.Data.ParameterDirection.Input);
                    //imposto il valore convertendo in array la nostra lista di id
                    inParam.Value = listaId.ToArray();
                    //SPECIFICO IL NOME DELL'UDT CHE INTENDO USARE
                    inParam.UdtTypeName = "MYINTTABLE";
 
                    //creo il parametro di output, di tipo cursore
                    OracleParameter outParam = new OracleParameter("POUT"OracleDbType.RefCursor, System.Data.ParameterDirection.Output);
 
                    //aggiungo i due parametri al command
                    adapter.SelectCommand.Parameters.Add(inParam);
                    adapter.SelectCommand.Parameters.Add(outParam);
 
                    //riempio un dataset con i risultati prodotti dalla procedura
                    var results = new System.Data.DataSet();
                    adapter.Fill(results);
 
                    System.Diagnostics.Debug.WriteLine("numero di record trovati: " + results.Tables[0].Rows.Count);
 
 
                }
            }
            catch (System.Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message);
            }
        }
    }
}


Come vedete, quando andiamo ad istanziare il parametro di input, indichiamo che è di tipo Array, che il suo valore è uguale alla nostra lista convertita ad array, e che l'UdtTypeName che vogliamo utilizzare è MYINTTABLE.

Se eseguiamo il codice otterremo un dataset di record i cui ID saranno uguali a 1, 10 o 20.

Uno dei vantaggi nell'usare un UDT di tipo tabella, come visto nella procedura, è quello di poterlo trattare come una tabella, con tutti i vantaggi che ne conseguono, seppur in maniera limitata, ovvero la possibilità di fare union, intersect, minus, e quant'altro, permettendoci di integrare le nostre query con liste di dati, invece che con singoli valori.

ODP.NET e oraprovcfg

A volte installando più versioni del client Oracle sulla propria macchina, si va incontro a problemi di sovrapposizione di priorità tra i client, o può accadere di dover modificare la propria configurazione per esigenze diverse. Nello specifico di Oracle DataAccess Provider for .NET (ODP.NET), il tool oraprovcfg.exe ci consente di gestire la configurazione del provider.

Ecco alcuni utilizzi comuni di oraprovcfg:

#registra odp.net nella gac
#path= percorso fisico di Oracle.DataAccess.dll
oraprovcfg /action:gac /providerpath:"[path]\Oracle.DataAccess.dll"

#cancella odp.net dalla gac
#Version è opzionale, può essere usato per rimuovere una versione specifica
oraprovcfg /action:ungac /providerpath:"Oracle.DataAccess.dll[, Version=2.111.5.10]"

#registra odp.net in machine.config
#frameworkversion va utilizzato per indicare il framework di riferimento
#path ovviamente varierà in base alla versione del framework, puntando alla directory \ODP.NET\bin\2.x o \4
oraprovcfg /action:config /product:odp /frameworkversion:v4.0.30319 /providerpath:"[path]\Oracle.DataAccess.dll"

#cancella odp.net in machine.config
oraprovcfg /action:unconfig /product:odp /frameworkversion:v4.0.30319

#registra gli ODP.NET performance counters sfruttabili poi con le WMI
oraprovcfg /action:register /product:odp /component:perfcounter /providerpath:"C:\oracle\product\11.2.0\client_112030_64\odp.net\bin\4\Oracle.DataAccess.dll"

#cancella gli ODP.NET performance counters
oraprovcfg /action:unregister /product:odp /component:perfcounter /providerpath:"[path]\Oracle.DataAccess.dll"