To execute a query we have to create an Environment object, that we use to create a Connection, that we use to create a Statement, that we (finally) could run to get a ResultSet back.
After that we should clean it up. So we should close the ResultSet from the Statement, then terminate the Statement from the Connection, terminate the Connection from the Environment, and (happy ending) terminate the Environment.
In our simple example there is no reason to complain. The code is reasonably straightforward, and looks easy to understand. Still, trouble is lurking from these lines. What happens if an exception breaks the natural execution flow? And what if future changes make the code less immediate? Maybe some part of the code could try to do silly things, not respecting the implicit contract that we should maintain to write OCCI code.
To make our code safer, we should make more explicit our requirements, using language constructs.
Let's think to the Environment object. It shouldn't be copied - maybe it could be moved, but we should do it carefully - it should be the first object in the OCCI chain to be created, and the last one to be deleted. We could verbosely comment our code to make it clear to the reader, but I have a sort of suspect that this is not a strategy that pays off.
Better using a smart pointer, and since I am writing this code explicitly for VC++ 2010, we can take advantage of its implementation for std::unique_ptr, that fits exactly to the requirements of our problem.
So, the initial situation is that we have a piece of code that looks like this:
try { // setup oc::Environment* env = oc::Environment::createEnvironment(); // 1 // ... // execution // ... while(res->next()) std::cout << res->getString(1) << ' ' << res->getString(2) << ' ' << res->getInt(3) << std::endl; // cleanup // ... oc::Environment::terminateEnvironment(env); // 2 } catch(const oc::SQLException& e) { // ...And we want apply a couple of changes on it:
1. Instead of a raw pointer we want to deal with an object on the stack, so that we could leave to the compiler (through the stack unwinding mechanism) the nuisance of taking care of its correct destruction, even in case of exceptions.
2. As direct consequence of (1), no explicit cleanup should be performed for our Environment object.
This is the code we want to get instead:
try { OcciEnvironment sEnv = OcciWrapper::createEnvironment(); // ... // ... while(sRes->next()) std::cout << sRes->getString(1) << ' ' << sRes->getString(2) << ' ' << sRes->getInt(3) << std::endl; // ... } catch(const oc::SQLException& e) { // ...At the end of the day, when all the OCCI raw pointers are wrapped in smart pointers, there would be no more need for a cleanup section, and the code would be more robust.
But, as usual, there is no free lunch, we just moved the complexity of the issue elsewhere. And if large part of it would stay hidden in the standard library implementation, some of it will sneak in this piece of code that we are about to put in an include file available to our source:
#pragma once #include <functional> #include <occi.h> namespace oc = oracle::occi; typedef std::unique_ptr<oc::Environment, std::function<void(oc::Environment*)> > OcciEnvironment; // 1 namespace OcciWrapper { OcciEnvironment createEnvironment() // 2 { oc::Environment* env = oc::Environment::createEnvironment(); // 3 return OcciEnvironment(env, std::bind(oc::Environment::terminateEnvironment, env)); // 4 } }1. This is not so terrible, after all. Instead of using a raw pointer to an oracle::occi::Environment object, we plan to use a smart pointer, but since its definition is a bit complex, I think it is better to typedef it to a more friendly name.
The std::unique_ptr is a scoped limited smart pointer that can't be copied (but could be moved) and that lets its user a way to define what should be called by its dtor. But when you want to use this feature we have to specify in the class template the type of the deleter. So we say to the compiler that we want to be able to pass a function that returns void and requires in input a raw pointer to Environment.
2. Actually, we should write a createEnvironment() overload for each oracle::occi::createEnvironment() we plan to use in our code. Let's assume that currently we could do with just this one.
3. Firstly we create a raw pointer, and then we use it to create its smart big brother.
4. Time to be smart. The raw pointer is wrapped in an unique_ptr, and the Environment cleanup function is passed as the deleter associated to this object. Notice that we had to use std::bind to let it know what it should use as parameter.
Informative article. After reading the complete post I must say that you do posses a great knowledge about this technology which can do wonders. The code that you have posted above is straightforward and I like your approach that you have implemented in the above example.
ReplyDeletesap testing
Thank you, Tee. I happy you enjoyed reading this post.
DeleteIMHO OTL library ( otl.sourceforge.net ) is better and easier to use than OCCI. OCCI is too java-like. OTL is more C++-like...
ReplyDeleteThanks for the hint, I'll have a look at it.
Delete