The Specialized SQL Structure (SSQLS) feature lets you easily define C++ structures that match the form of your SQL tables. Because of the extra functionality that this feature builds into these structures, MySQL++ can populate them automatically when retrieving data from the database; with queries returning many records, you can ask MySQL++ to populate an STL container of your SSQLS records with the results. When updating the database, MySQL++ can use SSQLS structures to match existing data, and it can insert SSQLS structures directly into the database.
You define an SSQLS using one of several macros. (These are in the file custom.h, and in the file that it includes, custom-macros.h.) There are a bunch of different macros, for different purposes. The following sections will discuss each macro type separately, beginning with the easiest and most generally useful.
This is the most basic sort of SSQLS declaration:
sql_create_5(stock, 1, 0, string, item, int, num, double, weight, double, price, mysqlpp::Date, date)
This creates a C++ structure called
stock
containing five member
variables (item
, num
,
weight
, price
and
date
), along with some constructors and
other member functions useful with MySQL++.
One of the generated constructors takes a reference to a mysqlpp::Row object, allowing you to easily populate a vector of stocks like so:
vector<stock> result; query.storein(result);
That's all there is to it. The only requirements are that the table structure be compatible with the SSQLS's member variables, and that the fields are in the same order.
The general format of this set of macros is:
sql_create_#(NAME, COMPCOUNT, SETCOUNT, TYPE1, ITEM1, ... TYPE#, ITEM#)
Where # is the number of member variables,
NAME
is the name of the structure you
wish to create, TYPEx
is the type of
a member variable, and ITEMx
is that
variable's name.
The COMPCOUNT
and
SETCOUNT
arguments are described in
the next section.
sql_create_
x
adds member functions and operators to each SSQLS that
allow you to compare one SSQLS instance to another. These
functions compare the first COMPCOUNT
fields in the structure. In the example above,
COMPCOUNT
is 1, so only the
item
field will be checked when comparing
two stock
structures.
This feature works best when your table's "key"
fields are the first ones in the table schema and you set
COMPCOUNT
equal to the number of key
fields. That way, a check for equality between two SSQLS
structures in your C++ code will give the same results as a
check for equality in SQL.
COMPCOUNT
must
be at least 1. The current implementation of
sql_create_
x cannot
create an SSQLS without comparison member functions.
Because our stock
structure
is less-than-comparable, you can use it in STL algorithms
and containers that require this, such as STL's associative
containers:
std::set<stock> result; query.storein(result); cout << result.lower_bound(stock("Hamburger"))->item << endl;
This will print the first item in the result set that begins with "Hamburger".
The third parameter to
sql_create_
x is
SETCOUNT
. If this is nonzero, it adds an
initialization constructor and a set()
member function taking the given number of arguments,
for setting the first N fields of the
structure. For example, you could change the above example
like so:
sql_create_5(stock, 1, 2, string, item, int, num, double, weight, double, price, mysqlpp::Date, date) stock foo("Hotdog", 52);
In addition to this 2-parameter constructor, this version
of the stock
SSQLS will have a similar
2-parameter set()
member function.
The COMPCOUNT
and
SETCOUNT
values cannot be equal. If
they are, the macro will generate two initialization
constructors with identical parameter lists, which
is illegal in C++. Why does this happen? It's often
convenient to be able to say something like x
== stock("Hotdog")
. This requires that there
be a constructor taking COMPCOUNT
arguments to create the temporary stock
instance used in the comparison. It is easy to work around
this limitation. Using our stock
example structure, if you wanted comparisons to consider all
5 fields and also be able to initialize all 5 fields at once,
you would pass 5 for COMPCOUNT
and
0 for SETCOUNT
. You would still get
a 5-parameter initialization constructor and a 5-parameter
set()
function.
It's not necessary to retrieve an entire table
row using SSQLS, as long as the fields you want are
grouped together at the start of the table schema.
examples/custom6.cpp
illustrates
this:
#include "util.h" #include <mysql++.h> #include <custom.h> #include <iostream> #include <iomanip> #include <vector> using namespace std; // To store a subset of a row, we define an SSQLS containing just the // fields that we will store. There are complications here that are // covered in the user manual. sql_create_1(stock_subset, 1, 0, string, item) int main(int argc, char *argv[]) { try { // Establish the connection to the database server. mysqlpp::Connection con(mysqlpp::use_exceptions); if (!connect_to_db(argc, argv, con)) { return 1; } // Retrieve a subset of the stock table, and store the data in // a vector of 'stock_subset' SSQLS structures. mysqlpp::Query query = con.query(); query << "select item from stock"; vector<stock_subset> res; query.storein(res); // Display the result set cout << "We have:" << endl; vector<stock_subset>::iterator it; for (it = res.begin(); it != res.end(); ++it) { cout << '\t' << it->item << endl; } } catch (const mysqlpp::BadQuery& er) { // Handle any query errors cerr << "Query error: " << er.what() << endl; return -1; } catch (const mysqlpp::BadConversion& er) { // Handle bad conversions; e.g. type mismatch populating 'stock' cerr << "Conversion error: " << er.what() << endl << "\tretrieved data size: " << er.retrieved << ", actual size: " << er.actual_size << endl; return -1; } catch (const mysqlpp::Exception& er) { // Catch-all for any other MySQL++ exceptions cerr << "Error: " << er.what() << endl; return -1; } return 0; }
(See the simple1 example in the Tutorial for another way to accomplish the same thing.)
This example illustrates an important point: you
could not use the 5-member stock
structure in this example. The reason is, when you assign a
Row
object to an SSQLS, the function
that copies the row's data into the structure expects to
see as many fields in the row as are in the SSQLS. Your
program will crash when the code tries to access fields
beyond those that exist in the Row
object. The converse is not true, however: if you change
the SELECT
statement above so that
it retrieves more than one column, the code will still
work, because the extra fields in each row will simply be
ignored.
Realize that the second and third parameters to
sql_create_1
can't be anything other
than 1 and 0, respectively. As discussed above, the second
parameter must be at least 1, but since there is only one
field in the structure, it cannot be higher than 1. Since
the third parameter cannot be equal to the second, only 0
works there.
Up to this point, we haven't been using all of the features in the SSQLS structures we've been generating. We could have used the sql_create_basic_* macros instead, which would have worked just as well for what we've seen so far, and the generated code would have been smaller.
Why is it worth ignoring the "basic" variants of these macros, then? Consider this:
query.insert(s);
This does exactly what you think it does: it inserts
's' into the database. This is possible because a standard
SSQLS has functions that the query object can call to get
the list of fields and such, which it uses to build an
insert query. query::update()
and
query::replace()
also rely on this
SSQLS feature. A basic SSQLS lacks these functions.
Another feature of standard SSQLSes you might find a use for is changing the table name used in queries. By default, the table in the MySQL database is assumed to have the same name as the SSQLS structure type. But if this is inconvenient, you can globally change the table name used in queries like this:
stock::table() = "MyStockData";
Continuing the discussion in the previous section,
there is a further set of methods that the non-"basic"
versions of the sql_create macros define
for each SSQLS. These methods are mostly for use within the
library, but some of them are useful enough that you might
want to harness them for your own ends. Here is some pseudocode
showing how the most useful of these methods would be defined
for the stock structure used in all
the custom*.cpp
examples:
// Basic form template <class Manip> stock_value_list<Manip> value_list(cchar *d = ",", Manip m = mysqlpp::quote) const; template <class Manip> stock_field_list<Manip> field_list(cchar *d = ",", Manip m = mysqlpp::do_nothing) const; template <class Manip> stock_equal_list<Manip> equal_list(cchar *d = ",", cchar *e = " = ", Manip m = mysqlpp::quote) const; // Boolean argument form template <class Manip> stock_cus_value_list<Manip> value_list([cchar *d, [Manip m,] ] bool i1, bool i2 = false, ... , bool i5 = false) const; // List form template <class Manip> stock_cus_value_list<Manip> value_list([cchar *d, [Manip m,] ] stock_enum i1, stock_enum i2 = stock_NULL, ..., stock_enum i5 = stock_NULL) const; // Vector form template <class Manip> stock_cus_value_list<Manip> value_list([cchar *d, [Manip m,] ] vector<bool> *i) const; ...Plus the obvious equivalents for field_list() and equal_list()
Rather than try to learn what all of these methods do at once, let's ease into the subject. Consider this code:
stock s("Dinner Rolls", 75, 0.95, 0.97, "1998-05-25"); cout << "Value list: " << s.value_list() << endl; cout << "Field list: " << s.field_list() << endl; cout << "Equal list: " << s.equal_list() << endl;
That would produce something like:
Value list: 'Dinner Rolls',75,0.95,0.97,'1998-05-25' Field list: item,num,weight,price,date Equal list: item = 'Dinner Rolls',num = 75,weight = 0.95, price = 0.97,date = '1998-05-25'
That is, a "value list" is a list of data member values within a particular SSQLS instance, a "field list" is a list of the fields (columns) within that SSQLS, and an "equal list" is a list in the form of an SQL equals clause.
Just knowing that much, it shouldn't surprise you
to learn that Query::insert()
is
implemented more or less like this:
*this << "INSERT INTO " << v.table() << " (" << v.field_list() << ") VALUES (" << v.value_list() << ")";
where 'v' is the SSQLS you're asking the Query object to insert into the database.
Now let's look at a complete example,
which uses one of the more complicated forms of
equal_list()
. This example
builds a query with fewer hard-coded strings than
the most obvious technique requires, which makes
it more robust in the face of change. Here is
examples/custom5.cpp
:
#include "stock.h" #include "util.h" #include <iostream> #include <vector> using namespace std; int main(int argc, char *argv[]) { try { mysqlpp::Connection con(mysqlpp::use_exceptions); if (!connect_to_db(argc, argv, con)) { return 1; } // Get all the rows in the stock table. mysqlpp::Query query = con.query(); query << "select * from stock"; vector<stock> res; query.storein(res); if (res.size() > 0) { // Build a select query using the data from the first row // returned by our previous query. query.reset(); query << "select * from stock where " << res[0].equal_list(" and ", stock_weight, stock_price); // Display the finished query. cout << "Custom query:\n" << query.preview() << endl; } } catch (const mysqlpp::BadQuery& er) { // Handle any query errors cerr << "Query error: " << er.what() << endl; return -1; } catch (const mysqlpp::BadConversion& er) { // Handle bad conversions cerr << "Conversion error: " << er.what() << endl << "\tretrieved data size: " << er.retrieved << ", actual size: " << er.actual_size << endl; return -1; } catch (const mysqlpp::Exception& er) { // Catch-all for any other MySQL++ exceptions cerr << "Error: " << er.what() << endl; return -1; } return 0; }
This example uses the list form of
equal_list()
. The
arguments stock_weight
and stock_price
are enum
values equal to the position of these columns
within the stock table.
sql_create_x generates
this enum for you automatically.
The boolean argument form of that
equal_list()
call would look like
this:
query << "select * from stock where " << res[0].equal_list(" and ", false, false, true, true, false);
It's a little more verbose, as you can see. And if you want to get really complicated, use the vector form:
vector<bool> v(5, false); v[stock_weight] = true; v[stock_price] = true; query << "select * from stock where " << res[0].equal_list(" and ", v);
This form makes the most sense if you are building many other queries, and so can re-use that vector object.
Many of these methods accept manipulators and custom delimiters. The defaults are suitable for building SQL queries, but if you're using these methods in a different context, you may need to override these defaults. For instance, you could use these methods to dump data to a text file using different delimiters and quoting rules than SQL.
At this point, we've seen all the major aspects of the SSQLS feature. The final sections of this chapter look at some of the peripheral aspects.
If for some reason you want your SSQLS data members to have different names than used in the MySQL database, you can do so like this:
sql_create_c_names_5(stock, 1, 5, string, item, "item", int, num, "quantity", double, weight, "weight", double, price, "price" mysqlpp::Date, date, "shipment")
If you want your SSQLS to have its data members in a different order from those in the MySQL table, you can do it like this:
sql_create_c_order_5(stock, 2, 5, mysqlpp::Date, date, 5, double, price, 4, string, item, 1, int, num, 2, double, weight, 3)
You can combine the custom names and custom ordering like this:
sql_create_complete_5(stock, 2, 5, mysqlpp::date, date, "shipment", 5, double, price, "price", 4, string, item, "item", 1, int, num, "quantity", 2, double, weight, "weight", 3)
All three of these macro types have "basic" variants that work the same way. Again, basic SSQLSes lack the features necessary for automatic insert, update and replace query creation.
If you ever need to see the code that a given
SSQLS declaration expands out to, use the utility
doc/ssqls-pretty
, like so:
doc/ssqls-pretty < myprog.cpp |less
This Perl script locates the first SSQLS declaration
in that file, then uses the C++ preprocessor to expand that
macro. (The script assumes that your system's preprocessor
is called cpp
, and that its command line
interface follows Unix conventions.)
If you run it from the top MySQL++ directory, as shown
above, it will use the header files in the distribution's
lib
subdirectory. Otherwise, it
assumes the MySQL++ headers are in their default location,
/usr/include/mysql++
. If you want to
use headers in some other location, you'll need to change
the directory name in -I flag at the top
of the script.
The SSQLS headers — custom.h and custom-macros.h
— are automatically generated by the Perl script
custom.pl
. Although it is possible to
change this script to get additional functionality, it's
usually better to do that through inheritance.
A regular user may find it helpful to change the the
limit on the maximum number of SSQLS data members allowed.
It's 25 out of the box. A smaller value may speed up
compile time, or you may require a higher value because
you have more complex tables than that. Simply change the
max_data_members
variable at the top of
custom.pl
and say make.
The limit for Visual C++ is 31, according to one report. There
doesn't seem to be a practical limit with GCC 3.3 at least:
I set the limit to 100 and the only thing that happened
is that custom-macros.h went from 1.3 MB to 18 MB and the
build time for examples/custom.*
got a
lot longer.
It takes special care to use SSQLS with BLOB
columns. It's safest to declare the SSQLS field as of
type mysqlpp::sql_blob
. This
is currently a typedef alias for ColData, which is the form the
data is in just before the SSQLS mechanism populates the
structure. Thus, when the data is copied from the internal
MySQL++ data structures into your SSQLS, you get a direct
copy of the ColData
object's contents,
without interference.
Because ColData
derives
from std::string
and C++
strings handle binary data just fine, you might think
you can use std::string
instead
of sql_blob
, but the current
design of ColData
converts to
std::string
via a C string. As a
result, the BLOB data is truncated at the first embedded
null character during population of the SSQLS. There's
no way to fix that without completely redesigning either
ColData
or the SSQLS mechanism.
The sql_blob
typedef may be
changed to alias a different type in the future, so using it
instead of ColData
ensures that your
code tracks these library changes automatically. Besides,
ColData
is only intended to be an
internal mechanism within MySQL++. The only reason the layering
is so thin here is because it's the only way to prevent BLOB
data from being corrupted while staying within the current
library interface design.
You can see this technique in action in the
cgi_jpeg
example:
#include <mysql++.h> #include <custom.h> using namespace std; using namespace mysqlpp; #define IMG_DATABASE "mysql_cpp_data" #define IMG_HOST "localhost" #define IMG_USER "root" #define IMG_PASSWORD "nunyabinness" sql_create_2(images, 1, 2, mysqlpp::sql_int_unsigned, id, mysqlpp::sql_blob, data) int main() { unsigned int img_id = 0; char* cgi_query = getenv("QUERY_STRING"); if (cgi_query) { if ((strlen(cgi_query) < 4) || memcmp(cgi_query, "id=", 3)) { cout << "Content-type: text/plain" << endl << endl; cout << "ERROR: Bad query string" << endl; return 1; } else { img_id = atoi(cgi_query + 3); } } else { cerr << "Put this program into a web server's cgi-bin " "directory, then" << endl; cerr << "invoke it with a URL like this:" << endl; cerr << endl; cerr << " http://server.name.com/cgi-bin/cgi_jpeg?id=2" << endl; cerr << endl; cerr << "This will retrieve the image with ID 2." << endl; cerr << endl; cerr << "You will probably have to change some of the #defines " "at the top of" << endl; cerr << "examples/cgi_jpeg.cpp to allow the lookup to work." << endl; return 1; } Connection con(use_exceptions); try { con.connect(IMG_DATABASE, IMG_HOST, IMG_USER, IMG_PASSWORD); Query query = con.query(); query << "SELECT * FROM images WHERE id = " << img_id; ResUse res = query.use(); if (res) { images img = res.fetch_row(); cout << "Content-type: image/jpeg" << endl; cout << "Content-length: " << img.data.length() << "\n\n"; cout << img.data; } else { cout << "Content-type: text/plain" << endl << endl; cout << "ERROR: No such image with ID " << img_id << endl; } } catch (const BadQuery& er) { // Handle any query errors cout << "Content-type: text/plain" << endl << endl; cout << "QUERY ERROR: " << er.what() << endl; return 1; } catch (const Exception& er) { // Catch-all for any other MySQL++ exceptions cout << "Content-type: text/plain" << endl << endl; cout << "GENERAL ERROR: " << er.what() << endl; return 1; } return 0; }