OCI: How to bind output parameter of object type - oracle

I'm trying to call stored procedure (on oracle db 11g) from OCI based client. The procedure contains single OUT parameter of object type.
The problem: I always get "ORA-21525: attribute number or (collection element at index) %s violated its constraints" error.
I would highly appreciate if someone could give me a hint what can be the reason.
Notice: Interestingly, everything works ok in the following cases:
I replace return type from object type to nested table of such objects.
I replace return type to some primitive type, e.g. NUMBER
I make this parameter direction IN and bind it the same way.
Also, I discovered the following things:
The same error present, no matter if I return the result as OUT parameter from the procedure or if I user RETURN from FUNCTION.
If I try to call the stored procedure via PLSQL script, everything goes as expected without errors.
I also tried to create "parallel" C++ struct made of two OCINumber fields and use its object instead of calling OCIObjectNew(), but get the same error.
I also tried to set pOutParam = NULL, and bind it, but then I got "access violation, reading from location 00000".
Here is the code. PLSQL object type:
CREATE OR REPLACE TYPE dl_fake_type AS OBJECT
(
attr_one NUMBER(12,0),
attr_two NUMBER(12,0)
);
/
Procedure (for brevity I skip procedure declaration)
PROCEDURE dl_fake_fun(out_result OUT dl_fake_type)
IS
l_result dl_fake_type;
BEGIN
SELECT dl_fake_type(23, 35) INTO l_result FROM DUAL;
out_result := l_result;
END dl_fake_fun;
C++ OCI code (without connection initialization for brevity). Notice: I use MSVS2013 and some C++11 features, like std::string::front() method.
// ...
typedef basic_string<OraText, char_traits<OraText>, allocator<OraText> > OraTextString;
typedef basic_ostringstream<OraText, char_traits<OraText>, allocator<OraText> > OraOStringStream;
// Check if ociStatus == OCI_SUCCESS. If not, then print error and assert.
void checkOciStatus(const sword ociStatus, OCIError * errorHandle = NULL);
// ...
const OraTextString DL_FAKE_TYPE = (OraText const *)"DL_FAKE_TYPE";
const OraTextString outParamName = (OraText const *)":out_param";
OraOStringStream query(ios::ate);
query << "BEGIN my_dummy_pkg.dl_fake_fun(" << outParamName << "); END;";
const OraTextString & queryString = query.str();
OCIStmt * statement;
const ub4 executionMode = OCI_DEFAULT;
checkOciStatus(OCIHandleAlloc(envhp, (void **)&statement, OCI_HTYPE_STMT, /* xtramemsz */ 0, /* usrmempp */ NULL), errhp);
checkOciStatus(OCIStmtPrepare(statement, errhp, queryString.c_str(), queryString.length(), OCI_NTV_SYNTAX, executionMode), errhp);
const OraTextString schemaName = (OraText const *)"MY_SCHEMA_NAME";
OCIType * typeDescriptor = NULL;
checkOciStatus(
OCITypeByName(
envhp,
errhp,
svchp,
schemaName.c_str(),
schemaName.size(),
DL_FAKE_TYPE.c_str(),
DL_FAKE_TYPE.length(),
/* version name */ NULL,
/* version name length */ 0,
OCI_DURATION_SESSION,
OCI_TYPEGET_HEADER,
&typeDescriptor
),
errhp
);
OCIBind* bindHandle = NULL;
checkOciStatus(OCIBindByName(statement, &bindHandle, errhp, outParamName.c_str(), outParamName.length(), NULL, 0, SQLT_NTY, NULL, NULL, NULL, 0, NULL, executionMode), errhp);
void * pOutParam = NULL;
checkOciStatus(OCIObjectNew(envhp, errhp, svchp, OCI_TYPECODE_REF, typeDescriptor, NULL, OCI_DURATION_DEFAULT, /* true = value, false = ref */ FALSE, &pOutParam), errhp);
checkOciStatus(OCIBindObject(bindHandle, errhp, typeDescriptor, &pOutParam, NULL, NULL, 0), errhp);
checkOciStatus(OCIStmtExecute(svchp, statement, errhp, 1, 0, NULL, NULL, executionMode), errhp);
cout << "executed." << endl;
// ...
Thanks in advance.

After a period of struggling I've found the solution, which worked for me.
There were two points, which are vital for successful binding:
Object, which is intended to be bound have to be created by call to OCIObjectNew() as it is done in the code provided with the question. Notice: I also considered option to create instance of the struct representing corresponding C++ type directly. But this did not work even if the second condition was met.
After the object is created, all of its fields have to be initialized. I.e. fields of type OCINumber have to be set to some specific value (e.g. with OCINumberFromInt()) and pointers to OCIString have to be set to NULL.
Another gocha of output object binding (also observed when binding output nested tables) is that the second pointer to object passed to OCIBindObject is second pointer for reason: it seems, that OCI considers it as array of pointers to output objects. This means, that variable containing first pointer (to which points second pointer) must be valid until OCIExecuteStatement is called. This meens, that if you want to extract binding process to separate function, than you have to pass there second pointer to the object, which you then pass to the OCIBindObject(). Otherwise it will not work.
I hope, that those hints will help someone to avoid hours of frustration and painful struggling through the jungle of OCI (and Oracle DB) documentation.

