22 September 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!

12 August 2007

ODP.NET Tip: Bind Variables and the BindByName Property

Note: For a brief introduction to bind variable usage in an ODP.NET application, see my September/October 2005 Oracle Magazine article "The Values That Bind".

When using bind variables in the CommandText property of an OracleCommand object, it is important to understand the meaning of the value of the BindByName property for the OracleCommand object. This property is a read/write Boole property which defaults to false. The fact that this property defaults to false means that ODP.NET operates, by default, in what is known as "bind by position" mode. In this mode, when OracleParameter objects are added to the parameters collection for an OracleCommand object, they must be added to the collection in the same order in which they appear in the CommandText property.

For example, say you have the following:

// assumes cmd is an object of type OracleCommand
cmd.CommandText = "select employee_id from hr.employees where first_name = :1 and last_name = :2"

In this sample, there are 2 bind variables used (denoted by :1 and :2 in the text). Using the default setting (false) for the BindByName property this means that the OracleParameter object for the first_name placeholder should be added to the parameters collection first, followed by the OracleParameter object for the last_name placeholder.

In contrast to this, if you set the BindByName property to true, you may add the OracleParameter objects to the parameters collection in any order you wish. Of course, if you set the property to true, you also need to ensure that the ParameterName property of the OracleParameter object matches the name used in the CommandText (without the ":").

Another area where the value of the BindByName property may be the source of confusion is when using a PL/SQL function. For example, say you have a function that returns a value and accepts 2 parameters as input. When you use this function in your code, the following pseudo-code represents what is sent to the database:

:return_value := some_function (:parameter1, :parameter2)

Notice that the return value is the first bind variable used. Therefore, in bind by position mode (the default!), you must add the OracleParameter object for the return value to the parameters collection first (not last!), followed by the parameter object for "parameter1", and then "parameter2".

10 May 2007

ODP.NET Tip: Ref Cursors and FetchSize

One useful technique for increasing performance in your ODP.NET applications is controlling the fetch size. In fact, I discussed this technique in the ODP.NET column in the July/August 2006 issue of Oracle Magazine. In terms of performance, fetching the data from the database to the client tier (or a middle tier) can be one of the more expensive operations in an application’s execution. By controlling the fetch size (i.e. the amount of data fetched during each round-trip to the database) you can dramatically decrease (or increase!) the amount of time taken to fetch the data.

However, if you are working with Ref Cursors rather than "plain" SQL statements, the ability to control the fetch size is not readily available. This is because the RowSize property of an OracleCommand object is set to 0 (zero) when working with PL/SQL code, as would be the case with Ref Cursor data. A simple example will illustrate why this is so. The RowSize property is an integer property. Let’s say you have the following Anonymous PL/SQL code block associated with an OracleCommand object:


begin
  open :1 for select * from scott.emp;
  open :2 for select * from scott.dept;
  open :3 for select * from scott.salgrade;
end;


Upon execution of this block, there will be three Ref Cursors opened. Therein lies the issue: A single integer property (RowSize) exposed by the OracleCommand object cannot properly represent the row size for the three opened Ref Cursors. Therefore, the property value is set to 0.

Typically you would create an OracleDataReader object for each of the Ref Cursors opened and read the data into the client. However, the OracleDataReader does not expose a RowSize property. Therefore, you can not set the FetchSize property on the OracleDataReader object to a specific number of rows using the normal technique of assigning the product of RowSize * NumberOfRowsToFetch to the FetchSize property. Keep in mind the RowSize property is 0 for the OracleCommand object and the OracleDataReader object does not expose a RowSize property.

However, if you have ever examined the members of the OracleDataReader in the debug window within Visual Studio, you may have noticed that the OracleDataReader does have a particularly enticing non-public member named "m_rowSize" which just happens to represent the row size. If only you could access that value you could then easily set the FetchSize property on the OracleDataReader to a specific (meaningful) value rather than be forced to accept the default of 64KB (65536). To see this, assume you have the following code:

OracleDataReader dr = cmd.ExecuteReader();

After executing this code while stepping through your application in the Visual Studio debugger, expand the "dr" node in the "Locals" window. Next navigate to the "Non-Public members" node, and expand this node. Scroll down the list and eventually you will find the "m_rowSize" member:

Image of m_rowSize in debugger

Now, of course, as this is a Non-Public member, you can’t just reference "m_rowSize" in your application code to retrieve this value. However, using the "reflection" capabilities of the .NET Framework, you can access this value. Once you have done that, you can set the value of the FetchSize property to whatever value is appropriate for your application.

