Tuesday, May 27, 2008

Recommended by Joe

A friend of mine at Oracle (that is to say Greg is still at Oracle whilst I am not) pointed out to me that Microsoft's Joe Stagner has Pro .NET Oracle Programming as a recommended book on the Oracle - ASP.NET forum. Currently the recommended books list looks like this (subject to change naturally):

joe_stagner_recommends_me

That got me to thinking a bit. It has been just over 4 years since I started writing that book. (I started the first chapter in March 2004). Certainly a lot has changed since then with several releases of the Oracle Data Provider for .NET, Oracle Database, Visual Studio, the Oracle Developer Tools for Visual Studio .NET, etc. I was just at one of the major booksellers here locally over the weekend and the "computer section" has dwindled immensely. I wonder if it would be worthwhile to update the book? There are several things I would want to change to be sure, but would it be worth it? Do people get their technical information from OTN and MSDN mostly now?

Friday, May 09, 2008

ODTUG Kaleidoscope 2008

It's ODTUG time again! It is a great opportunity to see lots of Oracle folks like Tom Kyte, Sue Harper, Joel Kallman, David Peake, Carl Backstrom, as well as a ton of other great people!

June 15-19, 2008
New Orleans, Louisiana

ODTUG Kaleidoscope 2008 – the annual conference of the Oracle Development Tools User Group, will be held this June 15-19, 2008 in New Orleans, Louisiana. Packed with technical sessions, four individual keynotes, three symposiums, a dedicated Hyperion track crafted by the masters, and a special gathering of Oracle Ace Directors, ODTUG Kaleidoscope offers the most comprehensive in-depth technical conference available for Oracle professionals. Unlike other conferences in this category, ODTUG Kaleidoscope strives to provide real world knowledge by real world technologists and provide a conference experience that will truly help Oracle developers take their role to the next level. For additional information and a complete agenda, visit www.odtugkaleidoscope.com.

Saturday, May 03, 2008

Using Delegates to make a Synchronous Database Call Asynchronously

There are times when a tight coupling exists between user interaction with an application and database calls. That is, a user initiates an action that requires a database call, the database call executes, the call to the database returns, the user responds, and the cycle continues in fairly rapid succession. However, there are also times when a user may initiate an action that requires a lengthy database call. Of course, lengthy is a relative term and would be defined within your own environment. What is lengthy to one may not be lengthy to another. Something of a judgement call on that one.

If a lengthy database call is initiated from an application you may desire the capability to continue to do other work within that application while the database call executes. However, as current production versions of the Oracle Data Provider for .NET (ODP.NET) do not offer BeginXXX and EndXXX methods (such as BeginExecuteNonQuery() for example), you may think offering this functionality in your ODP.NET application is not possible or is difficult. Again, depending on your application specifics, offering such asynchronous behavior may give your application better responsiveness and increase the end-user experience positively.

One method of achieving this goal (calling a synchronous method asynchronously) is by using delegates. If you are not familiar with delegates, I suggest reviewing the Delegates topic in the online version of the C# Programming Guide on Microsoft's MSDN site. In addition to the standard reference information, there is also a tutorial (again located on MSDN) which can be found here. Of course these links are subject to change in the future.

In order to implement this technique you will need two methods (other than the Main method of course). One method will perform the synchronous database call and the other method will be invoked when that call is complete. A delegate is created for the method that performs the database call. This is the key to the technique. By creating a delegate for this synchronous method it becomes possible to invoke it asynchronously via the BeginInvoke() method exposed by the delegate. The delegate keyword actually causes the compiler to expand the declaration into a class which contains a BeginInvoke() method (among others).

The sample code below is a simple implementation of this technique using a console application. In this application a "do nothing" call to the database occurs. This call does nothing more than sleep inside the database for a specified period of time. As noted in the source code comments, the database user must have the "execute" privilege on the dbms_lock PL/SQL package for the code to work. Of course a real application would not normally connect to the database only to sleep for a period of time and return; however, this is a sufficient method for illustrating that the call is asynchronous in relation to the main thread.

The basic flow is as follows:

  • Create the delegate
  • Invoke the delegate to execute asynchronously from the main thread
  • Continue processing in the main thread while the database call is in progress
  • When database call is complete, invoke the OnCallComplete() method
  • Work in the main thread continues for a short period of time
  • Along the way prompt information is displayed and a final prompt is provided

It is necessary to cause the main thread to have a longer duration than the thread performing the database call because if the main thread ended before the database call thread the entire console application would terminate.

When the application is executed you should see output similar to the following:

Asynchronous call in progress.
  Will sleep in database for 10 seconds.
    Doing work in Main thread...
  Enter LongDatabaseCall
    Doing work in Main thread...
    Doing work in Main thread...
    Doing work in Main thread...
    Doing work in Main thread...
    Doing work in Main thread...
    Doing work in Main thread...
    Doing work in Main thread...
    Doing work in Main thread...
    Doing work in Main thread...
    Doing work in Main thread...
  Exit LongDatabaseCall
  Enter OnCallComplete
    Perform post-database call actions...
  Exit OnCallComplete
    Doing work in Main thread...
    Doing work in Main thread...
    Doing work in Main thread...
Press ENTER to continue...

As you can see, the basic flow above is represented here and work in the main thread continues after the LongDatabaseCall method has been invoked via the delegate.

Here's the (hopefully) well-commented code to produce the above:

using System;
using System.Threading;
using System.Data;
using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;

namespace AsynchronousDemo
{
  class Program
  {
    // change as necessary for your environment
    // the user must have execute privilege on dbms_lock
    const string constr = "User Id=hr;" +
                          "Password=hr;" +
                          "Data Source=orademo;" +
                          "Enlist=false";

    // the delegate for the function that makes the db call
    internal delegate void LongCallDelegate(int sleepSeconds);

    static void Main(string[] args)
    {
      // the amount of time the call to the database will sleep
      int sleepSeconds = 10;

      // write marker text to console to indicate beginning of process
      Console.WriteLine("Asynchronous call in progress.");
      Console.WriteLine("  Will sleep in database for {0} seconds.", sleepSeconds.ToString());

      // used to call the function
      LongCallDelegate ldc = new LongCallDelegate(LongDatabaseCall);

      // call BeginInvoke to make call asynchronous
      // the method to invoke when the call has completed is passed
      // as the second parameter
      ldc.BeginInvoke(sleepSeconds, OnCallComplete, ldc);

      // cause main thread to "pause" while the database call is in progress
      // add an arbitrary number (5) to the sleepSeconds to help show that
      // the database call is "independent" of the main thread
      for (int i = 1; i < sleepSeconds + 5; i++)
      {
        Console.WriteLine("    Doing work in Main thread...");
        Thread.Sleep(1000);
      }

      // prevent console from automatically closing as when run from VS in debug mode
      Console.WriteLine("Press ENTER to continue...");
      Console.ReadLine();
    }

