POCO Data Library

POCO Data User Guide

Contents

First Steps

POCO Data is POCO's database abstraction layer which allows users to easily send/retrieve data to/from various databases. Currently supported database connectors are SQLite, MySQL and ODBC. Framework is opened for extension, so additional native connectors (Oracle, PostgreSQL, ...) can be added. The intent behind the Poco::Data framework is to produce the integration between C++ and relational databses in a simple and natural way.

The following complete example shows how to use POCO Data:

#include "Poco/Data/Session.h"
#include "Poco/Data/SQLite/Connector.h"
#include <vector>
#include <iostream>

using namespace Poco::Data::Keywords;
using Poco::Data::Session;
using Poco::Data::Statement;

struct Person
{
    std::string name;
    std::string address;
    int         age;
};

int main(int argc, char** argv)
{
    // register SQLite connector
    Poco::Data::SQLite::Connector::registerConnector();

    // create a session
    Session session("SQLite", "sample.db");

    // drop sample table, if it exists
    session << "DROP TABLE IF EXISTS Person", now;

    // (re)create table
    session << "CREATE TABLE Person (Name VARCHAR(30), Address VARCHAR, Age INTEGER(3))", now;

    // insert some rows
    Person person = 
    {
        "Bart Simpson",
        "Springfield",
        12
    };

    Statement insert(session);
    insert << "INSERT INTO Person VALUES(?, ?, ?)",
        use(person.name),
        use(person.address),
        use(person.age);

    insert.execute();

    person.name    = "Lisa Simpson";
    person.address = "Springfield";
    person.age     = 10;

    insert.execute();

    // a simple query
    Statement select(session);
    select << "SELECT Name, Address, Age FROM Person",
        into(person.name),
        into(person.address),
        into(person.age),
        range(0, 1); //  iterate over result set one row at a time

    while (!select.done())
    {
        select.execute();
        std::cout << person.name << " " << person.address << " " << person.age << std::endl;
    }

    return 0;
}

The above example is pretty much self explanatory.

The using namespace Poco::Data is for convenience only but highly recommended for good readable code. While ses << "SELECT COUNT(*) FROM PERSON", Poco::Data::Keywords::into(count), Poco::Data::Keywords::now; is valid, the aesthetic aspect of the code is improved by eliminating the need for full namespace qualification; this document uses convention introduced in the example above.

The remainder of this tutorial is split up into the following parts:

  • Sessions
  • Inserting and Retrieving Data
  • Statements
  • STL Containers
  • Tuples
  • Limits, Ranges and Steps
  • RecordSets, Iterators and Rows
  • Complex data types: how to map C++ objects to a database table
  • Conclusion

Creating Sessions

Sessions are created via the Session constructor:

Session session("SQLite", "sample.db");

The first parameter contains the type of the Session one wants to create. Currently, supported backends are "SQLite", "ODBC" and "MySQL". The second parameter contains the connection string.

In the case of SQLite, the path of the database file is sufficient as connection string.

For ODBC, the connection string may be a simple "DSN=MyDSNName" when a DSN is configured or a complete ODBC driver-specific connection string defining all the necessary connection parameters (for details, consult your ODBC driver documentation).

For MySQL, the connection string is a semicolon-delimited list of name-value pairs specifying various parameters like host, port, user, password, database, compression and automatic reconnect. Example: "host=localhost;port=3306;db=mydb;user=alice;password=s3cr3t;compress=true;auto-reconnect=true"

Inserting and Retrieving Data

Single Data Sets

Inserting data works by using the content of other variables. Assume we have a table that stores only forenames:

ForeName (Name VARCHAR(30))

If we want to insert one single forename we could simply write:

std::string aName("Peter");
session << "INSERT INTO FORENAME VALUES(" << aName << ")", now;

However, a better solution is to use placeholders and connect each placeholder via a use expression with a variable that will provide the value during execution. Placeholders, depending on your database are recognized by having either a colon(:) in front of the name or simply by a question mark (?) as a placeholder. While having the placeholders marked with a colon followed by a human-readable name is very convenient due to readability, not all SQL dialects support this and universally accepted standard placeholder is (?). Consult your database SQL documentation to determine the valid placeholder syntax.