Please note that this technique is not necessarily a supported technique, and, of course, if the "m_rowSize" member name should change, it would break any code using this technique - Caveat Emptor.

Here is a complete sample application using the SH sample schema. I used this schema simply because the SALES table is the largest table in the supplied sample schemas and, thus, is one that benefits from controlling the fetch size more than a small table (less than 64KB).



PL/SQL Code:

-- run as SH user

-- the SH user has the largest table in the demo schemas

create or replace procedure get_sales (p_refcur out sys_refcursor) is

begin

  -- open the cursor using the passed in ref cursor

  -- sys_refcursor is a built in type

  open p_refcur for

  select  *

  from    sales;

end;

/



C# Application Code:

using System;

using System.Data;

using System.Reflection;

using Oracle.DataAccess.Client;

using Oracle.DataAccess.Types;

 

namespace RefCursorFetchTest

{

  class Program

  {

    // connection string -- be sure to adjust for your environment

    public const string constr = "User Id=sh; Password=sh; Data Source=otndemo; Pooling=false; Enlist=false";

 

    static void Main(string[] args)

    {

      // create and open connection

      OracleConnection con = new OracleConnection(constr);

      con.Open();

 

      // enable extended sql tracing

      // this can be used to verify the number of rows fetched

      // uncomment to create trace file in user_dump_dest directory

      // EnableExtendedTrace(con);

 

      // do the fetch test

      // passing 0 for "rows" will use default fetch size of 64k

      FetchTest(con, 100);

      FetchTest(con, 1000);

      FetchTest(con, 10000);

      FetchTest(con, 0);

 

      // clean up

      con.Dispose();

 

      // prevent console window from automatically closing

      Console.Write("ENTER to continue...");

      Console.ReadLine();

    }

 

    static void EnableExtendedTrace(OracleConnection con)

    {

      // create, setup, and execute command to enable extended sql trace

      OracleCommand cmd = con.CreateCommand();

      cmd.CommandText = "alter session set events '10046 trace name context forever, level 4'";

      cmd.ExecuteNonQuery();

 

      // clean up

      cmd.Dispose();

    }

 

    static void FetchTest(OracleConnection con, int rows)

    {

      // used to track execution duration

      DateTime timeStart;

      DateTime timeEnd;

      double totalSeconds;

 

      // create and setup command to call stored procedure

      OracleCommand cmd = con.CreateCommand();

      cmd.CommandText = "get_sales";

      cmd.CommandType = CommandType.StoredProcedure;

 

      // create the ref cursor parameter and add to command parameter collection

      OracleParameter prm = new OracleParameter("p_refcur", OracleDbType.RefCursor, ParameterDirection.Output);

      cmd.Parameters.Add(prm);

 

      // get the data reader

      OracleDataReader dr = cmd.ExecuteReader();

 

      // There are 3 steps in getting the m_rowSize property value...

 

      // Step 1 - get the data reader type

      Type type = dr.GetType();

 

      // Step 2 - get the "m_rowSize" field info

      FieldInfo fi = type.GetField("m_rowSize", BindingFlags.Instance | BindingFlags.NonPublic);

 

      // Step 3 - get the value of m_rowSize for the dr object

      int rowSize = (int)fi.GetValue(dr);

 

      // if rows is greater than 0 use to set fetch size,

      // otherwise use default size (64k)

      if (rows > 0)

      {

        // set the fetch size on the dr object using the row size

        dr.FetchSize = rowSize * rows;

      }

 

      // capture test start time

      timeStart = DateTime.Now;

 

      // simply loop forcing the entire result set to be fetched

      while (dr.Read())

      {

      }

 

      // capture test end time

      timeEnd = DateTime.Now;

 

      // calculate total seconds for this test

      totalSeconds = timeEnd.Subtract(timeStart).TotalSeconds;

 

      // display time used for test

      Console.WriteLine("Number of rows = {0,5}: {1,8:###.0000} total seconds, fetch size = {2,9:###,###,###} bytes.", rows, totalSeconds, dr.FetchSize);

 

      // clean up

      dr.Dispose();

      prm.Dispose();

      cmd.Dispose();

    }

  }

}






Execution results on my laptop:
Number of rows = 100: 10.3125 total seconds, fetch size = 20,800 bytes.
Number of rows = 1000: 4.9219 total seconds, fetch size = 208,000 bytes.
Number of rows = 10000: 2.4219 total seconds, fetch size = 2,080,000 bytes.
Number of rows = 0: 4.0625 total seconds, fetch size = 65,536 bytes.
ENTER to continue...


06 May 2007

ODP.NET Tip: Retrieving dbms_output Text

