A Lightweight C++ Wrapper over Oracle's OCI Library - OraLib

Environment: VC6 SP4, STLport 4.0.x or 4.5.x, Oracle 8.x or 9

In This Article

  1. Introduction
  2. Connection Handling
  3. Executing Commands
    1. Data definition language (DDL)
    2. Storing data
    3. Transactions
    4. Retrieving data
    5. Executing PL/SQL blocks
  4. Use-Case Examples
    1. Inserting table row and retrieve sequence value
    2. Retrieving a particular table row
    3. Calling a stored procedure
    4. Calling a function in a package
  5. Alternatives
  6. Some Final Words
  7. Disclaimer

Introduction

This page will present some basic use-cases for OraLib—a lightweight C++ wrapper over Oracle's OCI library. My intention was to code an easy-to-use, yet feature-full, C++ library.

A note about resource ownership: Almost all classes have a release method and it is expected that this method will be called when the object instance is no longer needed by the user code. There are cases where the release body is empty, but this could change in the future. The release method in not called on any of the code examples below.

Pre-requisites: basic C++, SQL, and PL/SQL knowedge.

In order to run the samples you will also need Microsoft OS (Windows 9x, NT, or 2000), Microsoft Visual C++ compiler 6.0, STLport 4.0.x or 4.5.x, and Oracle 8.1.6 and up. I suppose it is possible to run the library on Linux/Unix, but I do not have the knowledge (or patience and time) to do it yet.

What is OraLib? OraLib is a C++ library, wrapped over Oracle's OCI library. OraLib saves you low-level function (API) calls and low-level knowledge you should otherwise posess. With OraLib you can easily do the following:

  • connect/disconnect an Oracle server;
  • execute DDL commands;
  • execute SQL select/insert/update/delete commands;
  • work with transactions;
  • call packages/stored procedures or execute anonymous PL/SQL blocks;
  • work with (named) bound variables;
  • use built-in error handling by using C++ exceptions.

The following topics will present you various use-cases for OraLib.

Back to top

Connection Handling

There are two ways to connect to an Oracle server: specify server name, login, and password as parameters to the connection object's constructor or by creating a blank object instance and calling the open method later. In either case, an error will be thrown if connect fails.

  oralib::connection cn ("MyOracleServer", 
                         "MyLogin", 
                         "MyPassword");
  ...
  cn.close ();

    // or

  oralib::connection cn ();
  cn.open ("MyOracleServer", "MyLogin", "MyPassword");
  ...
  cn.close ();

The connection could be explicitly closed by calling the close method or implicitly, the when object is deleted or goes out of scope. In the first case, the object instance could be reused to connect to the same or another Oracle instance at later time.

Back to top

Executing Commands

Data definition language (DDL)

Execution of DDL commands is the simpliest case. A connection object's execute method could be called explicitly with an SQL string to be executed.

cn.execute ("create table a (id numeric, name varchar2 (10))");
...
cn.execute ("drop table a");

Storing data

The easiest way to store data in an Oracle database is to use the SQL insert statement. A more complicated case is by calling a stored procedure, but generally there are two cases:

  • Data to be stored could be inside SQL statement—as text.
  • Data to be stored could be passed via bound variables (or parameters).

Both approaches have advantages and disadvantages. In the first case you should build a text string containing the SQL insert statement (or stored procedure name) along with fully formatted data values—for example, by calling printf. The second approach requires an SQL insert statement to include bound variables names only (or it will always be a constant) and to manually bind named variables and set their values without worying about formatting. Here is an example for both:

    // common for both
  long id_column;
  TCHAR *name_column;

    // 1st approach

  char sql [100];
  sprintf (sql, "insert into a (id, name) values (%d, '%s')",
    id_column, name_column);
  cn.execute (sql);

    // 2nd approach

  statement &st = *cn.prepare ("insert into a values (:n, :s)");
  st.bind (":n") = id_column;
  st.bind (":s") = name_column;
  st.execute ();
  st.release ();

The second approach is the better one, because:

  • Unicode text data could be used—OCI expects SQL statements to be in an ANSI string and Unicode text doesn't fit in ANSI character set;
  • wchar_t *name_in_unicode; // initialized somewhere below...
    ...
    statement &st =
            cn.prepare ("insert into a (name) values (:s)");
    st.bind (":s") = name_in_unicode;
    st.execute ();
    
  • Multiple SQL insert statements (where only insert values are different) could be executed sequentially (this is also much faster compared to the 1st approach).
  • statement &st = 
            *cn.prepare ("insert into a (id) values (:n)");
    parameter &p = st.bind (":n");
    for (long i=0; i<1000; i++)
    {
      p = i;
      st.execute ();
    }
    

Transactions

Normally in Oracle, the first data-related SQL statement creates an implicit transaction. For example, "insert into a (id) values (1)" creates a transaction that should be explicitly closed (commited or rolled-back) or it will be closed implicitly when the connection is closed. Until the transaction is closed, the change made is visible only from within the very same connection and in some cases other connections could be blocked.

The connection object provides two methods for transactions handling: commit and rollback. If you read the source code, you will find out that both are nothing more than simple calls to connection.execute. Anyway, you should consider that the transaction should be closed ASAP, because a contention could occur—either by calling one of connection.commit / connection.rollback or by including commit / rollback in your stored procedures.