    static void LongDatabaseCall(int sleepSeconds)
    {
      // this is the function that makes the database call
      // it will be called from the delegate to make the call
      // asynchronous in relation to the main thread

      // marker text
      Console.WriteLine("  Enter LongDatabaseCall");

      // create and open connection to database
      OracleConnection con = new OracleConnection(constr);
      con.Open();

      // create command object
      // will call dbms_lock to sleep for x seconds in database
      OracleCommand cmd = con.CreateCommand();
      cmd.CommandText = "begin dbms_lock.sleep(:1); end;";

      // parameter for the sleep duration
      OracleParameter p_sleepSeconds = new OracleParameter("1",
                                                           OracleDbType.Decimal,
                                                           sleepSeconds,
                                                           ParameterDirection.Input);

      // add parameter to the collection for the command object
      cmd.Parameters.Add(p_sleepSeconds);

      // make the database call
      cmd.ExecuteNonQuery();

      // clean up
      p_sleepSeconds.Dispose();
      cmd.Dispose();
      con.Dispose();

      // marker text
      Console.WriteLine("  Exit LongDatabaseCall");
    }

    static void OnCallComplete(IAsyncResult ar)
    {
      // marker text
      Console.WriteLine("  Enter OnCallComplete");

      // get the delegate from the IAsyncResult object
      LongCallDelegate ldc = (LongCallDelegate)ar.AsyncState;

      // Must call EndInvoke to pair with BeginInvoke
      ldc.EndInvoke(ar);

      // marker text
      Console.WriteLine("    Perform post-database call actions...");
      Console.WriteLine("  Exit OnCallComplete");
    }
  }
}

Tuesday, April 08, 2008

Generating Test Data

I'm an advocate of testing things - it can be a great way to discover different behaviors and learn how things work (or perhaps why they didn't work the way you thought they might!). When it comes to testing database applications, having some test data is certainly helpful. Ideally, I like to have a fully-loaded production database from which to draw this data into a test environment. Sometimes, however, this is not possible. In cases like these I use the "trick" described below to create my own test data.

When I need to generate test data I frequently call upon a seemingly little-known PL/SQL Package supplied with Oracle Database called "dbms_random". As you may guess this package can be used to generated random data. Rather than explain the package details (they are short and you can read them using the link), I'll just present a quick way to generate some data using SQL*Plus and the dbms_random package.

First, I'll need a table:

create table test_data
(
  id number,
  first_name varchar2(16),
  last_name varchar2(24),
  amount number(6,2),
  purchase_date date
);

As you can see, this table is nothing to write home about, but it does mirror what a real-world table would look like in a lot of cases.

When I am creating test data I generally prefer the data to be reproducible. That is, if I execute the process more than once I like to get the same data each time. To accomplish this with the dbms_random package I call the "seed" procedure and provide an initial seed value:

exec dbms_random.seed(42);

Let's say I wanted to populate this table with 10,000 rows of data. I use the following to do this:

insert into test_data
select
  rownum,
  initcap(dbms_random.string('l',dbms_random.value(2,16))),
  initcap(dbms_random.string('l',dbms_random.value(2,24))),
  round(dbms_random.value(1,1000),2),
  to_date('01-JAN-2008', 'DD-MON-YYYY') + dbms_random.value(-100,100)
from
  (select level from dual connect by level <= 10000);

Starting at the bottom of the SQL text is a neat trick using "dual" and "connect by" to essentially create an "unlimited dual" for generating lots of rows. This trick was (I believe) originally put forth by Mikito Harakiri and I discovered it via an Ask Tom thread.

The remainder of the text is fairly straight-forward in its use of the dbms_random package. I use the "string" function to generate a random, lowercase value (which is subsequently passed to "initcap" to capitalize the first character of each string), the "value" function is used to create a random numeric value (which is passed to "round" to make it look like a purchase amount), and then I use a fixed-date to which I add (possibly a negative value) to create a set of valid dates within a range.

The first 10 rows of this data when selected from the table look like:

ID FIRST_NAME       LAST_NAME                    AMOUNT PURCHASE_DATE
--- ---------------- ------------------------ ---------- --------------------
  1 Oqq              Mxsazbwyx                    521.33 22-MAR-2008 16:49:40
  2 Jjgqrywtxbdn     Fwwbzshhkbqzb                921.47 04-OCT-2007 09:10:00
  3 Zxflhufls        Mstwydowbaogeyyjiles         172.34 20-MAR-2008 10:22:05
  4 Zjjxtyysitsog    Zxrzqeflxgo                  882.16 26-DEC-2007 18:56:44
  5 Kjmuvbrqx        Hfu                          742.61 16-OCT-2007 14:35:27
  6 Oywaibiyp        Angvlehlmeujfdlhdmtt          664.5 29-FEB-2008 12:50:40
  7 Uhwyvla          Nhbwcv                       168.99 27-DEC-2007 22:29:59
  8 Kpdiqafanbvzt    Phjeqwelyugrmahybocwbhvp     813.81 01-MAR-2008 09:15:59
  9 Tvezuvrgnzzqkpq  Pjyygoqx                     880.09 21-NOV-2007 00:42:07
10 Olchylbeft       Nflaxjqfkmkgt                847.71 07-DEC-2007 16:53:23

10 rows selected.

Can this technique always be used? No, probably not. For example, the names are not exactly what you might call "proper" names. However, I do find that this technique can be useful when I need to create some data to perform some testing with. Perhaps it will be helpful to you too if you experiment with it and find the right combination of values to use in your tests.

Sunday, April 06, 2008

Applications and Data == Importance Envy?

I was recently reading an interesting posting (and comments) over on Doug Burns' Blog:

http://oracledoug.com/serendipity/index.php?/archives/1394-Theres-Hope-for-us-all.html

Doug's blog is a great resource and I encourage you to check it out if you have a few spare minutes.

I found this interesting because the "debate" around which is more important -- the application or the data -- raises its head every now and then. Now, my being a DBA in my "day job" may automatically bias me in this context; however, I was a developer before I was a DBA. This, of course, does not mean I am bias-free, but I do hope that I am not so closed-minded that I may see both sides of a discussion.

When I hear this question (which is more important? the application? or the data? OK, so that is three questions, but you understand.) I immediately think of a question I was posed long ago in school - which is more important, the heart or the blood? Of course there is not a perfect correlation between biology and applications and data, but I think that is not an unreasonable analogy. While not exactly easy, it is possible to find other mechanisms to transport the blood, but it is not so easy to replace the blood itself.