If you frequently work with PL/SQL code from your ODP.NET application you may have encountered code that uses the dbms_output package. This is a common debugging technique employed in PL/SQL code. In fact, the Oracle® Database PL/SQL Packages and Types Reference 10g Release 2 (10.2) has this to say about the dbms_output package:

The DBMS_OUTPUT package enables you to send messages from stored procedures, packages, and triggers. The package is especially useful for displaying PL/SQL debugging information.

In practice, calls to dbms_output are embedded in the PL/SQL code and then the resulting text is often displayed in a character-based tool such as SQL*Plus. But what if you are using an ODP.NET application? How is it possible to retrieve values into your client application that have been output by the dbms_output package into a server-side buffer?

Fortunately the dbms_output package provides two procedures that can be used to accomplish this:

- get_line
- get_lines

As their names suggest, get_line is used to retrieve a single line from the buffer while get_lines can be used to retrieve multiple lines from the buffer.

In this tip I show you how to use both of these procedures in a single sample application.

In order to retrieve a single line of text, you use a parameter object that represents a Varchar2 data type and a parameter object that represents a Decimal data type. The Varchar2 parameter holds the actual line of text while the Decimal parameter holds the status code returned from the dbms_output package.

The code to retrieve multiple lines of text also uses two OracleParameter objects: one parameter object is a PL/SQL Associative Array to hold the lines of text and the other is a Decimal parameter that holds the number of lines to fetch before the call to dbms_output or the number of lines that were fetched after the call to dbms_output.

Be sure to review the documentation for dbms_output linked above.

Begin by creating the PL/SQL code as the database user that will connect to the database from the ODP.NET application. Of course, this is just sample code to illustrate this technique. Your real code would perform additional activities and almost certainly would be more complex.

The PL/SQL code for the sample package and package body:

create or replace package dbms_output_test as

  procedure emit_single_line;

  procedure emit_multiple_lines;

end;

/

 

create or replace package body dbms_output_test as

  procedure emit_single_line is

  begin

    /* enable dbms_output using defaults */

    dbms_output.enable;

 

    /* output a single line of text */

    dbms_output.put_line('This is a simple line of text emitted by dbms_output.');

  end;

 

  procedure emit_multiple_lines is

  begin

    /* enable dbms_output using defaults */

    dbms_output.enable;

 

    /* output multiple lines of text */

    dbms_output.put_line('Sgt. Pepper''s Lonely Hearts Club Band Track List');

    dbms_output.put_line('================================================');

    dbms_output.put_line('01 - Sgt. Pepper''s Lonely Hearts Club Band');

    dbms_output.put_line('02 - With a Little Help From My Friends');

    dbms_output.put_line('03 - Lucy in the Sky With Diamonds');

    dbms_output.put_line('04 - Getting Better');

    dbms_output.put_line('05 - Fixing a Hole');

    dbms_output.put_line('06 - She''s Leaving Home');

    dbms_output.put_line('07 - Being for the Benefit of Mr. Kite!');

    dbms_output.put_line('08 - Within You Without You');

    dbms_output.put_line('09 - When I''m Sixty-Four');

    dbms_output.put_line('10 - Lovely Rita');

    dbms_output.put_line('11 - Good Morning Good Morning');

    dbms_output.put_line('12 - Sgt. Pepper''s Lonely Hearts Club Band (Reprise)');

    dbms_output.put_line('13 - A Day in the Life');

  end;

end;

/



As you can see, this code simply outputs text using the dbms_output package and performs no other processing. The emit_single_line procedure outputs a single line of sample text while the emit_multiple_lines procedure outputs the track listing for Sgt. Pepper's Lonely Hearts Club Band by The Beatles.

The C# sample code is fairly well commented and should be easy to follow:

using System;

using System.Data;

using Oracle.DataAccess.Client;

using Oracle.DataAccess.Types;

 

namespace DbmsOutput

{

  class Program

  {

    // connect to default database using o/s authenticated account

    // be sure to adjust for your environment

    private const string constr = "user id=/; enlist=false; pooling=false";

 

    static void Main(string[] args)

    {

      // create and open connection

      OracleConnection con = new OracleConnection(constr);

      con.Open();

 

      // call method for single line output

      GetSingleLine(con);

 

      // call method for multiple line output

      GetMultipleLines(con);

 

      // clean up

      con.Dispose();

 

      // prevent console from closing automatically when run from within VS

      Console.Write("ENTER to continue...");

      Console.ReadLine();

    }

 

    static void GetSingleLine(OracleConnection con)

