- General Procedure for Building Client Programs
- Client 1—Connecting to the Server
- Client 2—Adding Error Checking
- Client 3—Making the Connection Code Modular
- Client 4—Getting Connection Parameters at Runtime
- Processing Queries
- Client 5—An Interactive Query Program
- Miscellaneous Topics
Client 3Making the Connection Code Modular
For our third client, client3, we will make the connection and disconnection code more modular by encapsulating it into functions do_connect() and do_disconnect(), which can be used easily by multiple client programs. This provides an alternative to embedding the connection code literally into your main() function. That's a good idea anyway for any code that's stereotypical across applications. Put it in a function that you can access from multiple programs rather than writing it out in each one. If you fix a bug in or make an improvement to the function, you can change it once and all the programs that use the function can be fixed or take advantage of the improvement just by being recompiled. Also, some client programs are written such that they may connect and disconnect several times during the course of their execution. It's a lot easier to write such a client if you make the code modular by putting your setup and teardown machinery in connect and disconnect functions.
The encapsulation strategy works like this:
Split out common code into wrapper functions in a separate source file, common.c.
Provide a header file, common.h, containing prototypes for the common routines.
Include common.h in client source files that use the common routines.
Compile the common source into an object file.
Link that common object file into your client program.
With that strategy in mind, let's construct do_connect() and do_disconnect().
do_connect() replaces the calls to mysql_init() and mysql_real_connect(), as well as the error-printing code. You call it just like mysql_real_connect(), except that you don't pass any connection handler. Instead, do_connect() allocates and initializes the handler itself, then returns a pointer to it after connecting. If do_connect() fails, it returns NULL after printing an error message. (That way, any program that calls do_connect() and gets a NULL return value can simply exit without worrying about printing a message itself.)
do_disconnect() takes a pointer to the connection handler and calls mysql_close().
Here is the code for common.c:
#include <stdio.h> #include <mysql.h> #include "common.h" MYSQL * do_connect (char *host_name, char *user_name, char *password, char *db_name, unsigned int port_num, char *socket_name, unsigned int flags) { MYSQL *conn; /* pointer to connection handler */ conn = mysql_init (NULL); /* allocate, initialize connection handler */ if (conn == NULL) { fprintf (stderr, "mysql_init() failed\n"); return (NULL); } if (mysql_real_connect (conn, host_name, user_name, password, db_name, port_num, socket_name, flags) == NULL) { fprintf (stderr, "mysql_real_connect() failed:\nError %u (%s)\n", mysql_errno (conn), mysql_error (conn)); return (NULL); } return (conn); /* connection is established */ } void do_disconnect (MYSQL *conn) { mysql_close (conn); }
common.h declares the prototypes for the routines in common.c:
MYSQL * do_connect (char *host_name, char *user_name, char *password, char *db_name, unsigned int port_num, char *socket_name, unsigned int flags); void do_disconnect (MYSQL *conn);
To access the common routines, include common.h in your source files. Note that common.c includes common.h as well. That way, you get a compiler warning immediately if the function definitions in common.c don't match the declarations in the header file. Also, if you change a calling sequence in common.c without making the corresponding change to common.h, the compiler will warn you when you recompile common.c.
It's reasonable to ask why anyone would invent a wrapper function, do_disconnect(), that does so little. It's true that do_disconnect() and mysql_close() are equivalent. But suppose sometime down the road you decide there is some additional cleanup you'd like to perform whenever you disconnect. By calling a wrapper function that you have complete control over, you can modify the wrapper to do what you like and the change takes effect uniformly for any disconnect operation you do. You can't do this if you invoke mysql_close() directly.
Earlier, I asserted that it's beneficial to modularize commonly used code by encapsulating it in a function that can be used by multiple programs, or from multiple places within a single program. The preceding paragraph gives one reason why, and the following two examples provide some additional justification.
-
Example 1. In versions of MySQL prior to the 3.22 series, the mysql_real_connect() call was slightly different than it is now: There was no database name parameter. If you want to use do_connect() with an older MySQL client library, it won't work. However, it's possible to modify do_connect() so that it will work on pre-3.22 installations. This means that by modifying do_connect(), you can increase the portability of all programs that use it. If you embed the connect code literally in every client, you must modify each of them individually.
To fix do_connect() so that it can deal with the older form of mysql_real_connect(), use the MYSQL_VERSION_ID macro that contains the current MySQL version number. The modified do_connect() tests the value of MYSQL_VERSION_ID and uses the proper form of mysql_real_connect():
MYSQL * do_connect (char *host_name, char *user_name, char *password, char *db_name, unsigned int port_num, char *socket_name, unsigned int flags) { MYSQL *conn; /* pointer to connection handler */ conn = mysql_init (NULL); /* allocate, initialize connection handler */ if (conn == NULL) { fprintf (stderr, "mysql_init() failed\n"); return (NULL); } #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 32200 /* 3.22 and up */ if (mysql_real_connect (conn, host_name, user_name, password, db_name, port_num, socket_name, flags) == NULL) { fprintf (stderr, "mysql_real_connect() failed:\nError %u (%s)\n", mysql_errno (conn), mysql_error (conn)); return (NULL); } #else /* pre-3.22 */ if (mysql_real_connect (conn, host_name, user_name, password, port_num, socket_name, flags) == NULL) { fprintf (stderr, "mysql_real_connect() failed:\nError %u (%s)\n", mysql_errno (conn), mysql_error (conn)); return (NULL); } if (db_name != NULL) /* simulate effect of db_name parameter */ { if (mysql_select_db (conn, db_name) != 0) { fprintf (stderr, "mysql_select_db() failed:\nError %u (%s)\n", mysql_errno (conn), mysql_error (conn)); mysql_close (conn); return (NULL); } } #endif return (conn); /* connection is established */ }
The modified version of do_connect() is identical to the previous version in all respects except two:
-
It doesn't pass a db_name parameter to the older form of mysql_real_connect() because that version has no such parameter.
-
If the database name is non-NULL, do_connect() calls mysql_select_db() to make the named database current. (This simulates the effect of the missing db_name parameter). If the database cannot be selected, do_connect() prints an error message, closes the connection, and returns NULL to indicate failure.
Example 2. This example builds on the changes made to do_connect() for the first example. Those changes result in three sets of calls to the error functions mysql_errno() and mysql_error(), and it's really tiresome write those out each time the code needs to squawk about a problem. Besides, the error printing code is visually noisy and difficult to read. It's easier to read something like this:
print_error (conn, "mysql_real_connect() failed");
So let's encapsulate error printing in a print_error() function. We can write it to do something sensible even if conn is NULL. That way, we can use print_error() if the mysql_init() call fails, and we don't have a mix of calls (some to fprintf() and some to print_error()).
I can hear someone in the back row objecting: "Well, you don't really have to call both error functions every time you want to report an error, so you're making your code difficult to read on purpose just so your encapsulation example looks better. And you wouldn't really write out all that error-printing code anyway; you'd write it once, then use copy and paste when you need it again." Those are valid points, but I would address the objections like this:
-
Even if you use copy and paste, it's easier to do so with shorter sections of code.
-
Whether or not you prefer to invoke both error functions each time you report an error, writing out all the error-reporting code the long way leads to the temptation to take shortcuts and be inconsistent when you do report errors. Putting the error-reporting code in a wrapper function that's easy to invoke lessens this temptation and improves coding consistency.
-
If you ever do decide to modify the format of your error messages, it's a lot easier if you only need to make the change one place, rather than throughout your program. Or, if you decide to write error messages to a log file instead of (or in addition to) writing them to stderr, it's easier if you only have to change print_error(). This approach is less error prone and, again, lessens the temptation to do the job halfway and be inconsistent.
-
If you use a debugger when testing your programs, putting a breakpoint in the error-reporting function is a convenient way to have the program break to the debugger when it detects an error condition.
Here's our error-reporting function print_error():
void print_error (MYSQL *conn, char *message) { fprintf (stderr, "%s\n", message); if (conn != NULL) { fprintf (stderr, "Error %u (%s)\n", mysql_errno (conn), mysql_error (conn)); } }
print_error() is in common.c, so we add a prototype for it to common.h:
void print_error (MYSQL *conn, char *message);
Now do_connect() can be modified to use print_error():
MYSQL * do_connect (char *host_name, char *user_name, char *password, char *db_name, unsigned int port_num, char *socket_name, unsigned int flags) { MYSQL *conn; /* pointer to connection handler */ conn = mysql_init (NULL); /* allocate, initialize connection handler */ if (conn == NULL) { print_error (NULL, "mysql_init() failed (probably out of memory)"); return (NULL); } #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 32200 /* 3.22 and up */ if (mysql_real_connect (conn, host_name, user_name, password, db_name, port_num, socket_name, flags) == NULL) { print_error (conn, "mysql_real_connect() failed"); return (NULL); } #else /* pre-3.22 */ if (mysql_real_connect (conn, host_name, user_name, password, port_num, socket_name, flags) == NULL) { print_error (conn, "mysql_real_connect() failed"); return (NULL); } if (db_name != NULL) /* simulate effect of db_name parameter */ { if (mysql_select_db (conn, db_name) != 0) { print_error (conn, "mysql_select_db() failed"); mysql_close (conn); return (NULL); } } #endif return (conn); /* connection is established */ }
Our main source file, client3.c, is like client2.c, but has all the embedded connect and disconnect code removed and replaced with calls to the wrapper functions. It looks like this:
/* client3.c */ #include <stdio.h> #include <mysql.h> #include "common.h" #define def_host_name NULL /* host to connect to (default = localhost) */ #define def_user_name NULL /* user name (default = your login name) */ #define def_password NULL /* password (default = none) */ #define def_port_num 0 /* use default port */ #define def_socket_name NULL /* use default socket name */ #define def_db_name NULL /* database to use (default = none) */ MYSQL *conn; /* pointer to connection handler */ int main (int argc, char *argv[]) { conn = do_connect (def_host_name, def_user_name, def_password, def_db_name, def_port_num, def_socket_name, 0); if (conn == NULL) exit (1); /* do the real work here */ do_disconnect (conn); exit (0); }