Doing Transactions Right: Transactors

Writing database code can be tricky. One of the most complicated areas is dealing with unexpected error conditions, such as losing one's connection to the database server. For long-running processes you'll frequently find yourself rewriting code for a simple transaction to make it:

This is bad for the heart, and clutters up your code besides. The transactor framework will take this work out of your hands if you let it.

Functors

The mechanism is based on the concept of Functors, a powerful object-oriented design pattern that replaces the older practice of passing callback functions (or hooks, as they're sometimes called, or exits) to foreign code. Unlike classic callback functions, Functors provide an elegant way of maintaining custom state in your callback code, when the exact form or size of that state was not known in advance to the writer of the foreign code that will eventually invoke your callback.

Functors in C++ are simple objects that can be invoked just like functions or function pointers can, by virtue of providing the function invocation operator, operator().

A simple functor could look like this:

	  struct HelloFunctor
	  {
	  void operator()() { cout << "Hello World" << endl; }
	  };
	

...And once an object of this functor type has been created, it can be "invoked" just as if it were a function:

	  HelloFunctor Hi;
	  Hi();
	

But the invoking code may also be foreign code knowing nothing about the type of your functor. The foreign code is usually a template, so it automatically becomes "specialized" to your type of functor when the one meets the other:

	  template<typename FUNCTOR> void DoFunctor(const FUNCTOR &Hi)
	  {
	  Hi();
	  }
	

The great thing about functors is that they can carry state. This is most useful when you need to pass a functor object to foreign code like DoFunctor above, but you need certain extra parameters to be passed to your functor that the foreign code isn't going to pass. The classic C solution to this problem is to let you pass both a function pointer and a pointer-to-void as a method of letting you provide any type of data of your liking to it. This generally makes programs a little harder to read, and is not very safe or convenient.

With functors, there is a better way to do the same thing. Let's say you want to adapt the HelloFunctor class to print its output to a different output stream. What you'd really like to do is add a parameter to your () operator to indicate which stream to print to:

	  void operator()(ostream &Stream)
	  {
	  Stream << "Hello World" << endl;
	  }
	

Unfortunately, DoFunctor doesn't know about this new parameter, let alone what argument to pass! So instead, let's make this Stream a class member:

	  struct HelloFunctor
	  {
	  ostream &Stream;

	  // Set Stream when creating a HelloFunctor
	  explicit HelloFunctor(ostream &S) : Stream(S) { }

	  // Print to output stream selected at construction time
	  void operator()()
	  {
	  Stream << "Hello World" << endl;
	  }
	  };
	

We can now provide the necessary information (ie., which stream to print to) to our HelloFunctor before we pass it to DoFunctor:

	  HelloFunctor Hi1(cout), Hi2(cerr);

	  DoFunctor(Hi1);  // Print to cout
	  DoFunctor(Hi2);  // Print to cerr
	

Naturally a functor's () operator may also return some other type than void, and it may take arguments just like any other function. This mechanism is used extensively by the STL to sort containers, to find items in sorted containers like set or map, and so on.

How Transactors Work

A transactor is a functor derived from an instantiation of libpqxx's transactor class template. Instead of writing your database transaction inline with the rest of your code, you encapsulate it in your functor's () operator. When the time comes to execute your transaction, you create an object of your functor type and pass it to the perform method of your connection to the database.

perform will make a copy of your transactor (which means it needs to be copy-constructible, by the way). It will also create a transaction object and invoke your transactor's () operator, passing the transaction object to it. All your transactor needs to do is perform its queries on this object and return, after which perform will commit the transaction. If your () operator throws an exception instead, perform will discard the copy of your original transactor, and try again with a new copy until it either succeeds or eventually gives up [6]. If the connection is lost, the transaction will fail but perform will restore it transparently and simply try again.

To make all this work, your transactor's () operator must make no changes to the rest of your program's state. Any intermediate results, data to be processed, and so on, must stay within your transactor, and be destroyed when the transactor is. That is the magic trick that allows perform to create copies of your transactor and use them to rerun the transaction as many times as needed, without your program noticing.

So how does your transactor pass query results back to the outside world once it's done? For this purpose, you may redefine transactor's on_commit member function to pass any data back to the rest of your program. This member function will be called only if your transaction succeeded.

(You may also wish to go the other route, storing data to variables outside the transactor right away, and override the on_abort and on_doubt functions to remove the data again if the transaction failed, but this is much more likely to cause subtle bugs.)

Please refer to the reference manual, the source code, and the test programs that come with libpqxx to learn more about how transactors work.



[6] perform gives up if the transaction fails too many times in succession. The maximum number of attempts that are made can be passed to perform as an optional second argument.