    {

      // call the procedure that emits a single line of text

      string stored_procedure = "dbms_output_test.emit_single_line";

 

      // anonymous pl/sql block to get the line of text

      string anonymous_block = "begin dbms_output.get_line(:1, :2); end;";

 

      // create command and execute the stored procedure

      OracleCommand cmd = con.CreateCommand();

      cmd.CommandText = stored_procedure;

      cmd.CommandType = CommandType.StoredProcedure;

      cmd.ExecuteNonQuery();

 

      // create parameters for the anonymous pl/sql block

      OracleParameter p_line = new OracleParameter("",

                                                  OracleDbType.Varchar2,

                                                  32000,

                                                  "",

                                                  ParameterDirection.Output);

 

      OracleParameter p_status = new OracleParameter("",

                                                    OracleDbType.Decimal,

                                                    ParameterDirection.Output);

 

      // set command text and parameters to get the text output

      // and execute the anonymous pl/sql block

      cmd.CommandText = anonymous_block;

      cmd.CommandType = CommandType.Text;

      cmd.Parameters.Add(p_line);

      cmd.Parameters.Add(p_status);

      cmd.ExecuteNonQuery();

 

      // write location, return status value, and the text to the console window

      Console.WriteLine("In method GetSingleLine...");

      Console.WriteLine("Return status: {0}", p_status.Value.ToString());

      Console.WriteLine("Return text: {0}", p_line.Value.ToString());

      Console.WriteLine();

      Console.WriteLine();

 

      // clean up

      p_line.Dispose();

      p_status.Dispose();

      cmd.Dispose();

    }

 

    static void GetMultipleLines(OracleConnection con)

    {

      // write location to console window

      Console.WriteLine("In method GetMultipleLines...");

      Console.WriteLine();

 

      // call the procedure that emits multiple lines of text

      string stored_procedure = "dbms_output_test.emit_multiple_lines";

 

      // anonymous pl/sql block to get multiples lines of text per fetch

      string anonymous_block = "begin dbms_output.get_lines(:1, :2); end;";

 

      // used to indicate number of lines to get during each fetch

      const int NUM_TO_FETCH = 8;

 

      // used to determine number of rows fetched in anonymous pl/sql block

      int numLinesFetched = 0;

 

      // simple loop counter used below

      int i = 0;

 

      // create command and execute the stored procedure

      OracleCommand cmd = con.CreateCommand();

      cmd.CommandText = stored_procedure;

      cmd.CommandType = CommandType.StoredProcedure;

      cmd.ExecuteNonQuery();

 

      // create parameter objects for the anonymous pl/sql block

      OracleParameter p_lines = new OracleParameter("",

                                                    OracleDbType.Varchar2,

                                                    NUM_TO_FETCH,

                                                    "",

                                                    ParameterDirection.Output);

 

      p_lines.CollectionType = OracleCollectionType.PLSQLAssociativeArray;

      p_lines.ArrayBindSize = new int[NUM_TO_FETCH];

 

      // set the bind size value for each element

      for (i = 0; i < NUM_TO_FETCH; i++)

      {

        p_lines.ArrayBindSize[i] = 32000;

      }

 

      // this is an input output parameter...

      // on input it holds the number of lines requested to be fetched from the buffer

      // on output it holds the number of lines actually fetched from the buffer

      OracleParameter p_numlines = new OracleParameter("",

                                                      OracleDbType.Decimal,

                                                      "",

                                                      ParameterDirection.InputOutput);

 

      // set the number of lines to fetch

      p_numlines.Value = NUM_TO_FETCH;

 

      // set up command object and execute anonymous pl/sql block

      cmd.CommandText = anonymous_block;

      cmd.CommandType = CommandType.Text;

      cmd.Parameters.Add(p_lines);

      cmd.Parameters.Add(p_numlines);

      cmd.ExecuteNonQuery();

 

      // get the number of lines that were fetched (0 = no more lines in buffer)

      numLinesFetched = ((OracleDecimal)p_numlines.Value).ToInt32();

 

      // as long as lines were fetched from the buffer...

      while (numLinesFetched > 0)

      {

        // write the text returned for each element in the pl/sql

        // associative array to the console window

        for (i = 0; i < numLinesFetched; i++)

        {

          Console.WriteLine((p_lines.Value as OracleString[])[i]);

        }

 

        // re-execute the command to fetch more lines (if any remain)

        cmd.ExecuteNonQuery();

 

        // get the number of lines that were fetched (0 = no more lines in buffer)

        numLinesFetched = ((OracleDecimal)p_numlines.Value).ToInt32();

      }

 

      // just a couple of separator lines

      Console.WriteLine();

      Console.WriteLine();

 

      // clean up

      p_numlines.Dispose();

      p_lines.Dispose();

      cmd.Dispose();

    }

  }

}