Clearly both applications and data are important. However, rather than necessarily declare one to be more important than the other, my inclination is to view them in terms of life span. Often I hear something along the lines of: An application is used to access the data. To me this implies that there may be more than one application or a series of applications over time whereas the data itself is not really viewed as having multiple incarnations. If I may borrow a quote from The Kinks: "Rocks bands will come and rock bands will go, but Rock 'n Roll will go on forever!" (From a live performance of "All Day and All of the Night" as I recall).

Thursday, February 28, 2008

Experimenting with Connection Pooling

Connection pooling can be a great way to increase performance and scalability in your ODP.NET applications; however, it is also a feature that can be confusing to figure out as there are several parameters used in the connection string that control the behavior of the feature. These parameters are all fully documented in the documentation that ships with the data provider. In brief, the primary parameters used to control connection pooling are (see documentation for additional parameters):

  • Pooling - Enables or disables the connection pooling feature
  • Min Pool Size - Specifies the minimum number of connections that can be in the pool
  • Max Pool Size - Specifies the maximum number of connections that can be in the pool
  • Incr Pool Size - Specifies the increment value (the number of connections) to be added if the connection pool needs to grow in size
  • Decr Pool Size - Specifies the maximum number of connections that may be removed from the pool in a single "downsizing" operation
  • Connection Lifetime - Specifies the amount of time that a connection must be active after which the connection will not be returned to the pool (i.e. it will be disposed)
  • Connection Timeout - Specifies the amount of time that the provider will wait for an available connection from the pool

In ODP.NET each connection in the pool is represented by a physical connection to the database. There is a one-to-one relationship. If there are 4 connections in the pool there will be 4 corresponding connections in the database. This is another way of saying that ODP.NET connection pooling does not implement multiplexing (or sharing) of connections. Another important characteristic is that a connection pool is associated with a client process. This means that if you implement connection pooling in an application and there are 8 copies of that application simultaneously running at any given time, there will be 8 connections pools - one for each instance of the application process.

Because a connection pool is associated with a specific instance of an application it can possibly be a bit difficult to test the impact and operation of the various connection string parameters related to connection pooling. I've put together a simple project that uses threads to test connection pooling. This allows the single application process to create multiple connections (and thus multiple entries in the connection pool) to the database.

The sample code invokes a supplied PL/SQL procedure in the DBMS_LOCK package to sleep for a period of time (hard-coded as 30 seconds in the below code). The allows the connection to be "held active" and helps with testing the impact of the Connection Lifetime parameter. In order for this to work, the database user must be granted execution permission on the PL/SQL package. For example (as a DBA user via SQL*Plus):

grant execute on dbms_lock to orademo;

After starting the specified number of threads (and connections) the application holds the connection as active for the determined period of time, and then disposes of the connection object placing it back into the connection pool (depending on how you set the parameters!). In order to monitor the connections in the database, I executed the following query as a DBA user via SQL*Plus:

SELECT   SID,
         SERIAL#,
         USERNAME,
         STATUS,
         to_char(LOGON_TIME, 'DD-MON-YY HH24:MI') LOGON_TIME
FROM     V$SESSION
WHERE    LENGTH(USERNAME) > 1
ORDER BY 3, 4, 1;

The output from the application is as follows:

Thread 1 started...
Thread 2 started...
Thread 3 started...
Thread 4 started...
Thread 5 started...
Thread 6 started...
Thread 7 started...
Thread 8 started...
Thread 1 completing...
Thread 2 completing...
Thread 3 completing...
Thread 4 completing...
Thread 5 completing...
Thread 6 completing...
Thread 7 completing...
Thread 8 completing...

Paused after threads complete.
Monitor connections using tool of choice.
Be sure to wait several minutes (approx. 6) for clean-up to occur.

Press Enter when finished.

It is important to execute the monitoring query while the application is "paused" otherwise the application will terminate and the connection pool will be destroyed.

Here is the output of the monitoring query after the application has reached the "pause point":

SQL> SELECT   SID,
  2           SERIAL#,
  3           USERNAME,
  4           STATUS,
  5           to_char(LOGON_TIME, 'DD-MON-YY HH24:MI') LOGON_TIME
  6  FROM     V$SESSION
  7  WHERE    LENGTH(USERNAME) > 1
  8  ORDER BY 3, 4, 1;

       SID    SERIAL# USERNAME                       STATUS   LOGON_TIME
---------- ---------- ------------------------------ -------- ---------------
       135          2 ORADEMO                        INACTIVE 28-FEB-08 18:22
       136          2 ORADEMO                        INACTIVE 28-FEB-08 18:22
       137          2 ORADEMO                        INACTIVE 28-FEB-08 18:22
       138          4 ORADEMO                        INACTIVE 28-FEB-08 18:22
       139          6 ORADEMO                        INACTIVE 28-FEB-08 18:22
       142          3 ORADEMO                        INACTIVE 28-FEB-08 18:22
       143          3 ORADEMO                        INACTIVE 28-FEB-08 18:22
       164          6 ORADEMO                        INACTIVE 28-FEB-08 18:22
       134          2 SYSTEM                         ACTIVE   28-FEB-08 18:22

9 rows selected.

This shows the 8 connections from the 8 threads (adjustable) in the application and my SQL*Plus connection.

If you simply let the application sit paused for a period of time, the connections will automatically be cleaned up by ODP.NET (again, depending on the parameter values, but the values as provided in the below code allow this to occur). On my system it took approximately 6 minutes for the connection pool to be "cleaned" (i.e. reduced to the minimum of one connection based on the values I used in the connection string).

[ after waiting for about 6 minutes ]

SQL> /

       SID    SERIAL# USERNAME                       STATUS   LOGON_TIME
---------- ---------- ------------------------------ -------- ---------------
       164          6 ORADEMO                        INACTIVE 28-FEB-08 18:22
       134          2 SYSTEM                         ACTIVE   28-FEB-08 18:22

2 rows selected.

This shows the connection pool has been "downsized" to the minimum number (one) I specified.

I encourage you to experiment with the different parameters and values to see how they operate and what impact they have on your system.

The Code

using System;
using System.Threading;
using System.Data;
using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;