Retrieving data

There are two options when data should be retrieved. The choice depends on how much data you wish to retrieve. When required data is a flag or count, for example, named variables could be used. But if you need to fetch rows of data you should use cursors (resultsets).

The usage of named variables for data retrieval is similar to their use for storing of data:

  statement &st = *cn.prepare (
    "begin select count (id) into :n from a; end;");
  st.bind (":n");
  st.execute ();
  num_rows = st [":n"];

This approach is suitable for cases where you wish to use a same-named variable for both input and output.

To fetch data from an explicit SQL select statement, call either connection.select or statement.select, depending on whether you need to supply some input data (select condition for example).

    // connection.select case

  resultset &rs = *cn.select ("select name from a");
  if (!rs.eod ())
    do
      cout << (Pstr) rs ["NAME"] << '\n';
    while (++rs);
  rs.release ();

    // statement.select case

  statement &st = *cn.prepare
      ("select id, name from a where id = :n");
  st.bind (":n") = id_required;
  resultset &rs1 = *st.select ();
  cout << '#' << (long) rs1 ["ID"] << ' '
      << rs1 [2].as_string ();
  rs1.release ();

When a SQL select statement is executed and a resultset object is returned, columns could be accessed in two ways: (1) by name (case sensitive) and (2) by index (whether index is 0- or 1-based is configured in oralib.h).

If you need to execute more then one SQL select statement, then, cursor-bound variables should be used (described in the following section).

Executing PL/SQL blocks

One of the powerful features of an Oracle database is PL/SQL. By using OraLib, you can execute PL/SQL blocks, pass input parameters, and receive output parameters. Output parameters can even be resultsets (cursor in Oracle docs). The following example will execute two SQL select statements and fetch rows by using cursor-named variables:

  statement &st = *cn.prepare (
    "begin\n"
    " open :c1 for select id, name from a;\n"
    " open :c2 for select * from a;\n"
    "end;");
  st.bind (":c1");
  st.bind (":c2");
  st.execute ();

  resultset &rs = st [":c1"]; // id and name columns
  column &id_column = st [":c1"].as_resultset () ["ID"];
  column &name_column = rs ["NAME"];
  if (!rs.eod ())
    do
      cout << '#' << (long) id_column << ' '
        << (Pstr) name_column << '\n';
  while (++rs);
  name_column.release ();
  id_column.release ();
  rs.release ();
  ...

The resultset columns could be accessed by asking the resultset every time we need the column's value or by caching it in a column object. The second approach is faster, of course, but since late binding is being used, the statement should be executed first.

Back to top

Use-Case Examples

Inserting table row and retrieve sequence value

Oracle uses the concept of sequences to allow simultaneous inserts in a single table (Microsoft SQL Server uses autonumber columns). Because almost every modern system is used by more than one user at a time, the "select max (id) from a_table"-way is definately wrong. But actually retrieving a newly created row's id column is easy:

  statement &st = *cn.prepare (
    "begin\n"
    " insert into a (id, name) values (a_seq.nextval, :s);\n"
    " :n := a_seq.currval;\n"
    " commit;\n"
    "end;");
  st.bind (":s") = name_column;
  st.bind (":n");
  st.execute ();
  cout << "newly created row's id = " << (long) st [":n"];
  st.release ();

Of course, this should be placed into a stored procedure.

Retrieving a particular table row

  statement &st = *cn.prepare (
    "select col1, col2, col3 from table_name where id = :n");
  st.bind (":n") = id_we_re_looking_for;
  resultset &rs = *st.select ();
  ...
  rs.release ();
  st.release ();

Calling a stored procedure

  statement &st = *cn.prepare (
    "begin sp_name (:param1, :param2, :param3); end;");
  st.bind (":param1", DT_TYPE) = param1_value;
  st.bind (":param2", DT_TYPE) = param2_value;
  st.bind (":param3", DT_TYPE) = param3_value;
  st.execute ();
  ...
  st.release ();

Calling a function in a package

  statement &st = *cn.prepare (
    "begin :result := package_name.function_name ("
  ":param1, :param2, :param3); end;");
  st.bind (":param1", DT_TYPE) = param1_value;
  st.bind (":param2", DT_TYPE) = param2_value;
  st.bind (":param3", DT_TYPE) = param3_value;
  st.bind (":result", DT_TYPE);
  st.execute ();
    // use st [":result"] here
  ...
  st.release ();

Back to top

Alternatives

OraLib includes only a few classes (six to be exact), but it supports lots of powerful features provided by the OCI library: named variables, output cursors, PL/SQL blocks, and execution. The library is distributed with full source code.

OCI will take you 50 lines of formatted code to start with to connect an Oracle server, not to mention "simple" things, like executing a select statement and fetching the result or binding a named variable. OO4O for C++ is simply a COM wrapper. OCCI comes with the newer Oracle versions (9 and up), but it looks like that source code is unavailable.

There are other similar projects available—go here for a bigger list.

Back to top

Some Final Words

I hope you like it. Comments, feedback, and requests are welcome.

Back to top

Disclaimer

This software comes with no warranty. Use it at your own risk.

Include the library name and my e-mail in your projects. Notify me.

Back to top

Downloads

Download source - 32Kb

The latest source code and some docs are also available here.