Tuesday, January 19, 2010

How to pass arrays from .Net C# to Oracle

Guess you have the following query

select * from table where table.id in (:MyIDList)

and you want to pass a number of IDs in the binding variable :MyIDList from C# to your Oracle SQL. If you now check the types of binding variables available for this job, you find out that this Oracle only supports scalar types as binding variables.
If you now dig further you find a couple of bright solution that are all based on splitting up some string (varchar2) into several elements. But due to the fact, that the length of varchar2 binding variables is limited to 4/32K SQL/PLSQL this is not a scalable solution to the problem.

Even if you think you are smart, and you try a string replace of :MyIDList with the elements like 'a','b','c' let me assure you that the limit for written elements in an in statement is 1000. Also your performance will degrade significantly, as the query cache won't recognize the SQL as being executed before and therefore Oracle has to recompile it with every execution.

Is it impossible to pass an variable length array to SQL?

Let me put it straight!
Yes and No.

In pure SQL it is impossible to pass a variable length array to SQL.

But in PL/SQL it is not.

"Yes great", you think, "but I need it in SQL!"

The trick is a PL/SQL wrapper to SQL!

Simply use the following ingredients:

Define 2 global types (and don't try to be smart here. We need IDType for couple of reasons):

CREATE OR REPLACE
type IDTYPE as object (
id varchar2(20)
);

CREATE OR REPLACE
type IDTABLETYPE as table of IDType;

In PL/SQL now create a package

CREATE OR REPLACE PACKAGE MYPACKAGE
as

type stringTableType is table of varchar2(20) index by binary_integer;

procedure GetMyTableByIDs
(
p_MyIDList IN stringTableType,
p_outRefCursor out RefCursorType
);

end;

CREATE OR REPLACE PACKAGE BODY MYPACKAGE
as

TYPE RefCursorType IS REF CURSOR;
procedure GetMyTableByIDs
(
p_MyIDList IN stringTableType,
p_outRefCursor out RefCursorType
)
as
iMyIDList IDTableType;
begin

iMyIDList := IDTableType();
iMyIDList.Extend(p_MyIDList.count);

for i in p_MyIDList.First .. p_MyIDList.Last
loop
iMyIDList(i) := IDType(p_MyIDList(i));
end loop;

open p_outRefCursor
for
select * from table where table.id in (select id from table(iMyIDList));

end GetMyTableByIDs;

end;

What is going on here?

First thing you notice is that we have 2 very similar array (table) types.

Globally we defined the

type IDTABLETYPE as table of IDType -- IDType is varchar2(20)

This is in Oracle terms a "Nested Table". This type is available in SQL and PL/SQL. IDType brings the property "ID" of type varchar2(20).
In PL/SQL we defined the very similar type:

type stringTableType is table of varchar2(20) index by binary_integer;

This is an "index by table" or "associative array" in oracle terms. Associative arrays are better understood as "HashTable" and are available in PL/SQL only. For a more detailed explanation of the differences please have a look at "Collection Types in PL/SQL".

But why do you copy the arrays one by one?

Because you now see that Oracle has obviously 2 different development units for SQL and PL/SQL. And they do not seem to talk very much together.

The result of 3 days in short:
  • There is no way to pass a nested table as parameter to a stored procedure in C#
  • There is no way to use a associative array in SQL
  • There is no way to assign/initialize a nested table to/with an associative array

Great, but how do we use it in C#?

OracleConnection conn = new OracleConnection("MyConnectionString");
OracleCommand cmd = conn.CreateCommand();
cmd.CommandText = "MyPackage.GetMyTableByIDs";
cmd.CommandType = CommandType.StoredProcedure;
cmd.BindByName = true;

cmd.Parameters.Add(new OracleParameter("p_outRefCursor", OracleDbType.RefCursor)).Direction = ParameterDirection.Output;

cmd.Parameters.Add(new OracleParameter("p_MyIDList", OracleDbType.Varchar2)
{
CollectionType = OracleCollectionType.PLSQLAssociativeArray,
Value = my_list_with_ids.ToArray()
}

);

da = new OracleDataAdapter(cmd);

da.Fill(myDataSet);

It's not working!

I can't find neither

cmd.BindByName = true;

nor

OracleCollectionType.PLSQLAssociativeArray

The constraint of the solution is that you have to use Oracle .NET driver (Oracle.DataAccess) instead of the the Microsoft (System.Data.OracleClient) driver. But with .Net 4 the Microsoft's Oracle driver is marked deprecated anyway. So get used to it.

This is it. The only way to pass an array to a SQL.

"Holy shivers!" you think. This is a lot of glue for a simple task like this!

Basically yes, BUT
You can do 3 things now:
1.) I use a macro that converts my SQL into PL/SQL by automatically replacing the binding variable :MyIDList with the PL/SQL Parameter p_myIDList.
2.) You can tune your performance significantly by rewriting your SQL
3.) You can clean up your code a lot by using default values

