Using Server-Side Prepared StatementsIn the earlier parts of this chapter, the code for SQL statement processing is based on the original method provided by the MySQL client library that sends and retrieves all information in string form. This section discusses how to use the binary protocol that is available as of MySQL 4.1. The binary protocol supports server-side prepared statements and allows transmission of data values in native format. Not all statements can be prepared. The prepared-statement API applies to these statements: CREATE TABLE, DELETE, DO, INSERT, REPLACE, SELECT, SET, UPDATE, and most variations of SHOW. The binary protocol underwent quite a bit of revision during the earlier releases of MySQL 4.1. In particular, many functions were renamed from their pre-4.1.2 names. For best results, you should try to use a recent version of MySQL. To use the binary protocol, you must create a statement handler. With this handler, send a statement to the server to be "prepared," or preprocessed. The server analyzes the statement, remembers it, and sends back information about it that the client library stores in the statement handler. A statement to be prepared can be parameterized by including '?' characters to indicate where data values appear that you will supply later when you execute the statement. For example, you might prepare a statement like this: INSERT INTO score (event_id,student_id,score) VALUES(?,?,?) This statement includes three '?' characters that act as parameter markers or placeholders. Later, you can supply data values to be bound to the placeholders. These complete the statement when you execute it. By parameterizing a statement, you make it reusable: The same statement can be executed multiple times, each time with a new set of data values. What this means is that you send the text of the statement only once. Each time you execute the statement, you need send only the data values. For repeated statement execution, this provides a performance boost:
The binary protocol does have some disadvantages, compared to the original non-binary protocol:
The general procedure for using a prepared statement involves several steps: A client application can prepare multiple statements, and then execute each in the order appropriate to the application. If the client connection closes while the server still has prepared statements associated with the connection, the server disposes of them automatically. The following discussion describes how to write a simple program that inserts some records into a table and then retrieves them. The part of the program that processes INSERT statement illustrates how to use placeholders in a statement and transmit data values to the server to be bound to the prepared statement when it is executed. The part that processes a SELECT statement shows how to retrieve a result set produced by executing a prepared statement. You can find the source for this program in the prepared.c and process_prepared_statement.c files in the capi directory of the sampdb distribution. I won't show the code for setting up the connection because it is similar to that for earlier programs. The main part of the program that sets up to use prepared statements looks like this: void process_prepared_statements (MYSQL *conn) { MYSQL_STMT *stmt; char *use_stmt = "USE sampdb"; char *drop_stmt = "DROP TABLE IF EXISTS t"; char *create_stmt = "CREATE TABLE t (i INT, f FLOAT, c CHAR(24), dt DATETIME)"; /* select database and create test table */ if (mysql_query (conn, use_stmt) != 0 || mysql_query (conn, drop_stmt) != 0 || mysql_query (conn, create_stmt) != 0) { print_error (conn, "Could not set up test table"); return; } stmt = mysql_stmt_init (conn); /* allocate statement handler */ if (stmt == NULL) { print_error (conn, "Could not initialize statement handler"); return; } /* insert and retrieve some records */ insert_records (stmt); select_records (stmt); mysql_stmt_close (stmt); /* deallocate statement handler */ } First, we select a database and create a test table. The table contains four columns of varying data types: an INT, a FLOAT, a CHAR, and a DATETIME. These different data types need to be handled in slightly different ways, as will become evident. After the table has been created, we invoke mysql_stmt_init() to allocate a prepared statement handler, insert and retrieve some records, and deallocate the handler. All the real work takes place in the insert_records() and select_records() functions, which we will get to shortly. For error handling, the program also uses a function, print_stmt_error(), that is similar to the print_error() function used in earlier programs but invokes the error functions that are specific to prepared statements: static void print_stmt_error (MYSQL_STMT *stmt, char *message) { fprintf (stderr, "%s\n", message); if (stmt != NULL) { fprintf (stderr, "Error %u (%s): %s\n", mysql_stmt_errno (stmt), mysql_stmt_sqlstate(stmt), mysql_stmt_error (stmt)); } } The insert_records() function takes care of adding new records to the test table. It looks like this: static void insert_records (MYSQL_STMT *stmt) { char *stmt_str = "INSERT INTO t (i,f,c,dt) VALUES(?,?,?,?)"; MYSQL_BIND param[4]; int my_int; float my_float; char my_str[26]; /* ctime() returns 26-character string */ MYSQL_TIME my_datetime; unsigned long my_str_length; time_t clock; struct tm *cur_time; int i; printf ("Inserting records...\n"); if (mysql_stmt_prepare (stmt, stmt_str, strlen (stmt_str)) != 0) { print_stmt_error (stmt, "Could not prepare INSERT statement"); return; } /* * perform all parameter initialization that is constant * and does not change for each row */ memset ((void *) param, 0, sizeof (param)); /* zero the structures */ /* set up INT parameter */ param[0].buffer_type = MYSQL_TYPE_LONG; param[0].buffer = (void *) &my_int; param[0].is_unsigned = 0; param[0].is_null = 0; /* buffer_length, length need not be set */ /* set up FLOAT parameter */ param[1].buffer_type = MYSQL_TYPE_FLOAT; param[1].buffer = (void *) &my_float; param[1].is_null = 0; /* is_unsigned, buffer_length, length need not be set */ /* set up CHAR parameter */ param[2].buffer_type = MYSQL_TYPE_STRING; param[2].buffer = (void *) my_str; param[2].buffer_length = sizeof (my_str); param[2].is_null = 0; /* is_unsigned need not be set, length is set later */ /* set up DATETIME parameter */ param[3].buffer_type = MYSQL_TYPE_DATETIME; param[3].buffer = (void *) &my_datetime; param[3].is_null = 0; /* is_unsigned, buffer_length, length need not be set */ if (mysql_stmt_bind_param (stmt, param) != 0) { print_stmt_error (stmt, "Could not bind parameters for INSERT"); return; } for (i = 1; i <= 5; i++) { printf ("Inserting record %d...\n", i); (void) time (&clock); /* get current time */ /* set the variables that are associated with each parameter */ /* param[0]: set my_int value */ my_int = i; /* param[1]: set my_float value */ my_float = (float) i; /* param[2]: set my_str to current ctime() string value */ /* and set length to point to var that indicates my_str length */ (void) strcpy (my_str, ctime (&clock)); my_str[24] = '\0'; /* chop off trailing newline */ my_str_length = strlen (my_str); param[2].length = &my_str_length; /* param[3]: set my_datetime to current date and time components */ cur_time = localtime (&clock); my_datetime.year = cur_time->tm_year + 1900; my_datetime.month = cur_time->tm_mon + 1; my_datetime.day = cur_time->tm_mday; my_datetime.hour = cur_time->tm_hour; my_datetime.minute = cur_time->tm_min; my_datetime.second = cur_time->tm_sec; my_datetime.second_part = 0; my_datetime.neg = 0; if (mysql_stmt_execute (stmt) != 0) { print_stmt_error (stmt, "Could not execute statement"); return; } sleep (1); /* pause briefly (to let the time change) */ } } The overall purpose of insert_records() is to insert five records into the test table, each of which will contain these values:
The first thing we do in insert_records() is prepare an INSERT statement by passing it to mysql_stmt_prepare(). The statement looks like this: INSERT INTO t (i,f,c,dt) VALUES(?,?,?,?) The statement contains four placeholders, so it's necessary to supply four data values each time the statement is executed. Placeholders typically represent data values in VALUES() lists or in WHERE clauses. But there are places in which they cannot be used:
The next step is to set up an array of MYSQL_BIND structures, one for each placeholder. As demonstrated in insert_records(), setting these up involves two stages:
You could actually perform all initialization within the loop, but that would be less efficient. The first initialization stage begins by zeroing the contents of the param array containing the MYSQL_BIND structures. The program uses memset(), but you could use bzero() if your system doesn't have memset(). These two statements are equivalent: memset ((void *) param, 0, sizeof (param)); bzero ((void *) param, sizeof (param)); Clearing the param array implicitly sets all structure members to zero. Code that follows sets some members to zero to make it explicit what's going on, but that is not strictly necessary. In practice, you need not assign zero to any structure members after clearing the structures. The next step is to assign the proper information to each parameter in the MYSQL_BIND array. For each parameter, the structure members that need to be set depend on the type of value you're transmitting:
After the initial setup of the MYSQL_BIND array has been done, we bind the array to the prepared statement by passing the array to mysql_stmt_bind_param(). Then it's time to assign values to the variables that the MYSQL_BIND structures point to and execute the statement. This takes place in a loop that executes five times. Each iteration of the loop assigns values to the statement parameters:
With the parameter values set, we execute the statement by invoking mysql_stmt_execute(). This function transmits the current values to the server, which incorporates them into the prepared statement and executes it. When insert_records() returns, the test table has been populated and select_records() can be called to retrieve them. select_records() looks like this: static void select_records (MYSQL_STMT *stmt) { char *stmt_str = "SELECT i, f, c, dt FROM t"; MYSQL_BIND param[4]; int my_int; float my_float; char my_str[24]; unsigned long my_str_length; MYSQL_TIME my_datetime; my_bool is_null[4]; printf ("Retrieving records...\n"); if (mysql_stmt_prepare (stmt, stmt_str, strlen (stmt_str)) != 0) { print_stmt_error (stmt, "Could not prepare SELECT statement"); return; } if (mysql_stmt_field_count (stmt) != 4) { print_stmt_error (stmt, "Unexpected column count from SELECT"); return; } /* * initialize the result column structures */ memset ((void *) param, 0, sizeof (param)); /* zero the structures */ /* set up INT parameter */ param[0].buffer_type = MYSQL_TYPE_LONG; param[0].buffer = (void *) &my_int; param[0].is_unsigned = 0; param[0].is_null = &is_null[0]; /* buffer_length, length need not be set */ /* set up FLOAT parameter */ param[1].buffer_type = MYSQL_TYPE_FLOAT; param[1].buffer = (void *) &my_float; param[1].is_null = &is_null[1]; /* is_unsigned, buffer_length, length need not be set */ /* set up CHAR parameter */ param[2].buffer_type = MYSQL_TYPE_STRING; param[2].buffer = (void *) my_str; param[2].buffer_length = sizeof (my_str); param[2].length = &my_str_length; param[2].is_null = &is_null[2]; /* is_unsigned need not be set */ /* set up DATETIME parameter */ param[3].buffer_type = MYSQL_TYPE_DATETIME; param[3].buffer = (void *) &my_datetime; param[3].is_null = &is_null[3]; /* is_unsigned, buffer_length, length need not be set */ if (mysql_stmt_bind_result (stmt, param) != 0) { print_stmt_error (stmt, "Could not bind parameters for SELECT"); return; } if (mysql_stmt_execute (stmt) != 0) { print_stmt_error (stmt, "Could not execute SELECT"); return; } /* * fetch result set into client memory; this is optional, but it * allows mysql_stmt_num_rows() to be called to determine the * number of rows in the result set. */ if (mysql_stmt_store_result (stmt) != 0) { print_stmt_error (stmt, "Could not buffer result set"); return; } else { /* mysql_stmt_store_result() makes row count available */ printf ("Number of rows retrieved: %lu\n", (unsigned long) mysql_stmt_num_rows (stmt)); } while (mysql_stmt_fetch (stmt) == 0) /* fetch each row */ { /* display row values */ printf ("%d ", my_int); printf ("%.2f ", my_float); printf ("%*.*s ", my_str_length, my_str_length, my_str); printf ("%04d-%02d-%02d %02d:%02d:%02d\n", my_datetime.year, my_datetime.month, my_datetime.day, my_datetime.hour, my_datetime.minute, my_datetime.second); } mysql_stmt_free_result (stmt); /* deallocate result set */ } select_records() prepares a SELECT statement, executes it, and retrieves the result. In this case, the statement contains no placeholders: SELECT i, f, c, dt FROM t That means we don't need to set up any MYSQL_BIND structures before executing the statement. But we're not off the hook. The bulk of the work in select_records(), just as in insert_records(), is setting up an array of MYSQL_BIND structures. The difference is that they're used to receive data values from the server after executing the statement rather than to set up data values to be sent to the server before executing the statement. Nevertheless, the procedure for setting up the MYSQL_BIND array is somewhat similar to the corresponding code in insert_records():
After setting up the parameters, we bind the array to the statement by calling mysql_stmt_bind_result(), and then execute the statement. At this point, you can immediately begin fetching rows by calling mysql_stmt_fetch(). Our program demonstrates an optional step that you can do first: It calls mysql_stmt_store_result(), which fetches the entire result set and buffers it in client memory. The advantage of doing this is that you can call mysql_stmt_num_rows() to find out how many rows are in the result set. The disadvantage is that it uses more memory on the client side. The row-fetching loop involves calling mysql_stmt_fetch() until it returns a non-zero value. After each fetch, the variables associated with the parameter structures contain the column values for the current row. Once all the rows have been fetched, a call to mysql_stmt_free_result() releases any memory associated with the result set. At this point, select_rows() returns to the caller, which invokes mysql_stmt_close() to dispose of the prepared statement handler. The preceding discussion provides a broad overview of the prepared statement interface and some of its key functions. The client library includes several other related functions; for more information, consult Appendix G. |