Oracle and C# - UDT array as return value

Viewed 1123

I am trying to have C# read an array of a PL/SQL user defined type.

This is the definition of each object of the array:

CREATE OR REPLACE TYPE "REPORTDATUM" AS OBJECT
(
            FIELD1                                                   VARCHAR2(20),
            FIELD2                                                   VARCHAR2(3000),
            FIELD3                                                   NUMBER(15),
            FIELD4                                                   DATE,
            FIELD5                                                   DATE,
            FIELD6                                                   VARCHAR2(4000),
            FIELD7                                                   TIMESTAMP,
            FIELD8                                                   VARCHAR2(10),
            FIELD9                                                   NUMBER(35),
            FIELD10                                                  VARCHAR2(80),
            FIELD11                                                  VARCHAR2(50)
);

CREATE OR REPLACE TYPE "REPORTARRAY" IS TABLE OF REPORTDATUM;

This is the signature of the function which I am trying to call:

FUNCTION getReport(startTime IN DATE, endTime IN DATE) RETURN REPORTARRAY;

Now, based on a guide I found here: http://appsjack.blogspot.com/2010/09/pass-custom-udt-types-to-oracle-stored.html

I created the ReportDatum.cs class as follows:

public class ReportDatum : INullable,  IOracleCustomType {

    private bool objectIsNull;

    [OracleObjectMappingAttribute(Constants.ReportDatum.Aliases.Field1)]
    public string Field1 { get; set; }

    [OracleObjectMappingAttribute(Constants.ReportDatum.Aliases.Field2)]
    public string Field2 { get; set; }

                            [...]

    [OracleObjectMappingAttribute(Constants.ReportDatum.Aliases.Field11)]
    public string Field11 { get; set; }


    public static ReportDatum Null {
        get {
            ReportDatum reportDatum = new ReportDatum();
            reportDatum.objectIsNull = true;
            return reportDatum;
        }
    }

    public bool IsNull {
        get { return objectIsNull; }
    }

    public void FromCustomObject(OracleConnection databaseConnection, IntPtr userDefinedTypePointer) {
        if (string.IsNullOrEmpty(this.Field1) == false ) {
            OracleUdt.SetValue(databaseConnection, userDefinedTypePointer, Constants.ReportDatum.Aliases.Field1, this.Field1);
        }
        if (string.IsNullOrEmpty(this.Field2) == false) {
            OracleUdt.SetValue(databaseConnection, userDefinedTypePointer, Constants.ReportDatum.Aliases.Field2, this.Field2);
        }
        [...]
        if (string.IsNullOrEmpty(this.Field11) == false) {
            OracleUdt.SetValue(databaseConnection, userDefinedTypePointer, Constants.ReportDatum.Aliases.Field11, this.Field11);
        }
    }

    public void ToCustomObject(OracleConnection databaseConnection, IntPtr userDefinedTypePointer) {
        this.Field1 = parseString(databaseConnection, userDefinedTypePointer, Constants.ReportDatum.Aliases.Field1);
        this.Field2 = parseString(databaseConnection, userDefinedTypePointer, Constants.ReportDatum.Aliases.Field2);
                                            [...]
        this.Field11 = parseString(databaseConnection, userDefinedTypePointer, Constants.ReportDatum.Aliases.Field11);
    }

    private string parseString(OracleConnection databaseConnection, IntPtr userDefinedTypePointer, string columnName) {
        string valueToReturn = String.Empty;
        try {
            valueToReturn = ( String ) OracleUdt.GetValue(databaseConnection, userDefinedTypePointer, columnName);
        }
        catch (Exception e) {
            error("Exception while attempting to parse string value of column [" + columnName + "]. Message [" + (e == null || e.Message == null ? "NULL" : e.Message) + "]", e);
        }
        return valueToReturn;
    }

 }

I've also proceeded to create ReportDatumFactory.cs as follows:

public class ReportDatumFactory : IOracleCustomTypeFactory {

    public IOracleCustomType CreateObject() {
        return new ReportDatum();
    }

}

This, according to the guide I linked above should be sufficient to handle each item that composes the array.

In order to handle the array itself, I created the below classes.

ReportArray.cs

public class ReportArray : INullable, IOracleCustomType {

    [OracleArrayMapping()]
    public ReportDatum[] reportArray;

    private bool objectIsNull;

    public bool IsNull {
        get { return objectIsNull; }
    }

    public void FromCustomObject(OracleConnection databaseConnection, IntPtr userDefinedTypePointer) {
        try {
            OracleUdt.SetValue(databaseConnection, userDefinedTypePointer, 0, reportArray);
        }
        catch (Exception e) {
            error("Exception while populating OracleUdt from ReportArray.", e);
        }
    }