Related

Setting Linq2Db to read and update with CHAR type and NOT NULL constraint in Oracle

Reading value of fixed CHAR string from table, using Linq2db Oracle provider:
CREATE TABLE mytable
(pk NUMBER(15,0) NOT NULL,
fixed_data CHAR(20) DEFAULT ' ' NOT NULL)
Although in database, length of FIXED_DATA filed is 20,
SELECT LENGTH(fixed_data) FROM mytable WHERE pk = 1
-- result is 20
When same field is read using Linq2Db, value gets truncated to empty string:
var row = (from row in database.mytable where row.pk == 1 select row).ToList()[0];
Console.WriteLine(row.fixed_data.Length);
// result is zero
This causes problem when record is updated using Linq2Db, Oracle converts empty string to NULL, and UPDATE fails:
database.Update(row);
// Oracle.ManagedDataAccess.Client.OracleException: 'ORA-01407: cannot update ("MYSCHEMA"."MYTABLE"."FIXED_DATA") to NULL
Is there any setting in Linq2Db for read->update cycle to work with CHAR type and NOT NULL constraint?
Found a solution, thanks to source code openly available. By default, Linq2Db calls expression IDataReader.GetString(int).TrimEnd(' ') on every CHAR and NCHAR column. However this can be easily customized, by implementing custom provider, which overrides field value retrieval expression, with one that does not trim:
class MyOracleProvider : OracleDataProvider
{
public MyOracleProvider(string name)
: base(name)
{
// original is SetCharField("Char", (r,i) => r.GetString(i).TrimEnd(' '));
SetCharField("Char", (r, i) => r.GetString(i));
// original is SetCharField("NChar", (r,i) => r.GetString(i).TrimEnd(' '));
SetCharField("NChar", (r, i) => r.GetString(i));
}
}

PLSQL stored procedure query(toplink) returns bigdecimal fields without fraction

I call the stored procedure from Java (Toplink) as follows:
PLSQLStoredProcedureCall call = new PLSQLStoredProcedureCall();
call.setProcedureName("PACK_PORTAL_VIEW.get_payment_details");
call.addNamedArgument("p_order_id", JDBCTypes.NUMERIC_TYPE);
call.addNamedOutputArgument("some_variable", JDBCTypes.NUMERIC_TYPE);
DataReadQuery drQuery = new DataReadQuery();
drQuery.setCall(call);
drQuery.addArgument("p_order_id");
Query query = ((JpaEntityManager) em.getDelegate()).createQuery(drQuery);
query.setParameter("p_order_id", orderId);
DatabaseRecord record = (DatabaseRecord) query.getSingleResult();
record.get("some_variable");
record.get("some_variable") returns some value that is stored in the DB with a fractional part, but in java it is written without it
record.get("some_variable").getClass() returns BigDecimal.class
How can I get the fractional value stored in the database?
It was necessary to use the following method:
/**
* PUBLIC: Add a named OUT argument to the stored procedure. The databaseType parameter
* classifies the parameter (JDBCType vs. OraclePLSQLType, simple vs. complex). The extra scale
* and precision parameters indicates that this parameter, when used in an Anonymous PL/SQL
* block, requires scale and precision specification
*/
public void addNamedOutputArgument(String procedureParameterName, DatabaseType databaseType,
int precision, int scale) {
DatabaseType dt = databaseType.isComplexDatabaseType() ?
((ComplexDatabaseType)databaseType).clone() : databaseType;
arguments.add(new PLSQLargument(procedureParameterName, originalIndex++, OUT, dt,
precision, scale));
}

Debugging runtime pl/sql errors

I’m new in pl/sq.
So, I’m trying to call pl/sql stored procedure from Java, but appears error:
wrong number or types of arguments in call to ‘searchuser’.
Where can I find most specific exception?
Is there some error logs or errors table in oracle? How can I debug these kind of problems?
Thanks!
one thing is if you call stored procedures from java: don't forget to register the function result. Example Java code:
CallableStatement cstmt = connection.prepareCall("{? = call xxx.checkArea(?,?)}");
// the 1st Parameter is the result of the function checkArea
cstmt.registerOutParameter(1, Types.INTEGER); // Function Return
cstmt.setInt(2, status); // Parameter 1 (in)
cstmt.registerOutParameter(3, Types.INTEGER); // Parameter 2 (out)
// execute the call
cstmt.execute();
// check the results
sessionId = cstmt.getInt(1); // Session-ID
rows = cstmt.getInt(3); // Rows
The PL/SQL Function is declared as following:
function checkArea(pi_area in number,
po_rows out number) return number;

Unable to create a constant value - only primitive types or Enumeration types allowed