Executing the sample produces the following output in the console window:

In method GetSingleLine...

Return status: 0

Return text: This is a simple line of text emitted by dbms_output.

 

 

In method GetMultipleLines...

 

Sgt. Pepper's Lonely Hearts Club Band Track List

================================================

01 - Sgt. Pepper's Lonely Hearts Club Band

02 - With a Little Help From My Friends

03 - Lucy in the Sky With Diamonds

04 - Getting Better

05 - Fixing a Hole

06 - She's Leaving Home

07 - Being for the Benefit of Mr. Kite!

08 - Within You Without You

09 - When I'm Sixty-Four

10 - Lovely Rita

11 - Good Morning Good Morning

12 - Sgt. Pepper's Lonely Hearts Club Band (Reprise)

13 - A Day in the Life

 

 

ENTER to continue...



Happy coding...

21 April 2007

ODP.NET Tip: Anonymous PL/SQL and CommandType

When using an Anonymous PL/SQL block in your ODP.NET application code, one "gotcha" that may arise is setting the CommandType property of an OracleCommand object to an incorrect value. You may be tempted to set the value to CommandType.StoredProcedure. After all, we are talking about PL/SQL, and PL/SQL code means a stored procedure, function, package, etc. of some kind, right? Well, not exactly...

For details on PL/SQL and Anonymous Blocks, I suggest reviewing Chapter 1: Overview of PL/SQL in the Oracle® Database PL/SQL User's Guide and Reference 10g Release 2 (10.2) available for free on the Oracle Technology Network website. In a nutshell, an Anonymous PL/SQL block is a block of PL/SQL code that is not stored in the database, rather, it is created (or generated) in a client application and submitted to the database-side PL/SQL engine. As such, it is a block of text and not stored code. What is the appropriate CommandType to use when submitting a block of text to the database? CommandType.Text of course!

What happens when you accidentally do set CommandType to CommandType.StoredProcedure? You will see an ugly and somewhat confusing error message similar to:

ORA-06550: line 1, column 246:
PLS-00103: Encountered the symbol "(" when expecting one of the following:

begin case declare end exception exit for goto if loop mod
null pragma raise return select update while with
<an identifier> <a double-quoted delimited-identifier>
<a bind variable> << close current delete fetch lock insert
open rollback savepoint set sql execute commit forall merge
pipe
The symbol "case" was substituted for "(" to continue.
ORA-06550: line 1, column 261:
PLS-00103: Encountered the symbol ";" when expecting one of the following:

* & = - + < / > at in is mod remainder not rem when
<an exponent (**)> <> or != or ~= >= <= <> and or like LIKE2_
LIKE4_ LIKEC_ between overlaps mult


If you wish to see this in action, simply uncomment the following line of code in the sample application code below:

// cmd.CommandType = CommandType.StoredProcedure;

The sample application code uses an Anonymous PL/SQL block to perform a "select into" operation. This code does not use typical objects such as a data reader, etc. Instead, it uses parameter objects in the ODP.NET application code that correspond to the bind variables used in the Anonymous PL/SQL block.


The Sample C# Code:


using System;

using System.Data;

using Oracle.DataAccess.Client;

using Oracle.DataAccess.Types;

 

namespace SelectIntoTest

{

  class Program

  {

    // connection string using the "hr" sample user/schema

    // be sure to modify as necessary for your environment

    const string constr = "User Id=hr;" +

                          "Password=hr;" +

                          "Data Source=otndemo;" +

                          "Enlist=false;" +

                          "Pooling=false";

 

    // anonymous pl/sql to perform a select into

    // bind variables are used for the "output values"

    // as well as in the "country_id" in the "where clause"

    const string anonymous_block = "begin " +

                                  "  select  a.country_name, " +

                                  "          b.region_name " +

                                  "  into    :p_country_name, " +

                                  "          :p_region_name " +

                                  "  from    countries a, " +

                                  "          regions b " +

                                  "  where    a.country_id = :p_country_id " +

                                  "  and      a.region_id = b.region_id; " +

                                  "end;";

 

    static void Main(string[] args)