namespace Miscellaneous
{
  class Program
  {
    static void Main(string[] args)
    {
      // change connection string as appropriate and experiment with different values
      const string constr = "User Id=orademo; " +
                            "Password=oracle; " +
                            "Data Source=orademo; " +
                            "Enlist=false;" +
                            "Pooling=true;" +
                            "Min Pool Size=1;" +
                            "Max Pool Size=8;" +
                            "Incr Pool Size=1;" +
                            "Decr Pool Size=8;" +
                            "Connection Lifetime=60;" +
                            "Connection Timeout=5";

      // set to number of threads / connections to use
      const int numberOfThreads = 8;

      // create arrays for class instances and events
      ConnectionThread[] threadArray = new ConnectionThread[numberOfThreads];
      ManualResetEvent[] doneEvents = new ManualResetEvent[numberOfThreads];

      // populate arrays and start threads
      for (int i = 0; i < numberOfThreads; i++)
      {
        // initialize each event object in the array
        doneEvents[i] = new ManualResetEvent(false);
        // create a new instance of the ConnectionThread class
        ConnectionThread ct = new ConnectionThread(i + 1, constr, doneEvents[i]);
        // assign the new instance to array element
        threadArray[i] = ct;
        // Queue the thread for execution and specify the method to execute
        // when thread becomes available from the thread pool
        ThreadPool.QueueUserWorkItem(ct.ThreadPoolCallback);
      }

      // wait until all threads have completed
      WaitHandle.WaitAll(doneEvents);

      // keep application from terminating while monitoring connections in database
      // if the application / process terminates all connections will be removed
      Console.WriteLine();
      Console.WriteLine("Paused after threads complete.");
      Console.WriteLine("Monitor connections using tool of choice.");
      Console.WriteLine("Be sure to wait several minutes (approx. 6) for clean-up to occur.");
      Console.WriteLine();
      Console.Write("Press Enter when finished.");
      Console.ReadLine();
    }
  }

  public class ConnectionThread
  {
    // private class members
    private int _threadNumber;
    private string _constr;
    private ManualResetEvent _doneEvent;

    // parameterized constructor
    public ConnectionThread(int threadNumber, string connectionString, ManualResetEvent doneEvent)
    {
      _threadNumber = threadNumber;
      _constr = connectionString;
      _doneEvent = doneEvent;
    }

    // this will be called when the thread starts
    public void ThreadPoolCallback(Object threadContext)
    {
      Console.WriteLine("Thread {0} started...", _threadNumber);

      // do some database work that holds the connection open
      DoWork();

      Console.WriteLine("Thread {0} completing...", _threadNumber);

      // signal that this thread is done
      _doneEvent.Set();
    }

    public void DoWork()
    {
      // create and open connection
      OracleConnection con = new OracleConnection(_constr);
      con.Open();

      // command to do the database work (simply hold connection open for 30 seconds)
      // NOTE: execute privilege must be granted to user on the dbms_lock package
      OracleCommand cmd = con.CreateCommand();
      cmd.CommandText = "begin dbms_lock.sleep(30); end;";

      // execute the anonymous pl/sql block to does nothing but sleep to hold the connection
      cmd.ExecuteNonQuery();

      // clean up and return connection to pool (depending on connection string settings)
      cmd.Dispose();
      con.Dispose();
    }
  }
}

Thursday, January 31, 2008

VB, OracleCommandBuilder, and What's Wrong With This Code?

Below is some sample code. Create a new VB console application, add a reference to the Oracle.DataAccess.dll assembly, and add Oracle.DataAccess.Client to the Imported namespaces using the properties page for the project. Copy and paste the code into the .vb source file. Do you get any errors? Does it compile? Is there anything wrong with this code?

Module Module1
  Sub Main()
    ' connection string -- change as necessary
    Dim constr As String = "User Id=scott; " & _
                           "Password=tiger; " & _
                           "Data Source=orademo; " & _
                           "Enlist=false; " & _
                           "Pooling=false"

    ' will use below
    Dim ds As New DataSet
    Dim da As OracleDataAdapter
    Dim con As OracleConnection

    ' open connection
    con = New OracleConnection(constr)
    con.Open()

    ' get a data adapter for the emp table
    da = New OracleDataAdapter("select * from emp", con)

    ' get the schema information
    da.FillSchema(ds, SchemaType.Source)

    ' get command builder from data adapter
    Dim cb As New OracleCommandBuilder(da)

    ' set the insert command from the command builder
    da.InsertCommand = cb.GetInsertCommand(True)

    ' simple prompt to keep console from closing when
    ' run from within the Visual Studio environment
    Console.WriteLine("ENTER to continue...")
    Console.ReadLine()
  End Sub
End Module

If the code compiles, when you try to run it, do you get an error? If you get an error, does it indicate: "The DataAdapter.SelectCommand property needs to be initialized."

Hmm. Strange. It looks like the SelectCommand is being initialized right there in the OracleDataAdapter constructor: da = New OracleDataAdapter("select * from emp", con)

A quick check in the Oracle Data Provider for .NET documentation shows that the constructor for OracleDataAdapter can definitely take a string which represents the select command text and a connection object. Yet you may get an error stating that the SelectCommand property needs to be initialized.

If this code compiled fine and yet you get a runtime error, here's a pointer - add the following to the top of the source code file:

' Enable Option Strict checking
Option Strict On

(You can also change this via the project property page on the Compile tab)

Did anything change in the source code? In particular this part of the source code:

' set the insert command from the command builder
da.InsertCommand = cb.GetInsertCommand(True)

If Option Strict is not enabled, which happens to be the default, VB will try to perform an implicit conversion from one data type to another data type with no compile-time warning. However, at runtime the conversion may fail. In addition, if you consult the ODP.NET documentation you will see that there is no OracleCommandBuilder.GetInsertCommand that takes a Boolean. However, OracleCommandBuilder inherits from System.Data.Common.DbCommandBuilder which does have an overloaded GetInsertCommand that takes a Boolean.

So, in the case of the (incorrect) code above, DbCommandBuilder.GetInsertCommand(Boolean) is the method that was to be invoked not the OracleCommandBuilder.GetInsertCommand. This is the cause of the runtime error and explains the complaint about the select command not being initialized. If you try to compile the code with Option Strict enabled you should receive: "Option Strict On disallows implicit conversions from 'System.Data.Common.DbCommand' to 'Oracle.DataAccess.Client.OracleCommand'."

From my perspective it seems like a good idea to always enable Option Strict.

Thursday, December 27, 2007

ODAC 11g Released

ODAC 11g Version 11.1.0.6.20 (including the Oracle Developer Tools for Visual Studio .NET and ODP.NET) has now been released to production.

Get your download here and check out the new features list!

The Bulk Copy and User-Defined Types (UDT) features are likely to be well received along with the Instant Client support.

Wednesday, December 12, 2007

Dynamically Creating a Variable In-List

Frequently SQL statements in code use an "in-list". That is, SQL that resembles the following:

select   employee_id,
         first_name,
         last_name
from     employees
where    employee_id in (102,127,206)

Here I am providing an in-list for the employee_id column. Using the HR sample that ships with Oracle Database, the above query returns the following results:

EMPLOYEE_ID FIRST_NAME           LAST_NAME
----------- -------------------- -------------------------
        102 Lex                  De Haan
        206 William              Gietz
        127 James                Landry

Of course you may wish to vary the number of items in the in-list as well as the actual values. When the value of an item in the list changes, you may wish to use a bind variable for that item. See my Oracle Magazine column here for more details on bind variables and why you might want to use them.

The above SQL statement re-factored to use bind variables would look like:

select   employee_id,
         first_name,
         last_name
from     employees
where    employee_id in (:0,:1,:2)

In this re-factored statement I am still using 3 values in the in-list. This is definitely not a requirement: the in-list can contain anywhere from 1 to 1000 items. See the official Oracle documentation here for more details.

When attempting to use an in-list with bind variables you may be tempted to supply the comma-delimited values as a single string argument to the in-list. This may resemble:

select   employee_id,
         first_name,
         last_name
from     employees
where    employee_id in (:0)

In this case you may try to pass the string "102,127,206" as the value for the bind variable. However, this will not work as you might expect. You will receive "ORA-1722: invalid number" if you try this because the string "102,127,206" can not be converted to a numeric value. Instead you must create the statement with a separate bind variable for each item you wish to pass in the in-list.

One technique to do this uses a StringBuilder object to create the SQL statement and dynamically build the in-list items. Here is some sample code that illustrates this technique:

using System;
using System.Data;
using System.Text;
using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;

namespace VariableInListDemo
{
  class Program
  {
    static void Main(string[] args)
    {
      // change connection string for your environment
      string constr = "User Id=hr;" +
                      "Password=hr;" +
                      "Data Source=orademo;" +
                      "Pooling=false;" +
                      "Enlist=false";

      // the "base" sql statement to which the in-list will be appended
      StringBuilder sbSQL = new StringBuilder("select employee_id, " +
                                              "first_name, " +
                                              "last_name " +
                                              "from employees " +
                                              "where employee_id in (");

      // if no command-line arguments specified, simply return
      if (args.Length == 0)
      {
        Console.WriteLine("No employee_id values specified...");
        Console.WriteLine("Please specify values such as: 102,127,206");
        return;
      }
      // create and open connection
      OracleConnection con = new OracleConnection(constr);
      con.Open();

      // split the command-line argument into separate tokens
      // the command-line argument should be similar to:
      // 102,127,206
      string[] EmpNos = args[0].Split(',');

      // build the in-list part of the sql
      for (int i = 0; i < EmpNos.Length; i++)
      {
        // append the bind variables into the sql
        sbSQL.Append(":" + i.ToString());

        // add a "," after all but the last bind variable
        if (i < EmpNos.Length - 1)
        {
          sbSQL.Append(",");
        }
      }

      // close the in-list with the trailing ")"
      sbSQL.Append(")");

      // create the command object
      OracleCommand cmd = con.CreateCommand();
      cmd.CommandText = sbSQL.ToString();

      // create an array of OracleParameter objects for each
      // empno specified on the command-line
      OracleParameter[] EmpNoParams = new OracleParameter[EmpNos.Length];

      // set the properties for each parameter
      // and add to parameters collection
      for (int i = 0; i < EmpNos.Length; i++)
      {
        EmpNoParams[i] = new OracleParameter();
        EmpNoParams[i].OracleDbType = OracleDbType.Decimal;
        EmpNoParams[i].Value = Convert.ToDecimal(EmpNos[i]);

        cmd.Parameters.Add(EmpNoParams[i]);
      }

      // create a data reader
      OracleDataReader dr = cmd.ExecuteReader();

      // iterate over the reader and display to console
      while (dr.Read())
      {
        Console.WriteLine(dr[0].ToString() + ", " +
                          dr[1].ToString() + ", " +
                          dr[2].ToString());
      }

      // add clean up code as necessary for your environment

      // prevent console from closing when run from VS environment
      Console.WriteLine();
      Console.WriteLine("ENTER to continue...");
      Console.ReadLine();
    }
  }
}

Running the above sample from a console window using the values "102,127,206" produces the following results:

C:\>VariableInListDemo.exe 102,127,206
102, Lex, De Haan
206, William, Gietz
127, James, Landry

ENTER to continue...

If you need to use a variable in-list in your application, I encourage you to experiment with this technique using different values and numbers of items as a possible solution.

Saturday, November 24, 2007

Enable Database Auditing

I was recently asked about a "trick" I sometimes use to help troubleshoot issues in connecting to a database - in particular, when using an Operating System authenticated connection. The "trick" is really very simple: enable database auditing, audit connections, attempt to connect as the user having the issue, and then review the results.

Oracle have, of course, much more thoroughly documented auditing than I will do here. So, as usual, I recommend taking a look at the official documentation for 9i Release 2, 10g Release 2, and/or 11g Release 1 if needed.

Beginning with the 11g release, auditing is enabled by default and the audit stream is, also by default, captured in the database in a sys-owned table called "aud$". The audit information can be captured into the operating system audit trail rather than the database if desired. Auditing is enabled (or disabled) by the audit_trail parameter. The Database Reference Guide for 9i Release 2, 10g Release 2, and 11g Release 1 contains the valid settings for this parameter as well as a description for each setting.

Verify if Auditing is Enabled

To verify if auditing is enabled in the database, connect to the database as a dba (or sysdba) user via SQL*Plus. I will use a sysdba user here as I already know that auditing is not enabled and I will, therefore, need to enable it and restart the instance.

SQL*Plus: Release 10.2.0.3.0 - Production on Sat Nov 24 15:00:05 2007

Copyright (c) 1982, 2006, Oracle.  All Rights Reserved.

SQL> connect / as sysdba
Connected.
SQL> show parameter audit_trail

NAME        TYPE        VALUE
----------- ----------- -----
audit_trail string      NONE

Enable Auditing if Necessary

Since the audit_trail parameter is NONE, I need to enable auditing by setting the value to DB, stopping the instance, starting the instance, and then verifying the change:

SQL> alter system set audit_trail='DB' scope=spfile;

System altered.

SQL> shutdown immediate;

[ database shutdown ]

SQL> startup open;

[ database opened ]

SQL> show parameter audit_trail

NAME        TYPE        VALUE
----------- ----------- -----
audit_trail string      DB

Enable Connection Auditing

In order to audit the connection attempts to the database, I need to tell Oracle that I want to do so:

SQL> audit connect;

Audit succeeded.

Attempt to Connect

To illustrate the auditing of connections, I will connect as a valid user then attempt to connect as a non-existent user. Both of these connection attempts are from a new SQL*Plus session:

SQL> connect hr
Enter password:
Connected.
SQL> connect bad_user
Enter password:
ERROR:
ORA-01017: invalid username/password; logon denied

Warning: You are no longer connected to ORACLE.

Review the Results

Using a SQL*Plus session as a DBA user (here I am using my sysdba session previously started) you can query the sys.aud$ table for the results of the connection auditing:

SQL> select userid, returncode from sys.aud$;

USERID   RETURNCODE
-------- ----------
HR                0
BAD_USER       1017

2 rows selected.

Here you can see that my "good" connection attempt was done as the HR user and that the return code was 0 while the "bad" connection attempt was done as the BAD_USER user (which does not exist) and the return code was 1017. The 1017 return code corresponds to the "ORA-01017: invalid username/password; logon denied" error message.

Being able to capture and query this information can often times be helpful in determining the user id of an Operating System authenticated user that is having difficulty in establishing a connection to the database. Once you know the user id being used, you can ensure that you have properly created that user in the database. In addition, the dba_audit_session view presents information related to audited connect/disconnect operations. See your respective Oracle documentation for more details on this view.

Turning off the Connection Auditing

After performing the auditing steps above, you may wish to disable the connection auditing. To do so, simply enter the following command in SQL*Plus as a DBA user:

SQL> noaudit connect;

Noaudit succeeded.

Thursday, November 15, 2007

Cha-cha-cha-changes

With apologies to David Bowie.

Friday, 16 November 2007 marks the end (at least of this phase) of my tenure with Oracle Corporation. I will be turning in my badge. I worked with a fantastic and dedicated bunch of individuals. I wish them all the very best of luck and success. I will, of course, continue to be an Oracle advocate and will continue my relationship with Oracle "from the outside".

Tuesday, November 13, 2007

Oracle 11g and ORA-01017: invalid username/password; logon denied

If you have recently created an 11g database one behavior change that you may find confusing (if you are not aware of it) is that passwords are now case-sensitive by default. This behavior is controlled by the SEC_CASE_SENSITIVE_LOGON parameter. This behavior is easily demonstrated in SQL*Plus:

SQL*Plus: Release 11.1.0.6.0 - Production on Tue Nov 13 19:36:05 2007

Copyright (c) 1982, 2007, Oracle.  All rights reserved.

SQL> connect system/oracle
Connected.
SQL> show parameter sec_case_sensitive_logon

NAME                     TYPE    VALUE
------------------------ ------- -----
sec_case_sensitive_logon boolean TRUE

SQL> create user test identified by demo;

User created.

SQL> grant create session to test;

Grant succeeded.

As you can see, on my system the value of the "sec_case_sensitive_logon" logon parameter is the default of "true". This means that passwords are case-sensitive. Therefore, in order to connect as the test user I need to specify the password in lower case:

SQL> connect test/demo
Connected.

If I specify the password in anything other than the lower case I used when the user was created I will receive an error:

SQL> connect test/DEMO
ERROR:
ORA-01017: invalid username/password; logon denied

SQL> connect test/Demo
ERROR:
ORA-01017: invalid username/password; logon denied

If you use SQL*Plus for your administrative tasks it is also possible to encounter a potentially confusing situation even if you are aware of this password case-sensitivity:

SQL> connect system/oracle
Connected.
SQL> drop user test cascade;

User dropped.

SQL> create user test identified by demo;

User created.

SQL> grant create session to test;

Grant succeeded.

SQL> connect test/demo
ERROR:
ORA-01017: invalid username/password; logon denied

Warning: You are no longer connected to ORACLE.

I just demonstrated that the password is case-sensitive and I am entering the password in lower case as I specified when I created the user. Or did I?

SQL> connect test/DEMO
Connected.

Well, clearly I can connect by specifying the password in upper case, but I created the user with the password in lower case and I know the value of the sec_case_sensitive_logon parameter is true...

The culprit in this case is a SQL*Plus system variable: sqlcase

SQL> show sqlcase
sqlcase UPPER

Ah ha! Because the value of the sqlcase system variable was UPPER, SQL*Plus automatically converted my SQL (including the password) to upper case. So, while I did enter the password in lower case, it was transparently converted to upper case before the statement was submitted to the database; therefore, my user was created with an upper case password!

For more information on password case sensitivity see:

What Are the Oracle Database Built-in Password Protections?
http://download.oracle.com/docs/cd/B28359_01/network.111/b28531/authentication.htm#CHDBBGFG

Enabling or Disabling Password Case Sensitivity
http://download.oracle.com/docs/cd/B28359_01/network.111/b28531/authentication.htm#CHDJDCGI

Saturday, November 10, 2007

Configuring Windows Authentication

In order to connect to Oracle running on Windows using an operating system authenticated account, there are surprisingly few steps to take:

1. Ensure the sqlnet.ora file has a required entry

2. Verify (or set) a database/instance parameter

3. Create a database user

When connecting to the database using operating system authentication, you do not need to specify the "normal" user id -- instead you simply use a "/" character (without the quotes) and no password (the whole point!). Oracle allows for two classes of operating system authenticated users: enterprise users and external users. I am illustrating how to create and connect as an external user (i.e. everything is on the same machine). Creating and using an enterprise user requires a directory server and additional coordination between the database administrator and the operating system/directory server administrator.

The sqlnet.ora file Entry

When Oracle is initially installed and configured on the Windows platform the sqlnet.ora file is created in the %ORACLE_HOME%\network\admin folder. There may be multiple entries in this file; however, in order to connect using o/s authentication the file must have an entry similar to:

SQLNET.AUTHENTICATION_SERVICES = (NTS)

If this entry is not present in the sqlnet.ora file you are using then a connection attempt using o/s authentication will fail.

The Database/Instance Parameter

The os_authent_prefix parameter controls whether or not Oracle adds a prefix to the user id being presented during the o/s authentication process. By default the value of this parameter is "OPS$" though it is not uncommon for it to be changed or simply be set to an empty string (""). However, in order to create the user in the database, the value of this parameter must be known. Fortunately, determining the value is as simple as connecting to the database as a capable of successfully executing the SQL*Plus "show parameter" command:



SQL> show parameter os_authent_prefix


NAME TYPE VALUE
----------------- ----------- -----
os_authent_prefix string OPS$


As you can see, on my system this value has not been changed from the default value. This means that Oracle will prefix the "OPS$" value to the beginning of my Windows user name when I attempt to connect using o/s authentication. The Windows user name presented will be of the form DOMAIN or HOSTNAME\USERNAME. On my laptop (which is not part of a domain) my Windows user name will be presented to Oracle as "LIVERPOOL\MARKWILL".

Creating the Database User