I have seen some questions related to this Exception here but none made me understand the root cause of the problem. So here we have one more...
var testquery =
((from le in context.LoanEMIs.Include("LoanPmnt")
join lp in context.LoanPmnts on le.Id equals lp.LoanEMIId
where lp.PmntDtTm < date && lp.IsPaid == false
&& le.IsActive == true && lp.Amount > 0
select new ObjGetAllPendingPmntDetails
{
Id = lp.Id,
Table = "LoanEMI",
loanEMIId = lp.LoanEMIId,
Name = le.AcHead,
Ref = SqlFunctions.StringConvert((double)le.FreqId),
PmntDtTm = lp.PmntDtTm,
Amount = lp.Amount,
IsDiscard = lp.IsDiscarded,
DiscardRemarks = lp.DiscardRemarks
}).DefaultIfEmpty(ObjNull));
List<ObjGetAllPendingPmntDetails> test = testquery.ToList();
This query gives the following Exception Message -
Unable to create a constant value of type CashVitae.ObjGetAllPendingPmntDetails. Only primitive types or enumeration types are supported in this context.
I got this Exception after I added the SQL function statement to convert le.FreqId which is a byte to a string as ToString() is not recognized in the LINQ Expression Store.
ObjGetAllPendingPmntDetails is a partial class in my model which is added as it is used too many times in the code to bind data to tables.
It has both IDs as long, 'Amount' as decimal, PmntDtTm as Datetime,IsDiscard as bool and remaining all are string including 'Ref'.
I get no results as currently no data satisfies the condition. While trying to handle null, I added DefaultIfEmpty(ObjNull) and ObjNull has all properties initialized as follows.
ObjGetAllPendingPmntDetails ObjNull = new ObjGetAllPendingPmntDetails()
{ Id = 0, Table = "-", loanEMIId = 0, Name = "-", Ref = "-",
PmntDtTm = Convert.ToDateTime("01-01-1900"),
Amount = 0, IsDiscard = false, DiscardRemarks = "" };
I need this query to work fine as it has Union() called on it with 5 other queries. All returning the same ObjGetAllPendingPmntDetails columns. But there is some problem as this query has no data satisfying the conditions and the Exception Shared Above.
Any suggestions are appreciated as I am unable to understand the root cause of the problem.
#AndrewCoonce is right, the .DefaultIfEmpty(ObjNull) is the culprit here. Entity Framework turns DefaultIfEmpty into something like...
CASE WHEN ([Project1].[C1] IS NULL) THEN #param ELSE [Project1].[Value] END AS [C1]
...but there's no way to coerce an instance of ObjGetAllPendingPmntDetails into something that can take the place of #param, so you get an exception.
If you move the DefaultIfEmpty call to after the ToList it should work correctly (although you'll need to call ToList again after that if you really want a concrete list instance).

what are the OleDbTypes associated with Oracle Number and varchar2 when calling a function

I'm trying to map OleDb parameters to an Oracle Function. I was able to do this using the System.Data.Oracle namespace but then found that this is depricated, so I thought i would re-write it as OldDb to avoid installing the Oracle Provider.
I have defined the following oracle function as an example:
create function GetImagePath (AIRSNumber in number)
return varchar2
is
begin
return '\\aiimg524\images\Ofndrtrk\2010\01\0kvrv1p000lcs74j';
end;
and I'm calling it using the following code:
using (var command = new OleDbCommand())
{
command.Connection = con;
command.CommandText = ConfigurationManager.AppSettings[OTRAK_PHOTO_FUNC];
command.CommandType = CommandType.StoredProcedure;
string parm = ConfigurationManager.AppSettings[OTRAK_PHOTO_PARM];
command.Parameters.Add(parm, OleDbType.Decimal); // maps to oracle Number
command.Parameters[parm].Direction = ParameterDirection.Input;
command.Parameters[parm].Value = airsNumber;
command.Parameters.Add(RETURN_VALUE, OleDbType.Variant); // maps to Oracle varchar2
command.Parameters[RETURN_VALUE].Direction = ParameterDirection.ReturnValue;
try
{
con.Open();
command.ExecuteNonQuery();
path = command.Parameters[RETURN_VALUE].Value.ToString();
}
I tried a bunch of different OleDB types for the parameter and the return value. the current attempt is from a mapping table i found on the web that said number = decimal and varchar2 = variant. I'm about to try every permutation of types in the enum and wanted to ask for help. the not so useful error message i get is:
[System.Data.OleDb.OleDbException] = {"ORA-06550: line 1, column 7:\nPLS-00306: wrong number or types of arguments in call to 'GETIMAGEPATH'\nORA-06550: line 1, column 7:\nPL/SQL: Statement ignored"}
This actually had nothing to do with the type of the parameters but the order. Using the OleDb provider for Oracle does not respect the names of the parameters in the parameter collection but rather the order that the parameters are added. Wwhen calling an oracle function, the return value is a free parameter that must be declared first. by adding my return value parameter and then the actual function parameter things started working.
using the command.Parameters.AddWithValue(parm, value) also simplifies things a bit.

Resources