Main Page Content
Effectively Implementing Sql Phrasebooks
This article describes effective and portable implementations of
SQL Phrasebooks, using formatted constants as opposed to the currently accepted methodswhich call for XML Configuration files and dynamic general SQL Query generationroutines. Formatting is a very old and standardized method of makingstrings dynamic, and although there are currently numerous implementationsthat vary depending on language, the formatted strings we will use are completelyportable and standardized among all popular languages today.Why not use XML? Well, there is an argument (taken from
title="opens in new window">The Phrasebook Design Pattern) that mixing languages is bad. My feeling is that mixing SQL andXML, with library specific scalars intended to be parsed by the current application, is hardly better than mixing SQL with Perl, PHP, or any other programminglanguage. So our response is to isolate the SQL. This keeps us from havinga statement for any one table more than once, ever. We do thisby only using completely constant values where we don't need variations of thesame query, and otherwise using strict prototyping. This gives three distinctadvantages when writing Phrasebooks as libraries.The first is the obvious
speed advantage, resulting from not having to parse the XML file. The second isthat string formatting is extremely standard among today's modern languages, whichmeans our formatted queries are quite portable, and porting the Phrasebooksthemselves to different languages can be done with minimal effort.The third advantage is that it helps reduce repetition.
Reducing repetition is the main focus behind Phrasebook design. Repeatingyourself in the code is quite possibly the worst thing you can do, especiallywhen your repetition is large chunks of an alternate language within yourprogram. Why worry about repetition? Well, repetition makes maintaining codequite difficult, especially with SQL, since if you change your database, youhave to remember to search all your programs that access the specific tablesyou have changed, for whatever unanticipated reason you changed them, andchange the queries contained in them. If you have all of your queries in a singlelocation, and even then only once, this makes this process much less painful.When I first began discussing this in a public forum, the first question I
received was, "Well, then won't we need a bunch of extra sprintfs? I think thatis kind of annoying." Of course, my response was "Yes, but you don't have to seethem." The way we get around this is by writing our own extremely mild wrappersto whichever DBI we are using. The wrappers are quite simple; for instance,in PHP we simply make a class to wrap around the long, ugly mysql_* functionsthat PHP's mysql DBI provides:/* Simple DBI Wrapper */class EasyMySQL {var $Result;var $Link;function EasyMySQL ($User,$Pass,$Data,$Host = "localhost") {
$this->Link = mysql_connect($Host,$User,$Pass); mysql_select_db($Data,$this->Link);}function rqueryf () { $args = func_get_args(); return mysql_fetch_object(mysql_query ( call_user_func_array( 'sprintf', $args ), $this->Link));}function queryf () { $args = func_get_args(); return mysql_query(call_user_func_array( 'sprintf', $args ));}function rquery ($Query) { return mysql_fetch_object(mysql_query($Query, $this->Link));}function query ($Query) { return $this->Result = mysql_query($Query, $this->Link);}function next ($Handle="") { return mysql_fetch_object($Handle ? $Handle : $this->Result);}/* Destruction Function [ php has no destructor, call explicitly ] */
function close () { mysql_close($this->Link); return NULL;}}
This specific class may leave you with a few questions. It provides more
than just a simple wrapper around sprintf and the query at hand. It also providesslightly enhanced syntax for iterations, formatted and nonformattedfunctions, and an internal Link and Result reference. I used these because inthe particularprogram I wrote this class for, my module API left the globalmysql link available to extensions, without interference. Why did we fetch ourrows as objects, instead of arrays or associative arrays? That's a matter ofpersonal preference. You can use whichever methods you want.So, when does the Phrasebook come in? It comes in now. Personally, my
favorite way to design Phrasebooks in PHP is as a class that inherits my DBIwrapper. Here is a simple example:class Phrasebook extends EasyMySQL {function Q ($Query) { switch ($Query): case ACCOUNT:return <<<SQLSELECT id, name, address, city, state, zipFROM accountsWHERE id = %dSQL; case HITS:return <<<SQLSELECT sum(raw) as raw, sum(uni) as uni, dateFROM statsWHERE id = %dAND date >= %dAND date <= %dSQL; endswitch;}}
Here is an example of how we would use this:
/* Was like this... */mysql_connect($User, $Pass);mysql_select_db($Database);$result = mysql_query("SELECT id, name, address, city, state, zip
FROM accounts WHERE id = $id");while ($Data = mysql_fetch_object($result)) {...}/* Now it's like this */
$Database = new Phrasebook ($User, $Pass, $Database);$Database->queryf($Database->Q("ACCOUNTS"), $id);
while ($Data = $Database->next()) {...}
Obviously, the end result is a lot less code, especially for very large
queries, and looks much nicer, with all of the advantages discussed earlier.So what about Perl? Well, Perl actually has specific advantages in
Phrasebooks over PHP, mostly, that we can contain our SQL Phrasebook in a package with constant, null prototyped definitions (when possible) and thisincreases the speed of the Phrasebook access exponentially (not that it isn'talready very fast in the example above). One rule to keep in mind with Perlis that if you only use a table once, you should use a null prototype for thatquery. Why? Well if we're not doing anything dynamic, we don't need to passparameters. If we use a null prototype, the Perl compiler flags that methodas a constant, and compiles it as if it were a single quoted string into ourcode. So it has absolutely no runtime loss over single quoted, static stringformatting. That's pretty impressive, abstracting the code without slowing itdown whatsoever.Here is an example Phrasebook package in Perl that executes the same
queries as the above PHP example:package Phrase;Here is an example of how we would use this:sub ACCOUNT () {
<<SQL;SELECT id, name, address, city, state, zipFROM accountsWHERE id = %dSQL}sub HITS () {
<<SQL;SELECT sum(raw) as raw, sum(uni) as uni, dateFROM statsWHERE id = %dAND date >= %dAND date <= %dSQL}
# What was this$sth = $dbh->prepare("SELECT id, name, address, city, state, zip FROM accounts WHERE id = $id");This is obviously not only more pleasant to have in your code if you're not currently worrying about the SQL, but also has all the advantages mentionedearlier.# Now becomes this
$sth = $dbh->prepare(sprintf(Phrase->ACCOUNT, $id));
As you can see here, we didn't exercise the database abstraction as in the
PHP example mentioned, because the robustness of Perl's DBI doesn'treally require it. If you wanted, however, to rid your code of theextra sprintfs, considering that Perl's DBI already offers the other conveniences ourPHP wrapper had, write a simple object to inherit a DBI object and callprepare with a sprintf. Here is an extremely simple example:package FormatDBI;use DBI;use Data::Dumper;@FormatDBI::ISA = qw(DBI);
package FormatDBI::db;
@FormatDBI::db::ISA = qw(DBI::db);sub preparef {
return $_[0]->prepare(sprintf($_[1], @_[2..$#_]));}package FormatDBI::st;
@FormatDBI::st::ISA = qw(DBI::st);package main;
use strict;use warnings FATAL => qw( all );use Phrase;
my $dsn = "DBI:mysql:database=aff;host=localhost";
my $dbh = FormatDBI->connect($dsn, "username", "password");my ($Data,$sth);
$sth = $dbh->preparef(Phrase->ACCOUNT, 40);
...etc...
Hope you find this useful.