    {

      // create and open the connection

      OracleConnection con = new OracleConnection(constr);

      con.Open();

 

      // create parameter objects for the bind variables in the anonymous pl/sql block

      OracleParameter p_country_name = new OracleParameter("p_country_name",

                                                          OracleDbType.Varchar2,

                                                          40,

                                                          "",

                                                          ParameterDirection.Output);

 

      OracleParameter p_region_name = new OracleParameter("p_region_name",

                                                          OracleDbType.Varchar2,

                                                          25,

                                                          "",

                                                          ParameterDirection.Output);

 

      OracleParameter p_country_id = new OracleParameter("p_country_id",

                                                        OracleDbType.Varchar2,

                                                        "UK",

                                                        ParameterDirection.Input);

 

      // create command object and set CommandText

      // CommandType defaults to Text which is correct

      // for an anonymous pl/sql block

      OracleCommand cmd = con.CreateCommand();

      cmd.CommandText = anonymous_block;

 

      // it is incorrect to set CommandType = CommandType.StoredProcedure

      // cmd.CommandType = CommandType.StoredProcedure;

 

      // add the parameters to the collection for the command object

      cmd.Parameters.Add(p_country_name);

      cmd.Parameters.Add(p_region_name);

      cmd.Parameters.Add(p_country_id);

 

      // execute the anonymous pl/sql block

      cmd.ExecuteNonQuery();

 

      // write the values to the console window

      Console.WriteLine("Country Id '{0}' is country '{1}' located in region '{2}'\n",

                        p_country_id.Value.ToString(),

                        p_country_name.Value.ToString(),

                        p_region_name.Value.ToString());

 

      // keep the console window from automatically closing

      // such as when running from Visual Studio environment

      Console.WriteLine("ENTER to continue...");

      Console.ReadLine();

 

      // clean up

      p_country_id.Dispose();

      p_region_name.Dispose();

      p_country_name.Dispose();

      cmd.Dispose();

      con.Dispose();

    }

  }

}



Sample Application Output:



Country Id 'UK' is country 'United Kingdom' located in region 'Europe'


ENTER to continue...

21 March 2007

ODP.NET Tip: Exercise Explicit Transaction Control

The Oracle Data Provider for .NET (ODP.NET), by default, operates in what is known as "auto-commit" mode. What this means is that when a non-query statement such as an insert, an update, or a delete has been processed, a "hidden commit" is automatically and implicitly performed in the database on your behalf. When processing a single row insert statement, for example, this may well be acceptable. However, what if, rather than that single row, you needed to insert several thousand or several hundred thousand (or more!) rows into a table? In pseudo-code, using the default behaviour, this would look something like the following:


Begin loop
Insert row
Commit
If more rows continue loop
End loop


The key point in this pseudo-code is that the implicit "commit" is inside the loop and, thus, will be transparently invoked as many times as the loop executes.


In contrast to the "commit each pass through the loop" approach, a guiding practice in regard to Oracle database transactions is to commit when the transaction is complete. In this example, the transaction is not deemed to be complete until all rows have been inserted into the table. In pseudo-code, again, this would resemble:


Begin loop
Insert row
If more rows continue loop
End loop
Commit


Here the "commit" has been moved outside the main processing loop. While the difference may seem subtle and/or trivial, there can be a large performance difference between the two approaches. The TransactionDemo solution illustrates both of these approaches. The demo code contains two "test" methods: one method inserts 5,000 rows into a table ("demo_table") using a commit after each insert (simply employing the default behaviour) while the second method explicitly creates a transaction and only commits that transaction once all rows have been inserted into the table.


To explicitly create, and thus begin, a transaction, you use the "BeginTransaction" method exposed by an OracleConnection object (where "con" is an OracleConnection object):


OracleTransaction txn = con.BeginTransaction();


When you are ready to commit the transaction, you invoke the "Commit" method on the transaction object:


txn.Commit();


Note: You may also invoke the "Rollback" method if necessary.

The following table illustrates the results of executing the demo code five times:


Test/Commit Default Explicit
1 11.4688 1.2188
2 11.1406 1.3906
3 11.6406 1.2031
4 11.2500 1.2031
5 12.1094 1.2031
Min 11.1406 1.2031
Avg 11.5219 1.2437
Max 12.1094 1.3906


Table 1: The results, in seconds, of TransactionDemo over 5 executions



As you can see from the results, committing at the end of the transaction, rather than after each row is inserted, allows for code that executes much faster. The average time for the method that commits after each row is inserted is just over 11.5 seconds, while the average time for the method that performs a single commit at the end of the transaction is just over 1.2 seconds - almost a factor of 10 difference!


In order to run the sample code below unchanged, you need to first perform the following tasks:

- Create a "tns alias" called "orademo" in your tnsnames.ora file to connect to the appropriate database in your environment.

- Create a user called "orademo" in the database.

- Grant the "create session" and "create table" privileges to the "orademo" user.

- Create a table called "demo_table" in the "orademo" user's schema.