Knowing that the value of the os_authent_prefix parameter is "OPS$" and that my Windows user name is "LIVERPOOL\MARKWILL" a database user named "OPS$LIVERPOOL\MARKWILL" will need to be created in the database:



SQL*Plus: Release 11.1.0.6.0 - Production on Sat Nov 10 14:07:33 2007


Copyright (c) 1982, 2007, Oracle. All rights reserved.


SQL> connect / as sysdba
Connected.
SQL> create user "OPS$LIVERPOOL\MARKWILL" identified externally
2 default tablespace users
3 temporary tablespace temp
4 quota unlimited on users;


User created.


SQL> grant connect, alter session to "OPS$LIVERPOOL\MARKWILL";


Grant succeeded.


That's it!

Now I am ready to test my freshly created user that will connect to the database via my Windows-authenticated account (notice the connect string is just the "/" -- I am connecting to the database specified by the ORACLE_SID value on my system):


SQL*Plus: Release 11.1.0.6.0 - Production on Sat Nov 10 14:11:14 2007


Copyright (c) 1982, 2007, Oracle. All rights reserved.


SQL> connect /
Connected.
SQL> COL USERNAME FORMAT A32
SQL> COL DATABASE FORMAT A24
SQL> SELECT A.USERNAME,
2 B.GLOBAL_NAME DATABASE
3 FROM USER_USERS A,
4 GLOBAL_NAME B;


USERNAME DATABASE
-------------------------------- ------------------------
OPS$LIVERPOOL\MARKWILL LT11GR1.SAND


1 row selected.

If you wish to connect to a database by explicitly specifying a tns alias the connect string would look like:

connect /@tnsalias

For example:

connect /@lt11gr1

Specifying an Operating System Authenticated Account in ODP.NET

Just as the connect string in SQL*Plus consists of a "/" and possibly a tns alias, the connect string in an ODP.NET application can be as simple as:

"User Id=/"

or

"User Id=/; Data Source=mydatasource"

NOTE: There is a behaviour change from ODP.NET versions prior to 11g and the 11g release. In previous versions connection pooling was not used for operating system authenticated connections regardless of the setting of the "pooling" parameter in the connection string. Beginning with the 11g release, connection pooling is now supported using operating system authenticated connections.

Happy connecting!

Friday, October 19, 2007

ODP.NET Tip: Using Pipelined Functions

When using PL/SQL to return data to your ODP.NET client application typically that data is returned using a REF CURSOR or using PL/SQL Associative Arrays. I have written about using REF CURSORs here and about using PL/SQL Associative Arrays here.

However, another method is also available: using pipelined functions. Since the official documentation covers pipelined functions well, I will not go over them here.

The following simple PL/SQL code can be used to demonstrate this technique.

Note: I am using version 10.2.0.3 of all components in this sample.

-- the pl/sql package
create or replace package pipeline_test as
  -- output rows will be of this type
  type output_type is record (
    first_name varchar2(16),
    last_name  varchar2(16),
    instrument varchar2(16)
  );
 
  -- the type the function will return
  type output_set is table of output_type;
 
  -- the pipelined function that returns the rows
  function get_rows return output_set pipelined;
end;
 
-- the pl/sql package body
create or replace package body pipeline_test as
  function get_rows return output_set pipelined is
    -- used to build each row
    this_rec output_type;
  begin
    -- add row for John
    this_rec.first_name := 'John';
    this_rec.last_name := 'Lennon';
    this_rec.instrument := 'Guitar';
    pipe row(this_rec);
 
    -- add row for Paul
    this_rec.first_name := 'Paul';
    this_rec.last_name := 'McCartney';
    this_rec.instrument := 'Bass';
    pipe row(this_rec);
 
    -- add row for George
    this_rec.first_name := 'George';
    this_rec.last_name := 'Harrison';
    this_rec.instrument := 'Guitar';
    pipe row(this_rec);
 
    -- add row for Ringo
    this_rec.first_name := 'Ringo';
    this_rec.last_name := 'Starr';
    this_rec.instrument := 'Drums';
    pipe row(this_rec);
 
    return;
  end;
end;


Once you have created the PL/SQL Package and Package Body you can test the functionality from within SQL*Plus as follows:

SQL> select * from table(pipeline_test.get_rows());
 
FIRST_NAME       LAST_NAME        INSTRUMENT
---------------- ---------------- ----------------
John             Lennon           Guitar
Paul             McCartney        Bass
George           Harrison         Guitar
Ringo            Starr            Drums
 
4 rows selected.


The output is precisely what we want: a "normal" table.

After having created the PL/SQL code and verifying it functions as expected, the ODP.NET code is equally straight-forward:

using System;
using System.Data;
using Oracle.DataAccess.Client;
using Oracle.DataAccess.Types;
 
namespace PipelineTest
{
  class Program
  {
    // connection string - change as appropriate
    private const string constr = "User Id=markwill;" +
                                  "Password=oracle;" +
                                  "Data Source=otndemo;" +
                                  "Pooling=false;" +
                                  "Enlist=false";
 
    static void Main(string[] args)
    {
      // create and open connection
      OracleConnection con = new OracleConnection(constr);
      con.Open();
 
      // create and setup command
      // the "table" keyword is used with the pipelined function
      // in the pl/sql package body to get the rows
      OracleCommand cmd = con.CreateCommand();
      cmd.CommandText = "select * from table(pipeline_test.get_rows())";
 
      // get a data reader
      OracleDataReader dr = cmd.ExecuteReader();
 
      // loop through the data reader printing the data to the console
      while (dr.Read())
      {
        Console.WriteLine("{0} {1} - {2}", dr[0].ToString(), dr[1].ToString(), dr[2].ToString());
      }
 
      Console.WriteLine();
 
      // clean up
      dr.Dispose();
      cmd.Dispose();
      con.Dispose();
 
      // prevent console window from automatically closing
      Console.WriteLine("ENTER to continue...");
      Console.ReadLine();
    }
  }
}

Friday, September 21, 2007

ODP.NET: Using ObjectDataSource, Ref Cursors, and Custom Classes

When creating applications such as ASP.NET applications, often times the application is split into various conceptual or physical layers. There are typically three layers in such a scheme and they may generally be broken out as the presentation layer, the business logic layer, and the data access layer. Depending on your specific needs, standards, etc. sometimes the layers may be folded into a single layer. For example, you may choose to implement your business logic using stored procedures and co-locate the business logic layer with the data access layer in the database.

There are plusses and minuses (or trade-offs) associated with the architectural decisions for an application and I don't intend to propose any one being better than another. Instead, I would like to illustrate a simple method of creating a presentation layer and a data access layer using ObjectDataSource controls, Ref Cursors, and custom classes. There is no business logic layer in this simple example as it would merely be a proxy or wrapper of the data access layer. The data access layer returns collections of custom classes to the presentation layer using the .NET Framework 2.0 System.Collections.Generic.List generic type.

