OraLib - a lightweight C++ wrapper over Oracle's OCI library
 TOC 


OraLib, An Article 

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 release method and it is expected method to be called when object instance is no longer needed by the user code. There are cases, where release body is empty, but this could change in the future. release method in not called on all of the code examples below.

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

In order to run 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, Oracle 8.1.6 and up. I suppose it is possible to run the library on Linux/Unix, but I do not have the knowedge (nor patience and time) to do it yet.

Version 0.0.4 supports Borland C++ 5.5 command-line tools and MinGW 1.1.

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

Following topics will present you various use-cases for OraLib.

Connection handling 

There are two ways to connect to an Oracle server: specify server name, login and password as a parameters to connection object's constructor or by creating a blank object instance and calling 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 ();

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

Executing commands 

Data Definition Language (DDL) 

Execution of DDL commands is the simpliest case. connection object's execute method could be called explicitly with 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 SQL insert statement. A more complicated case is by calling a stored procedure, but generally there are two cases: (1) data to be stored could be inside SQL statement - as text - and, (2) data to be stored could be passed via bound variables (or parameters). Both approaches have advantages and disadvantages. In first case you should build a text string containing SQL insert statement (or stored procedure name) along with fully formatted data values - for example by calling printf. Second approach requires 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 ();

Second approach is the better one, because: (1) 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 ();

(2) 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 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) ot 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.

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 concider that 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 a 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"];

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 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 SQL select statement is executed and 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 powerful features of 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). Following example will execute two SQL select statements and will 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 ();
	...

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

Use-case examples 

Insert 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, "select max (id) from a_table"-way is definately wrong. But actually retrieving 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 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 ();

Alternatives 

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

OCI will took you 50 lines of formatted code to start with in order 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 - for a list visit my web site.

Some final words 

Hope you like it. Comments, feedback and requests are welcome.

Disclaimer 

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

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