I'll finish this chapter by developing a general purpose, interactive query processor. In this example, I'll describe the SQLPrepare()/SQLExec() query execution method. Finally, I'll show you a way to process result sets more efficiently.
This example is based on the libpq++/qt-sql.cpp client from Chapter 10, “The New PostgreSQL C++ API—libpqxx.” Rather than showing you the entire application again, I'll just explain the differences—refer to Chapter 10 for a complete explanation of the original application.
In this application, you can enter arbitrary SQL commands; the result set for SELECT statements appears in a table and the results for other commands display in a status bar.
The first thing that you need to change in this client is the MyTable class. The new MyTable class includes an environment handle (env) and a connection handle (db).
1 /* qt-sql.h */ 2 3 class MyTable : public QTable 4 { 5 public: 6 7 MyTable( QWidget * parent ); 8 9 SQLHDBC db; 10 SQLHENV env; 11 12 void buildTable( SQLHSTMT stmt ); 13 void displayErrors( SQLSMALLINT type, SQLHANDLE handle ); 14 15 };
Next, I'll borrow the resultField structure from the previous example. This structure contains metadata for a field and a pointer to a buffer (value) that holds the field data as you retrieve each row.
// File qt-sql.cpp (partial listing - see downloads for complete text) 22 typedef struct 23 { 24 char name[128+1]; 25 SQLSMALLINT nameLength; 26 SQLSMALLINT dataType; 27 SQLUINTEGER fieldLength; 28 SQLSMALLINT scale; 29 SQLSMALLINT nullable; 30 SQLINTEGER displaySize; 31 int headerLength; 32 SQLINTEGER resultLength; 33 char * value; 34 } resultField;
Now let's look at the MyTable constructor:
// File qt-sql.cpp (partial listing - see downloads for complete text) 109 MyTable::MyTable( QWidget * parent ) 110 : QTable( parent ) 111 { 112 // 113 // Create a database connection... 114 // 115 SQLRETURN res; 116 117 res = SQLAllocHandle( SQL_HANDLE_ENV, 118 SQL_NULL_HANDLE, 119 &env ); 120 if( !SQL_OK( res )) 121 { 122 displayErrors( SQL_HANDLE_ENV, env ); 123 exit( -1 ); 124 } 125 126 SQLSetEnvAttr( env, 127 SQL_ATTR_ODBC_VERSION, 128 (SQLPOINTER)SQL_OV_ODBC2, 129 0 ); 130 131 res = SQLAllocHandle( SQL_HANDLE_DBC, 132 env, 133 &db ); 134 135 if( !SQL_OK( res )) 136 { 137 displayErrors( SQL_HANDLE_ENV, env ); 138 exit( -1 ); 139 } 140 141 res = SQLConnect( db, 142 (SQLCHAR *)qApp->argv()[1], SQL_NTS, 143 (SQLCHAR *)qApp->argv()[2], SQL_NTS, 144 (SQLCHAR *)qApp->argv()[3], SQL_NTS ); 145 146 if( !SQL_OK( res )) 147 { 148 displayErrors( SQL_HANDLE_DBC, db ); 149 exit( -1 ); 150 } 151 152 153 // We don't have any table-oriented results to 154 // show yet, so hide the table. 155 // 156 setNumRows( 0 ); 157 setNumCols( 0 ); 158 }
The MyTable constructor should be familiar by now. You initialize an environment handle, inform ODBC that you are an ODBC version 2 (SQL_OV_ODBC2) application, and then try to connect to the database identified on the command line. When this application is invoked, it expects three command-line arguments: a data source name, a username, and a password. The qApp->argv() function returns a pointer to the array of command-line arguments. If the connection attempt fails, you call the displayErrors() function to display any error messages. displayErrors() is shown here:
// File qt-sql.cpp (partial listing - see downloads for complete text) 160 void MyTable::displayErrors( SQLSMALLINT type, SQLHANDLE handle ) 161 { 162 SQLHDBC dbc = SQL_NULL_HDBC; 163 SQLHENV env = SQL_NULL_HENV; 164 SQLHSTMT stmt = SQL_NULL_HSTMT; 165 166 switch( type ) 167 { 168 case SQL_HANDLE_ENV: env = (SQLHENV)handle; break; 169 case SQL_HANDLE_DBC: dbc = (SQLHENV)handle; break; 170 case SQL_HANDLE_STMT: stmt = (SQLHSTMT)handle; break; 171 } 172 173 SQLRETURN result; 174 SQLCHAR sqlState[6]; 175 SQLINTEGER nativeError; 176 SQLSMALLINT requiredLength; 177 SQLCHAR messageText[SQL_MAX_MESSAGE_LENGTH+1]; 178 179 QDialog * dlg = new QDialog( this, 0, TRUE ); 180 QVBoxLayout * vbox = new QVBoxLayout( dlg ); 181 QPushButton * ok = new QPushButton( "Ok", dlg ); 182 183 setCaption( "Error" ); 184 QMultiLineEdit * edit = new QMultiLineEdit( dlg ); 185 186 vbox->addWidget( edit ); 187 vbox->addWidget( ok ); 188 189 connect( ok, SIGNAL( clicked()), dlg, SLOT( accept())); 190 191 edit->setReadOnly( TRUE ); 192 193 do 194 { 195 result = SQLError( env, 196 dbc, 197 stmt, 198 sqlState, 199 &nativeError, 200 messageText, 201 sizeof( messageText ), 202 &requiredLength ); 203 204 if( SQL_OK( result )) 205 { 206 edit->append((char *)messageText ); 207 edit->append( " " ); 208 } 209 } while( SQL_OK( result )); 210 211 dlg->adjustSize(); 212 dlg->exec(); 213 214 }
The displayErrors() function is complicated by the fact that you may get multiple error messages from ODBC—you can't use the usual QT MessageBox class to display multiple errors. Instead, we construct a dialog that contains an edit control (to contain the error messages) and an OK button. Figure 12.9 shows a typical error message.
After the dialog object has been built, you call SQLError() to retrieve the error messages and append each message into the edit control. When you have retrieved the final error message, you display the dialog by calling the dlg->exec() function.
Now let's look at the code that used to execute a command:
// File qt-sql.cpp (partial listing - see downloads for complete text) 216 void MyMain::execute( void ) 217 { 218 // This function is called whenever the user 219 // presses the 'Execute' button (or whenever 220 // the user presses the Return key while the 221 // edit control has the keyboard focus) 222 SQLHDBC db = table->db; 223 SQLHSTMT stmt; 224 SQLRETURN res; 225 QString qcmd = edit->text(); 226 SQLCHAR * cmd; 227 228 // Convert the query command from Unicode 229 // into an 8-bit, SQLCHAR format 230 231 cmd = (SQLCHAR *)qcmd.latin1(); 232 233 SQLAllocHandle( SQL_HANDLE_STMT, db, &stmt ); 234 235 res = SQLPrepare( stmt, (SQLCHAR *)cmd, SQL_NTS ); 236 237 if( !SQL_OK( res )) 238 { 239 table->displayErrors( SQL_HANDLE_STMT, stmt ); 240 } 241 else 242 { 243 244 if( SQL_OK( SQLExecute( stmt ))) 245 { 246 SQLSMALLINT columnCount; 247 248 SQLNumResultCols( stmt, &columnCount ); 249 250 if( columnCount == 0 ) 251 { 252 SQLINTEGER rowCount; 253 SQLRowCount( stmt, &rowCount ); 254 255 if( rowCount == -1 ) 256 status->message( "Ok" ); 257 else 258 { 259 QString m( "Ok, %1 rows affected" ); 260 261 status->message( m.arg((int)rowCount )); 262 } 263 } 264 else 265 { 266 status->message( "Ok..." ); 267 table->buildTable( stmt ); 268 } 269 } 270 else 271 table->displayErrors( SQL_HANDLE_STMT, stmt ); 272 273 } 274 275 SQLFreeHandle( SQL_HANDLE_STMT, stmt ); 276 }
MyMain::execute() starts by making a copy of the query (edit->text()) and converts the string from Unicode (Qt's native character encoding) into ASCII (the format expected by ODBC).
Next, you initialize a statement handle.
In the previous example (client3.c), I used the SQLExecDirect() function to execute a SQL command. In this function, I am using a different execution model—the Prepare/Execute model.
You should use the Prepare/Execute model if you are expecting to execute the same SQL command multiple times, possibly substituting different values for each execution. Some ODBC-compliant databases support “parameter markers” within a SQL command. You generally use parameter markers when you are using the Prepare/Execute model. Here is an example of a command that contains parameter markers:
insert into customers values ( ?, ?, ? );
Each question mark in this command represents a parameter whose value is provided each time the command is executed. (The parameters are numbered—the leftmost question mark is parameter number 1, the next mark is parameter number 2, and so on.)
The advantage to the Prepare/Execute model is that you send the command to the server only once, but you can execute the command as many times as needed. Most ODBC-compliant databases parse the command and create an execution plan when you call SQLPrepare(). When you want to execute the statement, you bind each parameter to a memory address, place the appropriate value at that address, and then call SQLExecute() to execute the command. When you use the Prepare/Execute model with a database that supports parameter markers, you can gain a huge performance boost.
It's not really appropriate to use the Prepare/Execute model to process ad hoc queries. Prepare/Execute is useful when you plan to execute the same SQL command multiple times. You can also use Prepare/Execute to simplify your code: Factor the code that generates a command into a function separate from the code that generates data.
The Prepare/Execute model has been part of the ODBC architecture for quite a while, but it's a recent addition to the PostgreSQL server. In older versions of PostgreSQL, the ODBC driver simulated the Prepare/Execute process. If you're using a recent version of the ODBC driver (version 1.45 or later) and a recent PostgreSQL server (7.3 or later), and you have enabled the UseServerSidePrepare option, the ODBC driver implements the Prepare/Execute model by executing a PREPARE command followed by an EXECUTE command.
After you have successfully prepared and executed the command entered by the user, you are ready to process the results.
The first thing you need to know is whether the command could have returned any rows. (In other words, was this a SELECT command.) ODBC version 2.x does not provide a function that tells you what kind of SQL command you just executed, but you can use the SQLNumResultCols() to infer that information. If SQLNumResultCols() tells you that there are no columns in the result set, you can assume that you have not executed a SELECT command. In that case, you use SQLRowCount() to determine how many rows (if any) were affected by the command. For UPDATE, INSERT, and DELETE statements, SQLRowCount() returns a value (greater than or equal to zero) indicating how many rows were affected. For other types of statements (such as BEGIN WORK or CREATE TABLE), SQLRowCount() returns -1. Use the value returned by SQLRowCount() to determine how to update the status bar.
When you execute a SELECT command, you call the MyTable::buildtable() function to copy the result set into a table:
// File qt-sql.cpp (partial listing - see downloads for complete text) 278 void MyTable::buildTable( SQLHSTMT stmt ) 279 { 280 // This function is called to fill in 281 // the table control. We want to fill 282 // the table with the result set. 283 SQLSMALLINT i; 284 SQLSMALLINT columnCount; 285 resultField * fields; 286 287 setNumRows( 0 ); 288 setNumCols( 0 ); 289 290 // First, examine the metadata for the 291 // result set so that we know how much 292 // room we need for each column. 293 294 SQLNumResultCols( stmt, &columnCount ); 295 296 fields = new resultField[ columnCount+1 ]; 297 298 setNumCols( columnCount ); 299 300 for( i = 1; i <= columnCount; i++ ) 301 { 302 SQLDescribeCol( stmt, 303 i, 304 (SQLCHAR *)fields[i].name, 305 sizeof( fields[i].name ), 306 &fields[i].nameLength, 307 &fields[i].dataType, 308 &fields[i].fieldLength, 309 &fields[i].scale, 310 &fields[i].nullable ); 311 312 SQLColAttribute( stmt, 313 i, 314 SQL_DESC_DISPLAY_SIZE, 315 NULL, 316 0, 317 NULL, 318 &fields[i].displaySize ); 319 320 fields[i].value = (char *)malloc( fields[i].displaySize+1 ); 321 322 // Build the column headers as we go 323 horizontalHeader()->setLabel( i-1, fields[i].name ); 324 325 } 326 327 // Bind the fields to our buffers 328 for( i = 1; i <= columnCount; i++ ) 329 { 330 SQLRETURN res; 331 332 res = SQLBindCol( stmt, 333 i, 334 SQL_C_CHAR, 335 fields[i].value, 336 fields[i].displaySize+1, 337 &fields[i].resultLength ); 338 339 if( !SQL_OK( res )) 340 displayErrors( SQL_HANDLE_STMT, stmt ); 341 } 342 343 // 344 // Now, put the data into the table... 345 // 346 int row = 0; 347 SQLRETURN res; 348 349 while( SQL_OK(( res = SQLFetch( stmt )))) 350 { 351 if( res == SQL_SUCCESS_WITH_INFO ) 352 displayErrors( SQL_HANDLE_STMT, stmt ); 353 354 setNumRows( row+1 ); 355 356 for( int col = 1; col <= columnCount; col++ ) 357 { 358 setText( row, col-1, fields[col].value ); 359 } 360 361 row++; 362 363 } 364 }
buildTable() starts by initializing the table to zero rows and zero columns. Next, you use SQLNumResultCols() to determine how many columns are in the result set. You allocate a resultField structure for each column.
Then, you build an array of resultField structures (the same way you did in the odbc/client3.c example) using SQLDescribeCol() and SQLColAttribute(). You also populate the table's column headers as you process the metadata.
Rather than using SQLGetData() to retrieve field values, I'm going to bind each column to a memory buffer. Then, as you fetch each row from the server, ODBC automatically copies the data values into your bind buffers. Here is the function prototype for SQLBindCol():
SQLRETURN SQLBindCol( SQLHSTMT stmtHandle, SQLUSMALLINT columnNumber, SQLSMALLINT bindDataType, SQLPOINTER bindBuffer, SQLINTEGER bindBufferLength, SQLLEN * resultLength )
When you call SQLBindCol(), you are binding a column (columnNumber) to a memory address (bindBuffer and bindBufferLength) and asking ODBC to convert the field data into a specific data type (bindDataType). You can also provide a pointer to a result length - after you fetch a row, the result length will contain the length of the data value (or SQL_NULL_DATA if the field is NULL). In general, you will get better performance results if you bind each column rather than using SQLGetData(). You have to call SQLGetData() for each column in each row, but you have to bind each column only once regardless of the number of rows you actually fetch.
After you have bound all the columns in the result set, you can start fetching. For each row that you fetch, you increase the table size by one row (this isn't very efficient, but ODBC does not give you a way to determine the size of the result set without fetching each row).
Finally, use the QTable::setText() member function to insert each column into the table.
Figure 12.10 shows you an example of what you would see when you run the odbc/qt-sql sample.
That's it! The rest of the qt-sql application is explained in Chapter 10.