Improve overall performance

Our former SQL

select * from table where table.id in (select id from table(iMyIDList))

becomes unbearable slow with a large number of ID's and lines in table. What you can do now is to rewrite our SQL to

select * from table where table.id
join (select id from table(iMyIDList)) IdFilter on table.id = IdFilter.id

And if you want to develop in SQL and simply convert it with our macro you can add the following function to your package:

FUNCTION GetDefaultTable
(
param varchar2
)
RETURN IDTableType
is
begin
return IDTableType(IDType(param));
end;

and rewrite your regular SQL to:

select * from table where table.id
join (select id from table(MyPackage.GetDefaultTable(:MyIDList)) IdFilter
on table.id = IdFilter.id

Use PL/SQL defaults:

A feature of our PL/SQL is that you can define default values for all parameters.
In a simple case this is:

procedure GetSomeThing
(
p_param1 in varchar2 default 'SomeDefaultValue',
);

but what do we do with our associative array?

The fancy part about default parameters is that the value can be a call to function.....

So

procedure GetSomeThing
(
p_MyIDList IN stringTableType default GetDefaultTable('DefaultValueForElement'),
p_param1 in varchar2 default 'SomeDefaultValue',
p_outRefCursor out RefCursorType
);

works nice and easy.

in combination with the line in C#

cmd.BindByName = true;

what you can do now is only pass parameters that differ from their defaults what can be used to write a much nicer code. Instead of passing all the parameters defined a for each procedure defined (results in a clunky piece of code), you just just set the parameter for any stored procedure if the associated value is non-default.

Friday, January 8, 2010

How to install SQLXML 3 on IIS 7 (Windows Server 2008 64 Bit)

The release notes of SQLXML 4.1 state, that if you need SQLXML with ISAPI (Internet Information Server) support, the latest version for you is SQLXML 3 SP3.

If you try to install it on a Windows Server 2008 R2 64Bit you might see 2 issues with that:

After installing you click "Start - SQLXML 3.0 - Configure IIS Support"

If you get the error

"An error occurred connecting to 'server'. This server does not exist or is unavailable."

apply the following solution

Click "Start - Administrative Tools - Server Manager"

in the Server Manager go to

"Roles - Webserver (IIS)"

Go to section "Role Services"

verify that all of "IIS 6 Management Compatibilty" packages are installed.

After I installed the IIS 6 compatibility packages the SQLXML 3 snapin starts and connects just fine.

The second error can occur if you attempt to call one of the xml templates and IIS just gives a 500 error 0x800700c1. Permissions and all appear correct. The IIS Virtual Direcotry Manager for SQLXML 3.0 throws an error when I attempt to Restart Application DLL "Error occurred unloading the application".

Then apply the following solution:

1. Open IIS Manager, Select Application Pools.

2. Under Actions pane, Select 'Set Application Pool Defaults'.

3. Set 'Enable 32-bit Applications' to 'True'. (Solution found here)

Tuesday, December 15, 2009

A fix for: "Attempted to read or write protected memory " in Visual Studio

I got this error after installing the WPF Toolkit and not closing all my Visual Studios before installing. The error was thrown by Visual Studio when I invoked F5 to run my WPF application in debug mode.
In my case the fix was simple:
Goto: Tools - Import and Export Settings - Reset all Settings

Friday, September 4, 2009

How to fix: "shared heap exhausted or damaged"

The error can occur because your desktop heap is too small. This happened to me especially after opening a large number of windows.

Nview then filled up the application log with the error message:

"NVIEW : Explorer: shared heap exhausted or damaged"

Quick Solution:

1.) Start Regedit:

2.) Browse to

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems

3.) Open the "Windows" key

In the string you find a section like

....Windows SharedSection=1024,4096,512....

Edit the second value e.g.

Windows SharedSection=1024,8192,512


Click OK

4.) Restart Windows

To verify this solution is the correct one for you can:

1. ) Download dheapmon

2.) Open a command shell (Start - Run - "CMD")

3.) Change your directory to the location of the executables (e.g. C:\kktools\dheapmon8.1\x86)

4.) Type: dheapinst.exe -y srv*http://msdl.microsoft.com/download/symbols

5.) Type: dheapmon -l

6.) Type: dheapmon

Now you see:


If you see a high utilization of the "WinSta0\Default " session e.g.:


WinStation\Desktop Heap Size(KB) Used Rate(%)
---------------------------------------------
WinSta0\Default 4096 95,6

then the above solution will do the job for you because:

"
The second SharedSection value (3072) is the size of the desktop heap for each desktop that is associated with the "interactive" window station WinSta0. User objects like hooks, menus, strings, and windows consume memory in this desktop heap."

Warning

As always be careful with what you do. Modifying your registry has the potential to render your system useless.