Rewriting the above code now simply gives:

std::string aName("Peter");
ses << "INSERT INTO FORENAME VALUES(?)", use(aName), now;

In this example the use expression matches the placeholder with the Peter value. Note that apart from the nicer syntax, the real benefits of placeholders — which are performance and protection against SQL injection attacks — don't show here. Check the Statements section to find out more.

Retrieving data from the Database works similar. The into expression matches the returned database values to C++ objects, it also allows to provide a default value in case null data is returned from the database:

std::string aName;
ses << "SELECT NAME FROM FORENAME", into(aName), now;
ses << "SELECT NAME FROM FORENAME", into(aName, 0, "default"), now;

You'll note the integer zero argument in the second into() call. The reason for that is that Poco::Data supports multiple result sets for those databases/drivers that have such capbility and we have to indicate the resultset we are referring to. Attempting to create sufficient overloads of into() creates more trouble than what it's worth and null values can effectively be dealt with through use of either Poco::Nullable wrapper (see Handling Null Entries later in this document) or Poco::Dynamic::Var, which will be set as empty for null values when used as query output target.

It is also possible to combine into and use expressions:

std::string aName;
std::string match("Peter")
ses << "SELECT NAME FROM FORENAME WHERE NAME=?", into(aName), use(match), now;
poco_assert (aName == match);

Typically, tables will not be so trivial, i.e. they will have more than one column which allows for more than one into/use.

Lets assume we have a Person table that contains an age, a first and a last name:

std::string firstName("Peter";
std::string lastName("Junior");
int age = 0;
ses << INSERT INTO PERSON VALUES (?, ?, ?)", use(firstName), use(lastName), use(age), now;
ses << "SELECT (firstname, lastname, age) FROM Person", into(firstName), into(lastName), into(age), now;

Most important here is the order of the into and use expressions. The first placeholder is matched by the first use, the 2nd by the 2nd use etc.

The same is true for the into statement. We select firstname as the first column of the result set, thus into(firstName) must be the first into clause.

Handling NULL entries

A common case with databases are optional data fields that can contain NULL. To accomodate for NULL, use the Poco::Nullable template:

std::string firstName("Peter";
Poco::Nullable<std::string> lastName("Junior");
Poco::Nullable<int> age = 0;
ses << INSERT INTO PERSON VALUES (?, ?, ?)", use(firstName), use(lastName), use(age), now;
ses << "SELECT (firstname, lastname, age) FROM Person", into(firstName), into(lastName), into(age), now;
// now you can check if age was null:
if (!lastName.isNull()) { ... }

The above used Poco::Nullable is a lightweight template class, wrapping any type for the purpose of allowing it to have null value.

If the returned value was null, age.isNull() will return true. Whether empty string is null or not is more of a philosophical question (a topic for discussion in some other time and place); for the purpose of this document, suffice it to say that different databases handle it differently and Poco::Data provides a way to tweak it to user's needs through folowing Session features:

  • emptyStringIsNull
  • forceEmptyString

So, if your database does not treat empty strings as null but you want Poco::Data to emulate such behavior, modify the session like this:

ses.setFeature("emptyStringIsNull", true);

On the other side, if your database treats empty strings as nulls but you do not want it to, you'll alter the session feature:

ses.setFeature("forceEmptyString", true);

Obviously, the above features are mutually exclusive; an attempt to se them both to true will result in an exception being thrown by the Data framework.

Multiple Data Sets

Batches of statements are supported. They return multiple sets of data, so into() call needs and additional parameter to determine which data set it belongs to:

typedef Tuple<std::string, std::string, std::string, int> Person;
std::vector<Person> people;
Person pHomer, pLisa;
int aHomer(42), aLisa(10), aBart(0);

session << "SELECT * FROM Person WHERE Age = ?; "
    "SELECT Age FROM Person WHERE FirstName = 'Bart'; "
    "SELECT * FROM Person WHERE Age = ?",
    into(pHomer, 0), use(aHomer),
    into(aBart, 1),
    into(pLisa, 2), use(aLisa),
    now;

Note

Batches of statements can be used, provided, of course, that the target driver and database engine properly support them. Additionally, the exact SQL syntax may vary for different databases. Stored procedures (see below) returning multiple data sets are handled in the same way.

Now

And now, finally, a word about the now keyword. The simple description is: it is a manipulator. As it's name implies, it forces the immediate execution of the statement. If now is not present, the statement must be executed separately in order for anything interesting to happen.

More on statements and manipulators in the chapters that follow.

Stored Procedures And Functions Support

Most of the modern database systems support stored procedures and/or functions. Does Poco::Data provide any support there? You bet. While the specifics on what exactly is possible (e.g. the data types passed in and out, automatic or manual data binding, binding direction, etc.) is ultimately database dependent, POCO Data does it's best to provide reasonable access to such functionality through in, out and io binding functions. As their names imply, these functions are performing parameters binding tho pass in or receive from the stored procedures, or both. The code is worth thousand words, so here's an Oracle ODBC example:

session << "CREATE OR REPLACE "
    "FUNCTION storedFunction(param1 IN OUT NUMBER, param2 IN OUT NUMBER) RETURN NUMBER IS "
    " temp NUMBER := param1; "
    " BEGIN param1 := param2; param2 := temp; RETURN(param1+param2); "
    " END storedFunction;" , now;

int i = 1, j = 2, result = 0;
session << "{? = call storedFunction(?, ?)}", out(result), io(i), io(j), now; // i = 2, j = 1, result = 3

Stored procedures are allowed to return data sets (a.k.a. cursors):

typedef Tuple<std::string, std::string, std::string, int> Person;
std::vector<Person> people;
int age = 13;
session << "CREATE OR REPLACE "
    "FUNCTION storedCursorFunction(ageLimit IN NUMBER) RETURN SYS_REFCURSOR IS "
    " ret SYS_REFCURSOR; "
    "BEGIN "
    " OPEN ret FOR "
    " SELECT *  FROM Person  WHERE Age < ageLimit; " 
    " RETURN ret; "
    "END storedCursorFunction;" , now;

session << "{call storedCursorFunction(?)}", in(age), into(people), now;

The code shown above works with Oracle databases.

A Word of Warning

As you may have noticed, in the above example, C++ code works very closely with SQL statements. And, as you know, your C++ compiler has no clue what SQL is (other than a string of characters). So it is your responsibility to make sure your SQL statements have the proper structure that corresponds to the number and type of the supplied functions.

Statements

We often mentioned the term Statement in the previous section, but with the exception of the initial example, we have only worked with database session objects so far. Or at least, that's what we made you believe.

In reality, you have already worked with Statements. Lets take a look at the method signature of the << operator at Session:

template <typename T>
Statement Session::operator << (const T&amp; t);

Simply ignore the template stuff in front, you won't need it. The only thing that counts here is that the operator << creates a Statement internally and returns it.

What happened in the previous examples is that the returned Statement was never assigned to a variable but simply passed on to the now part which executed the statement. Afterwards the statement was destroyed.

Let's take one of the previous examples and change it so that we assign the statement:

std::string aName("Peter");
Statement stmt = ( ses << "INSERT INTO FORENAME VALUES(?)", use(aName) );

Note that the brackets around the right part of the assignment are mandatory, otherwise the compiler will complain.

What did we achieve by assigning the statement to a variable? Two things: Control when to execute and the possibility to create a RecordSet (described in its own chapter below).

Here's how we control when to actually execute the statement:

std::string aName("Peter");
Statement stmt = ( ses << "INSERT INTO FORENAME VALUES(?)", use(aName) );
stmt.execute();
poco_assert (stmt.done());

By calling execute we asserted that our query was executed and that the value was inserted. The check to stmt.done() simply guarantees that the statement was fully completed.

Prepared Statements

A prepared statement is created by omitting the "now" clause.

Statement stmt = ( ses << "INSERT INTO FORENAME VALUES(?)", use(aName) );

The advantage of a prepared statement is performance. Assume the following loop:

std::string aName;
Statement stmt = ( ses << "INSERT INTO FORENAME VALUES(?)", use(aName) );
for (int i = 0; i < 100; ++i)
{
    aName.append("x");
    stmt.execute();
}

Instead of creating and parsing the Statement 100 times, we only do this once and then use the placeholder in combination with the use clause to insert 100 different values into the database.

Still, this isn't the best way to insert a collection of values into a database. Poco::Data is STL-aware and will cooperate with STL containers to extract multiple rows from the database. More on that in the chapter titled "STL Containers".

Asynchronous Execution

So far, the statements were executing synchronously. In other words, regardless of whether the execute() method was invoked indirectly through now manipulator or through direct method call, it did not return control to the caller until the requested execution was completed. This behavior can be changed, so that execute() returns immediately, while, in fact, it keeps on running in a separate thread. This paragraph explains how this behavior can be achieved as well as warns about the dangers associated with asynchronous execution.

Asynchronous execution can be invoked on any statement, through the direct call to executeAsync() method. This method returns a const reference to Statement::Result. This reference can be used at a later time to ensure completion of the background execution and, for those statements that return rows, find out how many rows were retrieved.

Here's the code:

Statement stmt = (ses << "SELECT (firstname, lastname, age) FROM Person", into(firstName), into(lastName), into(age));
Statement::Result result = stmt.executeAsync();
// ... do something else
Statement::ResultType rows = result.wait();

The above code did not do anything "under the hood" to change the statement's nature. If we call execute() afterwards, it will execute synchronously as usual. There is, however, a way (or two) to turn the statement into asynchronous mode permanently.

First, there is an explicit setAync() call:

Statement stmt = (ses << "SELECT (age) FROM Person", into(age));
stmt.setAsync(true); // make stmt asynchronous
stmt.execute(); // executes asynchronously
// ... do something else
Statement::ResultType rows = stmt.wait(); // synchronize and retrieve the number of rows

And, then, there is also the async manipulator that has the same effect as the setAync(true) code above:

Statement stmt = (ses << "SELECT (age) FROM Person", into(age), async); // asynchronous statement
stmt.execute(); // executes asynchronously
// ... do something else
Statement::ResultType rows = stmt.wait();

Note

In the first example, we have received Result from the statement, while in the second two, we did not assign the return value from execute(). The Result returned from executeAsync() is also known as future — a variable holding a result that will be known at some point in future. The reason for not keeping the execute() return value is because, for asynchronous statements, execute() always returns zero. This makes sense, because it does not know the number of returned rows (remember, asynchronous execute() call returns immediately and does not wait for the completion of the execution).

A Word of Warning

With power comes responsibility. When executing asynchronously, make sure to synchronize accordingly. When you fail to synchronize explicitly, you may encounter all kinds of funny things happening. Statement does internally try to protect you from harm, so the following code will usually throw InvalidAccessException:

Statement stmt = (ses << "SELECT (age) FROM Person", into(age), async); // asynchronous statement
Statement::Result result = stmt.execute(); // executes asynchronously
stmt.execute(); // throws InvalidAccessException

We say "usually", because it may not happen every time, depending whether the first execute() call completed in the background prior to calling the second one. Therefore, to avoid unpleasant surprises, it is highly recommended to always call wait() on either the statement itself or the result (value returned from executeAsync()) prior to engaging into a next attempt to execute.

Things NOT To Do

The use keyword expects as input a reference parameter, which is bound later during execution. Thus, one should never pass temporaries to use():

Statement stmt = (ses << "INSERT INTO PERSON VALUES (?, ?, ?)", use(getForename()), use(getSurname()), use(getAge())); //!!!
// do something else
stmt.execute(); // oops!

It is possible to use bind() instead of use(). The bind() call will always create a copy of the supplied argument. Also, it is possible to execute a statement returning data without supplying the storage and have the statement itself store the returned data for later retrieval through RecordSet. For details, see RecordSet chapter.

Things TO Do

Constants, as well as naked variables (of POD and std::string types) are permitted in the comma-separated list passed to statement. The following example is valid:

std::string fname = "Bart";
std::string lname = "Simpson";
int age = 42;
Statement stmt = (ses << "INSERT INTO %s VALUES (?, ?, %d)", "PERSON", use(fname), use(lname), 12);
stmt.execute();

Placeholders for values are very similar (but not identical) to standard printf family of functions. For details refer to Poco::format() documentation. Note: If you are alarmed by mention of printf(), a well-known source of many security problems in C and C++ code, do not worry. Poco::format() family of functions is safe (and, admittedly, slower than printf).

For cases where this type of formatting is used with queries containing the percent sign, use double percent ("%%"):

Statement stmt = (ses << "SELECT * FROM %s WHERE Name LIKE 'Simp%%'", "Person");
stmt.execute();

yields the following SQL statement string:

SELECT * FROM Person WHERE Name LIKE 'Simp%'

STL Containers

To handle many values at once, which is a very common scenario in database access, STL containers are used.

The framework supports the following container types out-of-the-box:

  • deque: no requirements
  • vector: no requirements
  • list: no requirements
  • set: the < operator must be supported by the contained datatype. Note that duplicate key/value pairs are ignored.
  • multiset: the < operator must be supported by the contained datatype
  • map: the () operator must be supported by the contained datatype and return the key of the object. Note that duplicate key/value pairs are ignored.
  • multimap: the () operator must be supported by the contained datatype and return the key of the object

A "one-at-atime" bulk insert example via vector would be:

std::string aName;
std::vector<std::string> data;
for (int i = 0; i < 100; ++i)
{
    aName.append("x");
    data.push_back(aName);
}
ses << "INSERT INTO FORENAME VALUES(?)", use(data), now;

The same example would work with list, deque, set or multiset but not with map and multimap (std::string has no () operator).

Note that use requires a non-empty container!

Now reconsider the following example:

std::string aName;
ses << "SELECT NAME FROM FORENAME", into(aName), now;

Previously, it worked because the table contained only one single entry but now the database table contains at least 100 strings, yet we only offer storage space for one single result.

Thus, the above code will fail and throw an exception.

One possible way to handle this is:

std::vector<std::string> names;
ses << "SELECT NAME FROM FORENAME", into(names), now;

And again, instead of vector, one could use deque, list, set or multiset.

Things NOT To Do

C++ containers in conjunction with stored procedures input parameters (i.e in and io functions) are not supported. Furthermore, there is one particular container which, due to its peculiar nature, can not be used in conjunction with out and io under any circumstances: std::vector<bool> . The details are beyond the scope of this manual. For those interested to learn more about it, there is an excellent explanation in S. Meyers book "Efective STL", Item 18 or Gotw #50, When Is a Container Not a Container]] paragraph.

Tuples

Complex user-defined data types are supported through type handlers as described in one of the chapters below. However, in addition to STL containers, which are supported through binding/extraction there is another complex data type supported by POCO Data "out-of-the-box". The type is Poco::Tuple. The detailed description is beyond the scope of this manual, but suffice it to say here that this data structure allows for convenient and type-safe mix of different data types resulting in a perfect C++ match for the table row. Here's the code to clarify the point:

typedef Poco::Tuple<std::string, std::string, int> Person;
Person person("Bart Simpson", "Springfield", 12)
session << "INSERT INTO Person VALUES(?, ?, ?)", use(person), now;

Automagically, POCO Data internally takes care of the data binding intricacies for you. Of course, as before, it is programmer's responsibility to make sure the Tuple data types correspond to the table column data types.

I can already see the reader wondering if it's possible to put tuples in a container and kill more than one bird with one stone. As usual, POCO Data will not disappoint you:

typedef Poco::Tuple<std::string, std::string, int> Person;
typedef std::vector<Person> People;
People people;
people.push_back(Person("Bart Simpson", "Springfield", 12));
people.push_back(Person("Lisa Simpson", "Springfield", 10));
session << "INSERT INTO Person VALUES(?, ?, ?)", use(people), now;

And thats it! There are multiple columns and multiple rows contained in a single variable and inserted in one shot. Needless to say, the reverse works as well:

session << "SELECT Name, Address, Age FROM Person", into(people), now;

Limits and Ranges

Limit

Working with collections might be convenient to bulk process data but there is also the risk that large operations will block your application for a very long time. In addition, you might want to have better fine-grained control over your query, e.g. you only want to extract a subset of data until a condition is met.

To alleviate that problem, one can use the limit keyword.

Let's assume we are retrieving thousands of rows from a database to render the data to a GUI. To allow the user to stop fetching data any time (and to avoid having the user frantically click inside the GUI because it doesn't show anything for seconds), we have to partition this process:

std::vector<std::string> names;
ses << "SELECT NAME FROM FORENAME", into(names), limit(50), now;

The above example will retrieve up to 50 rows from the database (note that returning nothing is valid!) and append it to the names collection, i.e. the collection is not cleared!

If one wants to make sure that exactly 50 rows are returned one must set the second limit parameter (which per default is set to false) to true:

std::vector<std::string> names;
ses << "SELECT NAME FROM FORENAME", into(names), limit(50, true), now;

Iterating over a complete result collection is done via the Statement object until statement.done() returns true.

For the next example, we assume that our system knows about 101 forenames:

std::vector<std::string> names;
Statement stmt = (ses << "SELECT NAME FROM FORENAME", into(names), limit(50)); 
stmt.execute(); //names.size() == 50
poco_assert (!stmt.done());
stmt.execute(); //names.size() == 100
poco_assert (!stmt.done());
stmt.execute(); //names.size() == 101
poco_assert (stmt.done()); 

We previously stated that if no data is returned this is valid too. Thus, executing the following statement on an empty database table will work:

std::string aName;
ses << "SELECT NAME FROM FORENAME", into(aName), now;

To guarantee that at least one valid result row is returned use the lowerLimit clause:

std::string aName;
ses << "SELECT NAME FROM FORENAME", into(aName), lowerLimit(1), now;

If the table is now empty, an exception will be thrown. If the query succeeds, aName is guaranteed to be initialized. Note that limit is only the short name for upperLimit. To iterate over a result set step-by-step, e.g. one wants to avoid using a collection class, one would write

std::string aName;
Statement stmt = (ses << "SELECT NAME FROM FORENAME", into(aName), lowerLimit(1), upperLimit(1));
while (!stmt.done()) stmt.execute();

Range

For the lazy folks, there is the range command:

std::string aName;
Statement stmt = (ses << "SELECT NAME FROM FORENAME", into(aName), range(1,1));
while (!stmt.done()) stmt.execute();

The third parameter to range is an optional boolean value which specifies if the upper limit is a hard limit, ie. if the amount of rows returned by the query must match exactly. Per default exact matching is off.

Bulk

The bulk keyword allows to boost performance for the connectors that support column-wise operation and arrays of values and/or parameters (e.g. ODBC). Here's how to signal bulk insertion to the statement:

std::vector<int> ints(100, 1);
session << "INSERT INTO Test VALUES (?)", use(ints, bulk), now;

The above code will execute a "one-shot" insertion into the target table.

Selection in bulk mode looks like this:

std::vector<int> ints;
session << "SELECT * FROM Test", into(ints, bulk(100)), now;

Note that, when fetching data in bulk quantities, we must provide the size of data set we want to fetch, either explicitly as in the code above or implicitly, through size of the supplied container as in following example:

std::vector<int> ints(100, 1); session << "SELECT * FROM Test", into(ints, bulk), now;

For statements that generate their ow internal extraction storage (see RecordSet chapter below), bulk execution can be specified as follows:

session << "SELECT * FROM Test", bulk(100), now;

Usage Notes

When using bulk mode, execution limit is set internally. Mixing of bulk and limit keywords, although redundant, is allowed as long as they do not conflict in the value they specify.

Bulk operations are only supported for following STL containers:

  • std::deque
  • std::list
  • std::vector, including std::vector<bool>, which is properly handled internally

For best results with use(), when passing POD types, it is recommended to use std::vector as it is passed directly as supplied by the user. For all the other scenarios (other containers as well as non-POD types), framework will create temporary storage.

Data types supported are:

Important Considerations

Not all the connectors support bulk and some support it only to an extent, depending on the target system. Also, not all value types perform equally when used for bulk operations. To determine the optimal use in a given scenario, knowledge of the target system as well as some degree of experimentation is needed because different connectors and target systems shall differ in performance gains. In some scenarios, the gain is significant. For example, Oracle ODBC driver performs roughly 400-500 times faster when bulk-inserting a std::vector of 10,000 integers. However, when variable-sized entities, such as strings and BLOBs are brought into the picture, performance decreases drastically. So, all said, it is left to the end-user to make the best of this feature.

RecordSets, Iterators and Rows

In all the examples so far the programmer had to supply the storage for data to be inserted or retrieved from a database.

It is usually desirable to avoid that and let the framework take care of it, something like this:

session << "SELECT * FROM Person", now; // note the absence of target storage

No worries — that's what the RecordSet class does:

Statement select(session); // we need a Statement for later RecordSet creation
select << "SELECT * FROM Person", now;

// create a RecordSet 
RecordSet rs(select);
std::size_t cols = rs.columnCount();

// print all column names
for (std::size_t col = 0; col < cols; ++col)
    std::cout << rs.columnName(col) << std::endl;

// iterate over all rows and columns
for (RecordSet::Iterator it = rs.begin(); it != rs.end(); ++it) 
    std::cout << *it << " ";

As you may see above, RecordSet class comes with a full-blown C++ compatible iterator that allows the above loop to be turned into a one-liner:

std::copy(rs.begin(), rs.end(), std::ostream_iterator<Row>(std::cout));

RecordSet has the stream operator defined, so this shortcut to the above functionality will work, too:

std::cout << rs;

The default formatter supplied with RecordSet is quite rudimentary, but user can implement custom formatters, by inheriting from RowFormatter and providing definitions of formatNames() and formatValues() virtual functions. See the RowFormatter sample for details on how to accomplish this.

You'll notice the Row class in the above snippet. The RecordSet::Iterator is actually a Poco::Data::RowIterator. It means that dereferencing it returns a Poco::Data::Row object. Here's a brief example to get an idea of what the Poco::Data::Row class does:

Row row;
row.append("Field0", 0);
row.append("Field1", 1);
row.append("Field2", 2);

The above code creates a row with three fields, "Field0", "Field1" and "Field2", having values 0, 1 and 2, respectively. Rows are sortable, which makes them suitable to be contained by standard sorted containers, such as std::map or std::set. By default, the first field of the row is used for sorting purposes. However, the sort criteria can be modified at runtime. For example, an additional field may be added to sort fields (think "... ORDER BY Name ASC, Age DESC"):

row.addSortField("Field1"); // now Field0 and Field1 are used for sorting 
row.replaceSortField("Field0", "Field2");// now Field1 and Field2 are used for sorting

Finally, if you have a need for different RecordSet internal storage type than default (std::deque) provided by framework, there is a manipulator for that purpose:

select << "SELECT * FROM Person", list, now; // use std::list as internal storage container

This can be very useful if you plan to manipulate the data after retrieving it from database. For example, std::list performs much better than std::vector for insert/delete operations and specifying it up-front as internal storage saves you the copying effort later. For large datasets, performance savings are significant.

Valid storage type manipulators are:

  • deque (default)
  • vector
  • list

So, if neither data storage, nor storage type are explicitly specified, the data will internally be kept in standard deques. This can be changed through use of storage type manipulators.

Complex Data Types

All the previous examples were contented to work with only the most basic data types: integer, string, ... a situation, unlikely to occur in real-world scenarios.

Assume you have a class Person:

class Person
{
public:
    // default constructor+destr.
    // getter and setter methods for all members
    // ...

    bool operator <(const Person&amp; p) const
        /// we need this for set and multiset support
    {
        return _socialSecNr < p._socialSecNr;
    }

    Poco::UInt64 operator()() const
        /// we need this operator to return the key for the map and multimap
    {
        return _socialSecNr;
    }

private:
    std::string _firstName;
    std::string _lastName;
    Poco::UInt64 _socialSecNr;
};

Ideally, one would like to use a Person as simple as one used a string. All that is needed is a template specialization of the TypeHandler template. Note that template specializations must be declared in the same namespace as the original template, i.e. Poco::Data. The template specialization must implement the following methods:

namespace Poco {
namespace Data {

template <>
class TypeHandler<class Person>
{
public:
    static void bind(std::size_t pos, const Person&amp; obj, AbstractBinder::Ptr pBinder, AbstractBinder::Direction dir)
    {
        poco_assert_dbg (!pBinder.isNull());
        // the table is defined as Person (FirstName VARCHAR(30), lastName VARCHAR, SocialSecNr INTEGER(3))
        // Note that we advance pos by the number of columns the datatype uses! For string/int this is one.
        TypeHandler<std::string>::bind(pos++, obj.getFirstName(), pBinder, dir);
        TypeHandler<std::string>::bind(pos++, obj.getLastName(), pBinder, dir);
        TypeHandler<Poco::UInt64>::bind(pos++, obj.getSocialSecNr(), pBinder, dir);
    }

    static std::size_t size()
    {
        return 3; // we handle three columns of the Table!
    }

    static void prepare(std::size_t pos, const Person&amp; obj, AbstractPreparator::Ptr pPrepare)
    {
        poco_assert_dbg (!pPrepare.isNull());
        // the table is defined as Person (FirstName VARCHAR(30), lastName VARCHAR, SocialSecNr INTEGER(3))
        // Note that we advance pos by the number of columns the datatype uses! For string/int this is one.
        TypeHandler<std::string>::prepare(pos++, obj.getFirstName(), pPrepare);
        TypeHandler<std::string>::prepare(pos++, obj.getLastName(), pPrepare);
        TypeHandler<Poco::UInt64>::prepare(pos++, obj.getSocialSecNr(), pPrepare);
    }

    static void extract(std::size_t pos, Person&amp; obj, const Person&amp; defVal, AbstractExtractor::Ptr pExt)
        /// obj will contain the result, defVal contains values we should use when one column is NULL
    {
        poco_assert_dbg (!pExt.isNull());
        std::string firstName;
        std::string lastName;
        Poco::UInt64 socialSecNr = 0;
        TypeHandler<std::string>::extract(pos++, firstName, defVal.getFirstName(), pExt);
        TypeHandler<std::string>::extract(pos++, lastName, defVal.getLastName(), pExt);
        TypeHandler<Poco::UInt64>::extract(pos++, socialSecNr, defVal.getSocialSecNr(), pExt);
        obj.setFirstName(firstName);
        obj.setLastName(lastName);
        obj.setSocialSecNr(socialSecNr);
    }

private:
    TypeHandler();
    ~TypeHandler();
    TypeHandler(const TypeHandler&amp;);
    TypeHandler&amp; operator=(const TypeHandler&amp;);
};

} } // namespace Poco::Data

And that's all you have to do. Working with Person is now as simple as working with a string:

std::map<Poco::UInt64, Person> people;
ses << "SELECT * FROM Person", into(people), now;

Session Pooling

Creating a connection to a database is often a time consuming operation. Therefore it makes sense to save a session object for later reuse once it is no longer needed.

A Poco::Data::SessionPool manages a collection of sessions. When a session is requested, the SessionPool first looks in its set of already initialized sessions for an available object. If one is found, it is returned to the client and marked as "in-use". If no session is available, the SessionPool attempts to create a new one for the client. To avoid excessive creation of sessions, a limit can be set on the maximum number of objects.

The following code fragment shows how to use the SessionPool:

SessionPool pool("ODBC", "...");
// ...
Session sess(pool.get());

Pooled sessions are automatically returned to the pool when the Session variable holding them is destroyed.

One session pool, of course, holds sessions for one database connection. For sessions to multiple databases, there is SessionPoolContainer:

SessionPoolContainer spc;
AutoPtr<SessionPool> pPool1 = new SessionPool("ODBC", "DSN1");
AutoPtr<SessionPool> pPool2 = new SessionPool("ODBC", "DSN2");
spc.add(pPool1);
spc.add(pPool2);

Conclusion

This document provides an overview of the most important features offered by the POCO Data framework. The framework also supports LOB (specialized to BLOB and CLOB) type as well as Poco::DateTime binding. The usage of these data types is no different than any C++ type, so we did not go into details here.

The great deal of RecordSet and Row runtime "magic" is achieved through employment of Poco::Dynamic::Var, which is the POCO equivalent of dynamic language data type. Obviously, due to its nature, there is a run time performance penalty associated with Poco::Dynamic::Var, but the internal details are beyond the scope of this document.

POCO Data tries to provide a broad spectrum of functionality, with configurable efficiency/convenience ratio, providing a solid foundation for quick development of database applications. We hope that, by reading this manual and experimenting with code along the way, you were able to get a solid understanding of the framework. We look forward to hearing from you about POCO Data as well as this manual. We also hope that you find both to be helpful aid in design of elegant and efficient standard C++ database access software.