The sample uses a simple ASP.NET page:




In order to provide the data access layer with data from the database the following PL/SQL Package and Package Body should be created as the HR user in the database you plan to use:

create or replace package human_resources as
  procedure get_departments (p_departments out sys_refcursor);
 
  procedure get_department_employees(p_department_id in employees.department_id%type,
                                     p_employees out sys_refcursor);
end;
/
 
create or replace package body human_resources as
  procedure get_departments (p_departments out sys_refcursor) is
  begin
    open p_departments for
      select   department_id,
               department_name
      from     departments
      order by department_name;
  end;
 
  procedure get_department_employees(p_department_id in employees.department_id%type,
                                     p_employees out sys_refcursor) is
  begin
    open p_employees for
    select   employee_id,
             first_name,
             last_name
    from     employees
    where    department_id = p_department_id
    order by last_name,
             first_name;
  end;
end;
/



This sample uses a single ASP.NET web page (default.aspx) and three C# code files:

Department.cs -- the department class in the .NET code (get_departments procedure)

Employee.cs -- the employee class in the .NET code (get_department_employees procedure)

EmployeeDAL.cs -- the data access layer. This code accesses the database and returns the collection of either Department or Employee objects to the presentation layer.

The components that make up the Visual Studio Solution are as follows in the Solution Explorer:



As with the PL/SQL code in the database, the C# code is simple and omits many normal features such as error handling. In addition, the classes only implement "get" functionality for the class properties in order to keep the code as short and as simple as possible.

The Department class has DepartmentID and DepartmentName properties and is as follows:

public class Department {
  private int _department_id;
  private string _department_name;
 
  public int DepartmentID {
    get {
      return _department_id;
    }
  }
 
  public string DepartmentName {
    get {
      return _department_name;
    }
  }
 
  public Department(int DepartmentID, string DepartmentName) {
    _department_id = DepartmentID;
    _department_name = DepartmentName;
  }
}



The Employee class exposes EmployeeID, FirstName, and LastName properties:

public class Employee {
  private int _employee_id;
  private string _first_name;
  private string _last_name;
 
  public int EmployeeID {
    get {
      return _employee_id;
    }
  }
 
  public string FirstName {
    get {
      return _first_name;
    }
  }
 
  public string LastName {
    get {
      return _last_name;
    }
  }
 
  public Employee(int EmployeeID, string FirstName, string LastName) {
    _employee_id = EmployeeID;
    _first_name = FirstName;
    _last_name = LastName;
  }
}



The EmployeesDAL class handles connecting to the database, executing the stored procedures, building the collection of objects, and returning those objects to the caller:

public class EmployeesDAL {
  private static string constr = "User Id=hr; Password=hr; Data Source=lt10gr2; Enlist=false";
 
  public List<Department> GetDepartments()
  {
    OracleConnection con = new OracleConnection(constr);
    con.Open();
 
    OracleCommand cmd = con.CreateCommand();
    cmd.CommandText = "human_resources.get_departments";
    cmd.CommandType = CommandType.StoredProcedure;
 
    OracleParameter p_ref_cur = new OracleParameter();
    p_ref_cur.OracleDbType = OracleDbType.RefCursor;
    p_ref_cur.Direction = ParameterDirection.Output;
 
    cmd.Parameters.Add(p_ref_cur);
 
    OracleDataReader dr = cmd.ExecuteReader();
 
    List<Department> Departments = new List<Department>();
 
    while (dr.Read())
    {
      Department thisDept = new Department(dr.GetInt32(0), dr.GetString(1));
 
      Departments.Add(thisDept);
    }
 
    dr.Dispose();
    cmd.Dispose();
    con.Dispose();
 
    return Departments;
  }
 
  public List<Employee> GetDepartmentEmployees(int p_department_id) {
    OracleConnection con = new OracleConnection(constr);
    con.Open();
 
    OracleCommand cmd = con.CreateCommand();
    cmd.CommandText = "human_resources.get_department_employees";
    cmd.CommandType = CommandType.StoredProcedure;
 
    OracleParameter p_dept_id = new OracleParameter();
    p_dept_id.OracleDbType = OracleDbType.Decimal;
    p_dept_id.Value = p_department_id;
 
    OracleParameter p_ref_cur = new OracleParameter();
    p_ref_cur.OracleDbType = OracleDbType.RefCursor;
    p_ref_cur.Direction = ParameterDirection.Output;
 
    cmd.Parameters.Add(p_dept_id);
    cmd.Parameters.Add(p_ref_cur);
 
    OracleDataReader dr = cmd.ExecuteReader();
 
    List<Employee> Employees = new List<Employee>();
 
    while (dr.Read()) {
      Employee thisEmp = new Employee(dr.GetInt32(0), dr.GetString(1), dr.GetString(2));
 
      Employees.Add(thisEmp);
    }
 
    p_dept_id.Dispose();
    p_ref_cur.Dispose();
    dr.Dispose();
    cmd.Dispose();
    con.Dispose();
 
    return Employees;
  }
}



The ASP.NET page consists of simple text to denote the Department data which is presented in a DropDownList, a GridView control to display the employees in the selected department, and two ObjectDataSource controls. This is the Default.aspx page at design time:



To configure the ObjectDataSource1 control select "Configure Data Source..." from the quick tasks configuration:

Choose a business object:



Click Next and then select the GetDeparments method for the SELECT tab and click Finish:



Next, configure the DropDownList1 control which displays the Department information. Ensure that the EnableAutoPostBack option is checked in the quick task configuration or that the AutoPostBack property is True in the Properties window. To configure the Data Source for the control, select "Choose Data Source..." from the quick task configuration and complete as follows:



The ObjectDataSource2 control should be configured next by clicking the "Configure Data Source..." option on the quick tasks configuration:

Again, choose a Business Object:



Followed by the GetDepartmentEmployees method for the SELECT tab:



The GetDepartmentEmployees method takes an input parameter that specifies the Department ID for which employees should be retrieved. Therefore, it is necessary to configure the parameter input value so that the Department ID selected by the user in the DropDownList1 control is passed to the GetDepartmentEmployees method:




Finally, associate the ObjectDataSource2 control with the GridView1 control by choosing "ObjectDataSource2" in the "Choose Data Source" drop-down list for the GridView quick tasks. Of course you may also choose to enable paging and formatting as you desire.

Now that both the DropDownList1 and the GridView1 web controls are "hooked up" to their respective ObjectDataSource controls, and the ObjectDataSource controls are, in turn, configured to expose the data returned by the EmployeesDAL class, it is time to run the sample page!