Client 4—Working with transactors

libpqxx strives to provide a robust connection to a PostgreSQL server, and the transactor<> class reflects that effort. The transactor<> class defines four important member functions:

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 convenient quirk: If your operator() function throws an exception, transactor<> aborts the transaction, creates a new transaction, and re-invokes the operator() function. In fact, transactor<> repeats this cycle until it stops throwing exceptions or until a maximum retry count is reached. That's convenient because you don't have to litter your code with complex retry logic to handle connection failures.

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 increases or decreases the balance in every row in the customers table by some amount. The only public constructor for updateBalance expects a single argument—the amount to add to each balance. The operator() function (declared at line 16) is the function that is called repeatedly to carry out the work of the transaction.

operator() takes a single parameter of argument_type. When the transactor<> framework calls your operator() function, it passes a reference a transaction<>, and you use that transaction<> to execute commands on the server. You choose the type of transaction (transaction<> or robusttransaction<>, read_committed or serializable) when you specify the parent for your own transactor<>-derived class. If you've derived a class from transactor< transaction< serializable > >, operator() is called with a reference to a transaction< serializable >. If you've derived a class from transactor< transaction< read_committed > >, operator() is called with a reference to a transaction< read_committed >. The transactor<> class provides a typedef (argument_type) that specifies the exact transaction<> type.

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 members for updateBalance. m_increment holds a copy of the increment value given to the constructor, and m_increment is added to the balance column of every row in the customers table. After updating the customers table, the operator() function asks the server to compute the total of customer balances, and m_totalResult stores the result set for that query.

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 along—it 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 you—you 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 prohibited from changing the transactor<> inside your operator() function; it just means connection::perform() won't modify the transactor<> you give to it. It modifies a copy of your transactor<> instead. That means you can't interrogate the transactor<> you give to connection::perform() after it returns and it can't be changed within the call to connection::perform(). So, how do you get information back out of the transactor<>? You simply add a reference or pointer to your transactor<>. Initialize the reference when you create a transactor<> object and all copies of the transactor<> will refer to the same object. For example, if you want to extract a float value from your transactor<>, add a float reference to your transactor<>, make sure the reference points to an object outside your transactor<>, and then interrogate the referenced object after connection::perform() completes. Of course, you could store query results in a set of global variables, but using a reference or pointer to a local object is usually a better idea.

client4 is rather simple-minded (it just writes the result to cout), so you won't find a float reference in the updateBalance class.

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 funny-looking code at line 47 calls the constructor for the updateBalance's parent class. Notice that we've used argument_type here to locate the exact data type of updateBalance's parent. The constructor for transactor<> expects a single argument—a const char[] that provides descriptive name for the work performed by the operator() function.

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 extracts a value from the m_totalResult result set and writes that value to cout (the standard output stream). OnCommit() is called only if the transactor<> completes successfully.

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 acknowledgement is received. There isn't much useful work that you can do in an OnDoubt() function other than tell the user that something nasty just happened.

Designing transactor<>-based Applications

The 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 finish—the result values can be found in the output object.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.255.234