I'll continue right from where I left off in my previous entry which ended with two utilities:
template<typename Func,typename Tuple>applyWithIndex(Func f,Tuple&&t); template<typename ... Args>std::ostream&operator<<(std::ostream&os,std::tuple<Args ...>const&t);
The first function passes a tuple element index together with an element to F. The second one is a simple utility which prints each tuple element together with the type of the element. Because F must be able to take any type as the second parameter (i.e., the tuple element), it should be implemented as a struct or class with a template call operator.
Before attacking the design of an input iterator iterating through a collection of rows specified by a select statement I need two more utilities. The first one is a fetcher capable copying data from an OCCI result set into a tuple. The second utility is a binder which I will use to set bind variable in an OCCI statement. Both utilities are implemented along the same principles.
The fetcher is relatively simple to implement given that I already have a function - applyWithIndex - which passes each element of a tuple together with its index to a callable object. The class I'll use to fetch data from an OCCI result set looks like this:
class DataFetcher{ public: DataFetcher(std::shared_ptr<oracle::occi::ResultSet>rs):rs_(rs){} DataFetcher()=default; DataFetcher(DataFetcher const&)=default; DataFetcher(DataFetcher&&)=default; DataFetcher&operator=(DataFetcher const&)=default; DataFetcher&operator=(DataFetcher&&)=default; ~DataFetcher()=default; std::shared_ptr<oracle::occi::ResultSet>getResultSet()const{return rs_;} bool operator==(DataFetcher const&other)const{return rs_==other.rs_;} bool operator==(DataFetcher&&other)const{return rs_==other.rs_;} template<typename T> void operator()(int ind,T&t)const{ DataFetcherAux<T>::fetch(ind,rs_,t); } private: std::shared_ptr<oracle::occi::ResultSet>rs_; };
The key is the call operator which is a template taking an index together with a tuple element as parameters. Ones inside the call operator I know the type of the tuple element and can call the helper function - DataFetcherAux - which will do the job of actually getting data from the result set into the tuple element.
Whats left to do now is to write a set of functions which get the data from a result set and stores it in a tuple element. Here I'll need one function for each type since the OCCI getter functions have type dependent names. Here is the implementation of two of the functions:
// fetch data from a result set and store in a variable (one struct for each type) template<typename T>struct DataFetcherAux; template<>struct DataFetcherAux<int>{ static void fetch(int ind,std::shared_ptr<oracle::occi::ResultSet>rs,int&i){i=rs->getInt(ind+1);} }; template<>struct DataFetcherAux<std::string>{ static void fetch(int ind,std::shared_ptr<oracle::occi::ResultSet>rs,std::string&s){s=rs->getString(ind+1);} };
Here I only showed two type specific fetchers. But, adding new ones for other types is trivial.
The binder follows the same principles as the fetcher. I'll show the code here
// bind data to select statement (one variable at a time) template<typename T>struct DataBinderAux; template<>struct DataBinderAux<int>{ static void bind(int ind,std::shared_ptr<oracle::occi::Statement>stmt,int val){ stmt->setInt(ind+1,val); } }; template<>struct DataBinderAux<std::string>{ static void bind(int ind,std::shared_ptr<oracle::occi::Statement>stmt,std::string const&val){ stmt->setString(ind+1,val); } }; class DataBinder{ public: DataBinder(std::shared_ptr<oracle::occi::Statement>stmt):stmt_(stmt){} DataBinder()=default; DataBinder(DataBinder const&)=default; DataBinder(DataBinder&&)=default; DataBinder&operator=(DataBinder&)=default; DataBinder&operator=(DataBinder&&)=default; ~DataBinder()=default; template<typename T> void operator()(int ind,T const&t){ DataBinderAux<T>::bind(ind,stmt_,t); } private: std::shared_ptr<oracle::occi::Statement>stmt_; };
A was the case with the DataFetcher I only show two binders, one for string and one for int. Adding binders for other types is simple.
Now I'm at the point where it is' time to implement an iterator over a set of rows defined by a select statement. But first, let's recap what we have so far. First, we have a function which applies a user specified callable object to each one of the element in a tuple by calling the object with a tuple index together with a tuple element. Next, we have two classes which make use of this function to retrieve data from an OCCI result set into a tuple and bind data in a tuple to an OCCI statement. It seems like we now have the tools needed to go ahead with an iterator implementation!
The simplest way of coding up an iterator is to go for the boost:: iterator_facade. The façade takes care of lots of details that are easy to get wrong by manually coding an STL compliant iterator. What's left to code ourselves are two functions: increment and dereference together with constructors and other things that are specific to an OCCI iterator.
The iterator, even though it has a few lines of code, is simple. The full implementation of the iterator is shown here:
template<typename Row,typename Bind=std::tuple<>> class OcciSelectIterator:public boost::iterator_facade<OcciSelectIterator<Row,Bind>,Row const,boost::forward_traversal_tag>{ friend class boost::iterator_core_access; public: // ctor, assign,dtor explicit OcciSelectIterator(oracle::occi::Connection*conn,std::string const&select,Bind const&bind=Bind{}): conn_(conn),select_(select),bind_(bind),fetcher_(),stmt_(nullptr),end_(false){ // create row fetcher and fetch first row stmt_=std::shared_ptr<oracle::occi::Statement>{conn_->createStatement(),occi_stmt_deleter(conn_)}; stmt_->setSQL(select); DataBinder binder(stmt_); applyWithIndex(binder,bind_); std::shared_ptr<oracle::occi::ResultSet>rs(stmt_->executeQuery(),occi_rs_deleter(stmt_.get())); fetcher_=DataFetcher(rs); nextrow(); } OcciSelectIterator():end_(true),bind_(Bind{}){} OcciSelectIterator(OcciSelectIterator const&)=default; OcciSelectIterator(OcciSelectIterator&&)=default; OcciSelectIterator&operator=(OcciSelectIterator const&)=default; OcciSelectIterator&operator=(OcciSelectIterator&&)=default; ~OcciSelectIterator()=default; private: // iterator functions void increment(){nextrow();} bool equal(OcciSelectIterator const&other)const{ // any iterators which is marked with 'end_' are identical if(end_||other.end_)return end_&&other.end_; return conn_==other.conn_&&select_==other.select_&&stmt_==other.stmt_&&fetcher_==other.fetcher_; } Row const&dereference()const{ if(end_)throw std::runtime_error("OcciSelectIterator<>: attempt to dereference end iterator"); return currentRow_; } // get next row void nextrow(){ if(end_)throw std::runtime_error("OcciSelectIterator<>: attempt to step past end iterator"); if(fetcher_.getResultSet()->next())applyWithIndex(fetcher_,currentRow_); else end_=true; } // state const std::string select_; const Bind bind_; oracle::occi::Connection*conn_; std::shared_ptr<oracle::occi::Statement>stmt_; DataFetcher fetcher_; Row currentRow_; bool end_; };
It's worth noticing how the end iterator is implemented: an end iterator is simply an iterator created using the default constructor. Of course this means that an end iterator can be used with any select statement as long as the template parameters are of the same type. This may seem strange, but in reality it does not create any problems.
It's easy to use the iterator to select rows from the database. For example:
// authentication info (user, passwd, database OcciAuth const auth{"hans","ewetz","mydb"}; // row and bind typedef tuple<int,string,string,string>row_t; typedef tuple<string>bind_t; string select{"select rownum,dbid,tab,cutoff_dt from MT_DBSYNC_CUTOFF_DT where dbid=:1"}; bind_t bind{"Policy"}; // use iterator interface oracle::occi::Connection*conn{env->createConnection(std::get<0>(auth),std::get<1>(auth),std::get<2>(auth))}; OcciSelectIterator<row_t,bind_t>begin{conn,select,bind}; OcciSelectIterator<row_t,bind_t>end; for(auto it=begin;it!=end;++it)cout<<*it<<endl;
The code generates the output:
[(int: 1)(std::string: Policy)(std::string: TMLO)(std::string: 12-08-13)] [(int: 2)(std::string: Policy)(std::string: TMMAT)(std::string: 12-08-13)]
The OcciAuth type is a simple typedef:
// typedef for authentication data typedef std::tuple<std::string,std::string,std::string>OcciAuth;
That seems easy enough. However, wrapping the iterator inside a collection simplifies execution of a select statement even more. Specifically I want to have the choice of letting the collection manage the database connection. A simple collection can be implemented as follows:
// select collection template<typename Row,typename Bind=std::tuple<>> class OcciSelectCollection{ public: // typedefs typedef typename OcciSelectIterator<Row,Bind>::value_type value_type; typedef typename OcciSelectIterator<Row,Bind>::pointer pointer; typedef typename OcciSelectIterator<Row,Bind>::reference reference; typedef OcciSelectIterator<Row,Bind>iterator; // ctor taking an already created environment explicit OcciSelectCollection(oracle::occi::Environment*env,OcciAuth const&auth,std::string const&select,Bind const&bind=Bind{}): conn_(nullptr),connpool_(nullptr),env_(env),auth_(auth),select_(select),bind_(bind),closeConn_(true),releaseConn_(false){ conn_=env->createConnection(std::get<0>(auth_),std::get<1>(auth_),std::get<2>(auth_)); } // ctor taking an open connection explicit OcciSelectCollection(oracle::occi::Connection*conn,std::string const&select,Bind const&bind=Bind{}): conn_(conn),connpool_(nullptr),env_(nullptr),select_(select),bind_(bind),closeConn_(false),releaseConn_(false){ } // ctor taking a connection pool explicit OcciSelectCollection(oracle::occi::StatelessConnectionPool*connpool,std::string const&select,Bind const&bind=Bind{}): conn_(nullptr),connpool_(connpool),env_(nullptr),select_(select),bind_(bind),closeConn_(false),releaseConn_(true){ conn_=connpool_->getConnection(); } // ctors (all deleted since we don't want to duplicate connection) OcciSelectCollection()=delete; OcciSelectCollection(OcciSelectCollection const&)=delete; OcciSelectCollection(OcciSelectCollection&&)=delete; OcciSelectCollection&operator=(OcciSelectCollection const&)=delete; OcciSelectCollection&operator=(OcciSelectCollection&&)=delete; // dtor - close open occi connection ~OcciSelectCollection(){ if(closeConn_)env_->terminateConnection(conn_);else if(releaseConn_)connpool_->releaseConnection(conn_); } // get begin/end iterators iterator begin()const{return iterator(conn_,select_,bind_);} iterator end()const{return iterator();} // utility functions void setBind(Bind const&bind){bind_=bind;} Bind const&getBind(Bind const&bind)const{return bind_;} private: oracle::occi::Connection*conn_; oracle::occi::StatelessConnectionPool*connpool_; oracle::occi::Environment*env_; const OcciAuth auth_; const std::string select_; Bind bind_; bool closeConn_; bool releaseConn_; };
Management of OCCI objects can be simplified using smart pointers together with deleters. Internally in the implementation I've used smart pointers together with deleters. However, when passing parameters to the collection I've used raw pointers so that a user is not locked into the use of smart pointers. The deleters have the following implementation:
#ifndef __OCCI_BASIC_UTILS_H__ #define __OCCI_BASIC_UTILS_H__ #include <occiData.h> // deleter for environment. struct occi_env_deleter{ void operator()(oracle::occi::Environment*env)const{ if(env)oracle::occi::Environment::terminateEnvironment(env); } }; // deleter for stateless connection pool class occi_stateless_pool_deleter{ public: explicit occi_stateless_pool_deleter(oracle::occi::Environment*env):env_(env){} void operator()(oracle::occi::StatelessConnectionPool*connpool)const{ if(env_&&connpool)env_->terminateStatelessConnectionPool(connpool); } private: oracle::occi::Environment*env_; }; // deleter for connection. class occi_conn_deleter{ public: explicit occi_conn_deleter(oracle::occi::Environment*env):env_(env){} void operator()(oracle::occi::Connection*conn)const{ if(env_&&conn)env_->terminateConnection(conn); } private: oracle::occi::Environment*env_; }; // deleter for statement. class occi_stmt_deleter{ public: explicit occi_stmt_deleter(oracle::occi::Connection*conn):conn_(conn){} void operator()(oracle::occi::Statement*stmt)const{ if(conn_&&stmt)conn_->terminateStatement(stmt); } private: oracle::occi::Connection*conn_; }; // deleter for result set. class occi_rs_deleter{ public: explicit occi_rs_deleter(oracle::occi::Statement*stmt):stmt_(stmt){} void operator()(oracle::occi::ResultSet*rs)const{ if(stmt_&&rs)stmt_->closeResultSet(rs); } private: oracle::occi::Statement*stmt_; }; #endif
So what's the conclusion of all this fiddling with templates? Yes, the template machinery is not so nice unless you are into C++ template meta programming. On the other hand, I didn't really use any horrendously complicated template constructs. Whether you like template meta programming or not, the result is not too bad; without even breaking a sweat I can pick up data from an Oracle database with just a handful lines of code:
#include "occi_utils.h" #include <occiData.h> #include <iostream> using namespace std; using namespace oracle::occi; // main test program int main(){ // auth credentials and OCCI environment OcciAuth const auth{"hans","ewetz","mydb"}; std::shared_ptr<Environment>env(Environment::createEnvironment(Environment::DEFAULT),occi_env_deleter()); try { // row and bind typedef tuple<int,string,string,string>row_t; typedef tuple<string>bind_t; string select{"select rownum,dbid,tab,cutoff_dt from MT_DBSYNC_CUTOFF_DT where dbid=:1"}; bind_t bind{"Policy"}; // use collection interface with authentication OcciSelectCollection<row_t,bind_t>rows{env.get(),auth,select,bind}; for(auto r:rows)cout<<r<<endl; } catch(std::exception&e){ cerr<<"caught exception: "<<e.what()<<endl; return 1; } return 0; }
Now, maybe there is one or two more lines here than in a PERL program. After all, the reason I wrote the code was to show that C++ code can be just as compact as PERL code. So why do I have a few more lines than a PERL program? Well, typesafety has some cost associated with it. Variables have to be declared to ensure that we don't write rubbish code. OK, I could have reduced the code by a few lines at the cost of readability. But all in all, compiled typesafe C++ code is still superior to non-compiled non-typesafe PERL code.
The next step is to write some support classes for updating, inserting and deleting in a database. This part I'll leave for another entry since I haven't written the code yet.
In case you want a copy of the code, feel free to send me an email at: hansewetz@hotmail.com.
No comments:
Post a Comment