    public void ToCustomObject(OracleConnection databaseConnection, IntPtr userDefinedTypePointer) {
        try {
            reportArray = ( ReportDatum[] ) OracleUdt.GetValue(databaseConnection, userDefinedTypePointer, 0);
        }
        catch (Exception e) {
            error("Exception while reading values from OracleUdt related to ReportArray.", e);
        }
    }

}         

And the associated factory class

ReportArrayFactory.cs

public class ReportArrayFactory : IOracleCustomTypeFactory, IOracleArrayTypeFactory {

    public IOracleCustomType CreateObject() {
        return new ReportArray();
    }


    public Array CreateArray(int numberOfElements) {
        return new ReportArray[numberOfElements];
    }

    public Array CreateStatusArray(int numberOfElements) {
        return null;
    }

}

Now, in regards to the method that calls the stored procedure, this is what I have:

Report.cs

    private void executeStoredProcedure2(OracleConnection databaseConnection, string schema, string storedProcedureName, DateTime startTime, DateTime endTime, int numberOfElements) {
        OracleCommand databaseCommand = new OracleCommand();
        ReportArray reportArray = new ReportArray();

        debug("Attempting to populate the report array");
        if (databaseConnection != null && databaseConnection.State == ConnectionState.Open) {
            try {
                databaseCommand.Connection = databaseConnection;
                databaseCommand.CommandType = CommandType.StoredProcedure;
                databaseCommand.CommandText = schema + "." + storedProcedureName;
                databaseCommand.Parameters.Add(createUserDefinedParameter(numberOfElements, reportArray));
                databaseCommand.Parameters.Add(Constants.ReportDatum.ParameterNames.startTime, OracleDbType.Date, startTime, ParameterDirection.Input);
                databaseCommand.Parameters.Add(Constants.ReportDatum.ParameterNames.endTime, OracleDbType.Date, endTime, ParameterDirection.Input);
                databaseCommand.ExecuteNonQuery();
                //How do I populate the reportArray object?
            }
            catch (Exception exception) {
                error("Exception while executing stored procedure [" + storedProcedure2 + "].", exception);
            }
            finally {
                databaseCommand.Dispose();
            }
        }
    }



    private OracleParameter createUserDefinedParameter(int size, ReportArray reportArray) {
        OracleParameter valueToReturn = null;
        try {
            valueToReturn = new OracleParameter();
            valueToReturn.ParameterName = "reportData"; //this is the name of the REPORTARRAY object in the Oracle function that is being returned.
            valueToReturn.OracleDbType = OracleDbType.Array;
            valueToReturn.Direction = ParameterDirection.ReturnValue;
            valueToReturn.CollectionType = OracleCollectionType.PLSQLAssociativeArray;
            valueToReturn.UdtTypeName = Constants.ReportDatum.arrayDataTypeDefinition;
            valueToReturn.ArrayBindSize = new int[size];
            valueToReturn.ArrayBindStatus = new OracleParameterStatus[size];
            valueToReturn.Size = size;
            valueToReturn.Value = reportArray.reportArray;
        }
        catch (Exception e) {
            error("Exception while attempting to create user defined parameter.", e);
        }
        return valueToReturn;
    }

The problem I am facing is that I am unable to obtain the array back in C#. Has anyone faced this problem? What am I missing?

I have reason to believe the SQL code is working. It's only the C# that is creating the problem at the moment.

1 Answers

There look to be a few things wrong.

Firstly, the factories need OracleCustomTypeMapping attributes. Without them, Oracle won't pick up that these mapping factory classes should be used at all:

    [OracleCustomTypeMapping("SCHEMA.REPORTDATUM")]
    public class ReportDatum ...

and

    [OracleCustomTypeMapping("SCHEMA.REPORTARRAY")]
    public class ReportArray ...

Secondly, the array mapping factory has a bug in it: when creating an array, you want to create an array of ReportDatum objects. At the moment you're creating an array of ReportArray objects, i.e. an array of arrays, which you don't want. In other words, replace

    public Array CreateArray(int numberOfElements) {
        return new ReportArray[numberOfElements];
    }

with

    public Array CreateArray(int numberOfElements) {
        return new ReportDatum[numberOfElements];
    }

Thirdly, remove the line

        CollectionType = OracleCollectionType.PLSQLAssociativeArray,

from your createUserDefinedParameter method. The returned value isn't a PL/SQL associative array (it's a different kind of collection) so this property should not be used. If you use this property you will probably find that the return value coming back from the stored function is an empty object array.

Finally, to get the array back from the stored function call, add the following line after the call to databaseCommand.ExecuteNonQuery():

    reportArray = (ReportArray)databaseCommand.Parameters["reportData"].Value;
Related