Create the ORADEMO User


As a DBA user, perform the following tasks in your database to create the user and grant privileges (adjust as needed for your environment):


create user orademo identified by demo
default tablespace users
temporary tablespace temp
quota unlimited on users;


grant create session to orademo;
grant create table to orademo;


Create the Demo Table


Once you have created the "orademo" user, connect to the database as "orademo" and execute the following to create the table:


create table demo_table
(
c1 varchar2(32),
c2 varchar2(32),
c3 varchar2(32)
);



The Sample C# Code:




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


namespace TransactionDemo
{
class Program
{
// be sure to adjust the connection string for your environment
private const string constr = "User Id=orademo; Password=demo; Data Source=orademo; Enlist=false; Pooling=false";


static void Main(string[] args)
{
Console.WriteLine("Beginning TransactionDemo...\n");


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


// start with empty table
delete_table(con);


// perform insert with commit after each row
multi_commit(con);


// empty the table
delete_table(con);


// perform insert with a single commit at end
single_commit(con);


// clean up
con.Dispose();


// prevent command window from closing automatically when running from visual studio
Console.WriteLine();
Console.WriteLine("ENTER to continue...");
Console.ReadLine();
}


static void multi_commit(OracleConnection con)
{
Console.WriteLine("Starting multi-commit test...\n");


// used to track execution duration
DateTime timeStart;
DateTime timeEnd;
double totalSeconds;


// create command to insert rows into demo table
string sql = "insert into demo_table (c1, c2, c3) values ('This is column 1', 'This is column 2', 'This is column 3')";
OracleCommand cmd = con.CreateCommand();
cmd.CommandText = sql;


// capture test start time
timeStart = DateTime.Now;


// an implicit commit will occur for each ExecutionNonQuery call
for (int i = 0; i < 5000; i++)
{
cmd.ExecuteNonQuery();
}


// capture test end time
timeEnd = DateTime.Now;


// calculate total seconds for this test
totalSeconds = timeEnd.Subtract(timeStart).TotalSeconds;


// display time used for test
Console.WriteLine(" Multi commit: {0,7:##.0000} total seconds.\n", totalSeconds);


// clean up
cmd.Dispose();
}


static void single_commit(OracleConnection con)
{
Console.WriteLine("Starting single-commit test...\n");


// used to track execution duration
DateTime timeStart;
DateTime timeEnd;
double totalSeconds;


// create command to insert rows into demo table
string sql = "insert into demo_table (c1, c2, c3) values ('This is column 1', 'This is column 2', 'This is column 3')";
OracleCommand cmd = con.CreateCommand();
cmd.CommandText = sql;


// capture test start time
timeStart = DateTime.Now;


// create an explicit transaction
OracleTransaction txn = con.BeginTransaction();


// explicit transaction started, therefore no implicit commit
// for each ExecuteNonQuery call
for (int i = 0; i < 5000; i++)
{
cmd.ExecuteNonQuery();
}


// commit the transaction
txn.Commit();


// capture test end time
timeEnd = DateTime.Now;


// calculate total seconds for this test
totalSeconds = timeEnd.Subtract(timeStart).TotalSeconds;


// display time used for test
Console.WriteLine("Single commit: {0,7:##.0000} total seconds.\n", totalSeconds);


// clean up
cmd.Dispose();
}


static void delete_table(OracleConnection con)
{
Console.WriteLine("Deleting from demo_table...\n");


// delete all rows from the table
OracleCommand cmd = con.CreateCommand();
cmd.CommandText = "delete from demo_table";
cmd.ExecuteNonQuery();
cmd.Dispose();
}
}


Note: This content is based on my HotSos Symposium 2007 paper.

HotSos Symposium 2007 Materials

The paper and all sample code from my HotSos Symposium 2007 presentation entitled "Oracle Data Provider for .NET (ODP.NET) Performance Tips and Techniques" is available as a single .zip file. To download the .zip file, click here.

16 February 2007

Oracle Error ORA-1578: RMAN to the Rescue

If you are familiar with Oracle error messages, you will recognise that the true error message text for the ORA-1578 error is not, in fact, "RMAN to the Rescue". If you look this error up in the Oracle Database Error Messages manual, you'll find the text is:

ORA-01578: ORACLE data block corrupted (file # string, block # string)
Cause: The data block indicated was corrupted, mostly due to software errors.

Action: Try to restore the segment containing the block indicated. This may involve dropping the segment and recreating it. If there is a trace file, report the errors in it to your ORACLE representative.

Uh-oh. This sounds like it could be rather bad news indeed. In particular, the statement "This may involve dropping the segment and recreating it." does not have an especially appealing quality to it when the segment in question is, say, a several hundred million row table, just as an example. It is a time like this that RMAN really can come to the rescue.

"But, we don't use RMAN to back up our database. We only perform user-managed backups." I can hear you say. Many people are surprised to learn that even if you don't use RMAN to back up your database (even though you really should!) it can still be used to perform what is known as block media recovery. It is just this situation that I will illustrate here - performing block media recovery on a corrupt data block (ORA-1578) using RMAN but without having an RMAN backup.

Before embarking on this adventure, I think it wise to create a new tablespace, and, thus, a datafile dedicated to this purpose. I will, naturally, be using SQL*Plus for this activity:


sqlplus "/ as sysdba"

create tablespace test logging datafile 'D:\ORACLE_DB\LT10GR2\TEST01.DBF' size 1M;

create table ctest (c varchar2(1)) tablespace test;

insert into ctest values ('a');

commit;


So, now I have a test tablespace and a table.

Since I am going to show how to perform block media recovery using a non-RMAN backup, I had better back up the data file! In order to do that, I simply shut down the database:


shutdown immediate;


Then I back up the file in a Command Prompt window by copying it to a directory called "backup":


D:\oracle_db\lt10gr2>copy TEST01.DBF backup\TEST01.DBF
1 file(s) copied.


NOTE: If your database is in ARCHIVELOG mode, you do not need to shut it down before copying the data file; however, remember to issue an “alter tablespace TEST begin backup;” before copying the data file and an “alter tablespace TEST end backup;” after the file has been copied. As this database is a “sandbox” database on my laptop, it runs in NOARCHIVELOG mode most of the time.


At this time some weird magic happened and a block belonging to the table has become corrupt. It's funny how these things can happen.


Time to start the database back up and see if the block is corrupted from Oracle’s perspective.

Back in my SQLPLUS session:


startup open;


The database has successfully started back up. Now I try to perform a select against the table:


select * from ctest;

ERROR at line 1:
ORA-01578: ORACLE data block corrupted (file # 6, block # 12)
ORA-01110: data file 6: 'D:\ORACLE_DB\LT10GR2\TEST01.DBF'


Hey! There’s a familiar-looking error message.

OK, so the data block belonging to my CTEST table is definitely corrupt. I’d say it is time for that RMAN magic!

Because I did not use RMAN to perform any backup operations on this database – the “backup” was simply copying a single datafile to another directory – I do not have any RMAN Recovery Catalogue. Therefore I specify the “nocatalog” option when I start RMAN:


rman target=/ nocatalog


After connecting to the database with RMAN, I need to tell RMAN about the backup file which is also known as the “datafilecopy” in RMAN terms. I refer to this as “registering” the file with RMAN. Here’s how I do that:


catalog datafilecopy 'D:\ORACLE_DB\LT10GR2\BACKUP\TEST01.DBF';


RMAN responds with something similar to:


cataloged datafile copy
datafile copy filename=D:\ORACLE_DB\LT10GR2\BACKUP\TEST01.DBF recid=2 stamp=614641757


RMAN is now “aware” of this file and can use it to perform the block media recovery. Also note that all of the redo that was generated from the time I copied the file up to the present time is also available on my system. To recover the block simply enter:


blockrecover datafile 6 block 12;


RMAN responds with text similar to the following:


Starting blockrecover at 15-FEB-07
allocated channel: ORA_DISK_1
channel ORA_DISK_1: sid=148 devtype=DISK

channel ORA_DISK_1: restoring block(s) from datafile copy D:\ORACLE_DB\LT10GR2\BACKUP\TEST01.DBF

starting media recovery
media recovery complete, elapsed time: 00:00:03

Finished blockrecover at 15-FEB-07


Now that RMAN has performed the block media recovery, I verify if I can access the information in the block that I previously corrupted by issuing the following statement in SQLPLUS:


SQL> select * from ctest;

C
-
a

1 row selected.


How about that? Having never taken an RMAN backup of the database, I was able to use RMAN to completely recover a corrupted block in my table!

Having successfully recovered the data block, I remove the datafilecopy registration in RMAN like so:


change datafilecopy 'D:\ORACLE_DB\LT10GR2\BACKUP\TEST01.DBF' uncatalog;


You may also use the “delete” command if you wish to both remove the registration and delete the physical file in a single command.

One final reminder: the purpose of this was to demonstrate an RMAN capability. If you encounter a corrupt data block in your database and need assistance, please ensure you contact Oracle Support for the proper procedures to recover. Remember this is for demonstration purposes only. Do not perform this on a production server!