Client 4Working with TRansactors
libpqxx strives to provide a robust connection to a PostgreSQL server, and the
void operator()( TRANSACTION & trans ); void OnAbort( const char errorMessage[] ) throw(); void OnCommit( void ) throw(); void OnDoubt( void ) throw();
When you derive a class from
TRansactor<>
, you must provide your own implementation for the
operator()
member function: That's where all the real work is done. To execute a transaction, you create an object derived from
TRansactor<>
and then pass that object to the
connection_base::perform()
member function.
connection_base::perform()
calls the
operator()
function in your
TRansactor<>
object and then calls
OnAbort()
,
OnCommit()
, or
OnDoubt()
to complete the transaction.
transactor<>
has a rather surprising but
Listings 10.1010.14 show a simple application ( client4.cc ) that updates a table using a transactor<> . Listing 10.10. client4.cc (Part 1)
1 /* client4.cc */
2
3 #include <string>
4 #include <iostream>
5
6 #include <pqxx/pqxx>
7
8 using namespace std;
9 using namespace pqxx;
10
11 class updateBalance : public transactor<>
12 {
13 public:
14 updateBalance( float increment = 10.0F );
15
16 void operator()( argument_type & T );
17 void OnCommit( void );
18
19 private:
20 float m_increment;
21 result m_totalResult;
22 };
23
The interesting part of this listing starts with the definition of the updateBalance class at line 11. updateBalance derives from the transactor<> template class. The TRansactor<> template class is parameterized by a TRansaction<> , which itself is a template class parameterized by a transaction isolation type. Class updateBalance derives from the default parameter type and is equivalent to class updateBalance : public transactor< transaction<read_committed> > You can use any transaction<> type to parameterize a transactor<> , although it seems silly to derive a class from TRansactor< nontransaction > . Any of the following choices are valid: transactor<> transactor< transaction<> > transactor< transaction< read_committed > > transactor< transaction< serializable > > transactor< robusttransaction< read_committed > > transactor< robusttransaction< serializable > >
The
updateBalance
class
operator()
takes a single parameter of
argument_type
. When the
transactor<>
framework calls your
operator()
function, it
Remember that the real work performed by a transactor<> happens when the transactor<> framework calls your operator() function. Inside operator() (which we'll show you in a moment), you use a transaction<> to execute commands on the server and process the result set of each command. Where does the transaction<> come from? The transactor<> framework creates a new transaction<> for you every time it calls operator() . When you commit the transaction, the TRansactor<> framework calls your OnCommit() function. If you abort the TRansaction<> or throw an exception, the transactor<> framework calls your OnAbort() function, passing in the reason for failure.
Lines 20 and 21 declare the two data
Listing 10.11. client4.cc (Part 2)
24 int main( int argc, const char * argv[] )
25 {
26
27 try
28 {
29 connection conn( argv[1] );
30
31 conn.perform( updateBalance( 3.0F ));
32 }
33 catch( runtime_error & err )
34 {
35 cerr << err.what();
36 }
37 catch( ... )
38 {
39 cerr << "Unexpected exception" << endl;
40 return( EXIT_FAILURE );
41 }
42
43 return( EXIT_SUCCESS );
44 }
45
Listing 10.11 shows the main() function for client4 . Line 29 creates a connection to the server using the first command-line parameter, if present, to specify connection properties. Line 31 creates a new object of type updateBalance and then calls connection::perform() with that object. connection::perform() is the magic framework we've been talking about all alongit creates a TRansaction<> (of the appropriate type), invokes your operator() function, and then commits the TRansaction<> if everything worked. If the operator() function throws an exception (or calls the transaction<>::abort() function), connection::perform() repeats the cycle until it succeeds. If you call connection::perform() with two arguments, the second argument limits the number of retries; by default, connection::perform() calls your operator() function a maximum of three times. Each time connection::perform() calls your operator() function, it creates a new transaction<> and a new copy of your transactor<> . Because connection::perform() always makes a copy of your transactor<> , you must ensure that your class defines a public copy constructor. You can let the compiler generate the copy constructor for youyou just have to make sure it's public. Also, take a look at the prototype for connection::perform() (we've tidied it up a bit for the sake of readability): void connection::perform( const transactor<> & T, int Attempts = 3 );
Notice that it expects a
const
TRansactor<>
reference. The
const
qualifier does
not
mean you're
client4
is rather simple-minded (it just
Listing 10.12. client4.cc (Part 3)
46 updateBalance::updateBalance( float increment )
47 : transactor< argument_type >( "updating balance" ),
48 m_increment( increment )
49 {
50 }
51
The
updateBalance
constructor in Listing 10.12 is very simple. It expects a single argument (
increment
) and stores that value in the
m_increment
member variable. The
Listing 10.13. client4.cc (Part 4)
52 void updateBalance::operator()( argument_type & trans )
53 {
54
55 string command( "UPDATE customers SET balance = balance + " );
56
57 command += to_string( m_increment );
58
59 result updateResult( trans.exec( command ));
60
61 m_totalResult = trans.exec( "SELECT SUM( balance ) FROM customers" );
62
63 }
64
Listing 10.13 shows the updateBalance::operator() function. operator() is where all the database interaction occurs in a transactor<> . When the transactor<> framework calls operator() , it creates a new transaction<> of type argument_type and provides a reference to that transaction<> . This function executes two commands on the server. The first command adds m_increment to the balance column in every customers row. The second command asks the server to compute the SUM() of all customer balances. operator() stores the result object in m_totalResult for use in the OnCommit() member function. This illustrates an important concept you must keep in mind when you write your own transactor<> -derived classes. Because the operator() function can be called an unpredictable number of times, you should not modify any values outside the transactor<> in operator() . Instead, you should modify your program's state in the OnCommit() function because OnCommit() will never be called more than once and won't be called at all if the transactor<> exceeds its retry limit. Listing 10.14. client4.cc (Part 5)
65 void updateBalance::OnCommit( void )
66 {
67 cout << "Total Balance = " << m_totalResult[0][0] << endl;
68 }
69
The
updateBalance::OnCommit()
function in Listing 10.14 is straightforward. It simply
You can also define OnAbort() and OnDoubt() functions for your own TRansactor<> -derived classes. connection::perform() calls OnAbort() each time the operator() function fails. That means OnAbort() might not be the best place to report error messages. For example, if your transaction fails 50 times, you'll see 50 copies of the same error message. Instead, you can report any error messages in the TRy / catch handler that wraps the call to connection::perform() the runtime_error you catch will contain a copy of the most recent error message.
The
OnDoubt()
function is called if the connection to the server is lost and can't be recovered after executing a
COMMIT
but before an
Designing transactor<> -based ApplicationsThe overall architecture of your application changes when you design around transactors<> . Every transaction becomes an object, and the transaction might execute many times behind the scenes. Here's a strategy you can use when designing transactor<> -based applications. First, define two classes. The first class (which we'll call input ) carries data into the transaction. Any values that act as input to the commands within the TRansactor<> should be stored in an input object. Then, add an input, a reference to an input, or a pointer to an input to your transactor<> . Don't forget that libpqxx might make multiple copies of your transactor<> , so you might prefer to store an input reference in the TRansactor<> instead of a copy of input. In the client4 application, the m_increment value goes into the input class. The second class ( output ) carries result values out of the transaction. Remember that it can't store results inside the transactor<> because libpqxx gets a const copy of the object. Instead, add a reference to output to the transactor<> . That way, the operator() function can store result values somewhere other than the transactor<> itself. If you were to add an output object to your transactor<> (as opposed to a reference to an output), it wouldn't do any good because libpqxx will never let you modify the TRansactor<> object that you give to connection::perform() . In the client4 sample application, you would store the SUM(balances) result in class output. Now, define a transactor<> class. It should contain a reference to an input and a reference to an output. The constructor should expect a const input reference and a non- const output reference. Interact with the server in the transactor<> 's operator() function, but don't modify the output object there. Instead, store any results in the output object when the OnCommit() function is called. When you're ready to execute the transactor<> , create an input object and an output object. Fill the input object with the data values required by the transactor<> . Now create an instance of your transactor<> object, passing references to the input and output objects to the constructor. Call the connection::perform() function with a reference to your new transactor<> and wait for it to finishthe result values can be found in the output object. |