mwp has asked for the wisdom of the Perl Monks concerning the following question:

BACKGROUND

DBI has a wonderful call called bind_columns, which takes a list of references to variables and assigns those variables to columns. This is A) the most efficient data retrieval method, and B) kinda neat and very simple to create output.

#!/usr/bin/perl use DBI; use warnings 'all'; use strict; my $dbh = DBI->connect(...) || die("could not create handler: ", $DBI::errstr); my $sth = $dbh->prepare(q{ SELECT Name, Price, Description FROM Products ORDER BY Name, Price }) || die("could not prepare product listing: ", $dbh->errstr); $sth->execute || die("could not execute product listing: ", $sth->errstr); my($name, $price, $description); $sth->bind_columns(\($name, $price, $description)); while($sth->fetch) { print "<tr><td>$name</td><td>$price</td></tr>\n"; print "<tr><td colspan=\"2\">$description</td></tr>\n"; }

(DISCLAIMER: Yes, I use CGI.pm normally, Yes, I know about RaiseError, and Yes, I know that $DBI::errstr, $dbh->errstr, and $sth->errstr are all the same thing :-)

At this point, all is well in the garden of good and evil.

MIKE STEPS UP TO BAT

Somewhere along the lines, I became good 'n lazy. I decided that I wanted a sleeker, more elegant method than declaring my variables one at a time, and then binding my columns (more or less) one at a time. Why not use a loop, or map, or some such? Thus the following code was born unto my script:

# ... assume up to $sth->execute $sth->bind_columns( map { \${$_} } @{ $sth->{NAME_lc} } ) || die("unable to bind columns: ", $sth->errstr); # ... assume same while-loop and output as before

This performed wonderfully and did exactly what I asked it to. I could query as many columns as I darn well pleased and bind them to variables, with a minimal amount of typing and no variable spelling errors. There was just one teeny tiny little problem... it would not run under `strict', unless I declared all my variables with `my' beforehand.

So, naturally, I made sure the rest of my code worked, then turned strict off. >:)

A CHANGE OF HEART

Several weeks (months? years?) later, I came to my senses and decided that this code really needed to run with strict on after all. Not willing to do a `no strict qw(vars)', because in my eyes that's as sinful as `no strict', I began wresting with scoping variables who's names were contained in other variables. I've gotten it to work (mostly) with symbolic references, but `no strict qw(refs)' is, again, as sinful as `no strict'.

It is at this point that I appeal to my fellow monks. At this point, having already gotten it to compile using symbolic references, I am interested primarily in a learning experience.

Is there any way to do this with hard references?

Below is my current code for you to check out. Any help is greatly appreciated.

Alakaboo


#!/usr/bin/perl use DBI; use warnings 'all'; use strict; no strict 'refs'; # Ni! my $dbh = DBI->connect(...) || die("could not create handler: ", $DBI::errstr); my $sth = $dbh->prepare(q{ SELECT Name, Price, Description FROM Products ORDER BY Name, Price }) || die("could not prepare product listing: ", $dbh->errstr); $sth->execute || die("could not execute product listing: ", $sth->errstr); $sth->bind_columns( map { \${"main::$_"} } # `my' isn't going to cut it @{ $sth->{NAME_lc} } # who knows where this is going to run? ) || die("unable to bind columns: ", $sth->errstr); while($sth->fetch) { print "<tr><td>$name</td><td>$price</td></tr>\n"; print "<tr><td colspan=\"2\">$description</td></tr>\n"; }

Replies are listed 'Best First'.
You can't get there from here....
by chip (Curate) on Aug 11, 2000 at 05:32 UTC
    The whole point of 'use strict' and hard refs is to get away from mixing the realm of strings and the realm of variable names. Here there be dragons.

    Your code is a marvelous example of the danger of such approaches. Your code could easily break if someone were to add to the database a column that has the same name as an existing variable in your program!

    I agree with the first replier: If spelling out your variable names is too tedious (and it probably would be too tedious for me), use hashes. That's what they're for. And they can have nice short names.

        -- Chip Salzenberg, Free-Floating Agent of Chaos

      The problem that I have with hashes is that you go through all of the work in strict to make sure you cannot mistype your variable names and then you throw it all away because there is no check for misspelled hash entries.

      I have used 2 solutions to get around that in the past. One is Class::Struct, the other is pseudo-hashes. Each has their gotchas. Now I know you had some bad luck with pseudo-hashes in the Topaz effort, which leads me to a couple of questions.

      First of all what do you suggest to this problem? (Want to use a hash or reference to one, but with only validated keys allowed.)

      And secondly is it likely that stuff I did with pseudo-hashes is going to be low priority (ie in the 5% acceptable breakage) for Perl 5.6? (I know it is early for this question, I just would like an educated guess.)

      Thanks,
      Ben

        Plan A: Put a unique prefix on the database column names, turning off strict temporarily:
        { no strict 'refs'; $sth->bind_columns( map { \${"c_$_"} @{ $sth->{NAMES_lc} } ) }
        Plan B: If you want the advantages of variables and the advantages of hashes, there is a way, but it will cost you a little in speed. There is one language construct that can see variables and can be built with strings, even with strict enabled: eval.
        local $, = ',$'; # saves on the joins eval "\$sth->bind_columns(\\( \$@{$sth->{NAMES_lc}} ))"
        However, now you're actually letting column names into the Perl parser! A saying about frying pans and fires comes to mind.... I'd much rather disable strict than make wide use of eval STRING.

        As for pseudohashes: I think they're great. The only thing about their implementation in Perl 5 that ate me alive was the fact that they weren't a distinct data type, but rather a mere access method, usable with any array at any time. If they had to be declared somehow at their creation, hey, I'd have been praising them in my Topaz talk.

            -- Chip Salzenberg, Free-Floating Agent of Chaos

        As a runtime solution, create a tied hash that supported only a limited set of keys, throwing an exception if a distinct key was used.

        -- Randal L. Schwartz, Perl hacker

      > I agree with the first replier: If spelling out your variable names is too tedious (and it probably would be too tedious for me), use hashes. That's what they're for. And they can have nice short names.

      Too slow. :-)

      > Your code is a marvelous example of the danger of such approaches. Your code could easily break if someone were to add to the database a column that has the same name as an existing variable in your program!

      I appreciate the wake-up call, but consider this:

      my($field1, $field2, $field3); $sth->prepare("SELECT Field1, Field2, Field3 ..."); $sth->bind_columns(\($field1, $field2, $field3)); while($sth->fetch) { print "$field1\t$field2\t$field3\n"; }

      Versus:

      my($field1, $field2, $field3); $sth->prepare("SELECT Field1, Field2, Field3 ..."); $sth->bind_columns(map { \$$_ } @{ $sth->{NAME_lc} }); while($sth->fetch) { print "$field1\t$field2\t$field3\n"; }

      In both situations, our would-be saboteur has to add a field variable to the my() list, a field name to the SELECT list, and then add the output command. If that doesn't hammer the point home that the variable name is reserved, I doubt one more step (adding the same field to the list of binded variables) is going to make much of a difference.

      I guess the point I'm trying to make here is that there's more than one way to do it... and there's more than one way to break it. In-line comments and self-documenting code (ie, I did not use a SELECT *, the field list was explicitly defined) are really our best tools in fighting "ignorance". Any script can be easily broken in any number of ways.

      Besides the fact, my proposal to use a separate package name gets around this problem. :-) Other than, of course, someone using the same package name. But I think it's safe to assume that a programmer experienced and confident enough to be creating packages like that will at least take a look around to make sure the package is being used. This is where documentation and commenting comes in handy!

      Thanks for your insight.

      Alakaboo

Re: References of Eternal Peril
by chromatic (Archbishop) on Aug 11, 2000 at 05:01 UTC
    You could probably, with a bit of work, use a hash and a list of values. Here's some relatively untested, unbeautiful stuff that makes me more likely to stick with declaring the variables in advance:
    my %columns; # stuff here $sth->bind_columns( map { \${ $columns{$_} } @{ $sth->{NAME_lc} } ) || die("unable to bind columns: ", $sth->errstr); # dereference somehow, may not be correct while($sth->fetch) { print "<tr><td>$name</td><td>$columns{price}</td></tr>\n"; print "<tr><td colspan=\"2\">$columns{description}</td></tr>\n"; }
    Update: Yeah, there's really no big benefit to doing it this way. :)

      If I were going to use that approach, I would almost certainly drop the bind_columns altogether and do:

      while(my $columns = $sth->fetchrow_hashref) { print "<tr><td>$columns->{name}</td><td>$columns->{price}</td></tr +>\n"; print "<tr><td colspan=\"2\">$columns->{description}</td></tr>\n"; }

      But I'm trying to avoid that. Primarily, I asked this question in hopes of learning a bit more about hard references.

      Thanks for your help. :-)

      Alakaboo

RE: References of Eternal Peril
by mikfire (Deacon) on Aug 11, 2000 at 07:32 UTC
    I can't say I have any sympathy. The original code, while somewhat annoying to type, is terribly clear. I understand at a glance what it does. This funky loop buys what? A severe headache when I have to fix it, by my estimates.

    If the typing is the problem, consider ( in vi ) yanking to end of line ( y$<ENTER> in command mode ) and pasting ( p ). I don't use emacs, but I will bet lots of money it can do something very similar. That is even faster than typing in the loop and retains clarity.

    Probably over simplistic and not very monkish to shrink from the weird loop. But I like simplicity at times, and I think this is one of them.

    mikfire

      > This funky loop buys what? A severe headache when I have to fix it, by my estimates.

      • Use of the speedy binded columns feature in DBI
      • A quick & dirty solution for a poor speller/typist
      • A method that will hopefully never break to be fixed. Hah!

      > Probably over simplistic and not very monkish to shrink from the weird loop. But I like simplicity at times, and I think this is one of them.

      On the contrary, I think it is very "monkish" to go the simple route. I'm inclined to agree 100% in that understand-at-a-glance is best. But I became lazy, and you know what that means.

      > ...consider ( in vi ) yanking to end of line ( y$ in command mode ) and pasting ( p )...

      Hmm a little sarcasm there? Duly noted for future flamebait... mua! ;-)

      Thanks for your comments, mik.

      Alakaboo

Re: References of Eternal Peril
by prodevel (Scribe) on Aug 11, 2000 at 09:24 UTC
    This is beautiful reading. I've been interested in better structs for my abysmal CGI scripting for some time now. All I wanted to be able to do is assign my text item names directly to the same short scalar of a corresponding name, simply identified. Of course, I'm ready to take any intelligent variation thereof, and it seems I have it.

    Bravo to ya alakaboo, accolades to all that responded.

      You're welcome. :-)

      Just please, please pay heed to all the things mentioned here:

      • Garbaging the main package with variables, that may 'accidentally' be overwritten sometime
      • Lack of clarity for someone unfamiliar with your code
      • Why we have 'strict' to begin with: to avoid this kind of problem
      • Any others I can think of between now and later... *grin*

      And keep bringing those issues to us if you're ever in doubt about something!

      Alakaboo

RE: References of Eternal Peril (while we're on the subject)
by steveAZ98 (Monk) on Aug 11, 2000 at 17:40 UTC
    I've been wondering if the method below is terribly inefficient or if there is a better way to do the same thing. I'm using this method to return the results of a select statement to object attributes which can then latter be accessed. I'm thinking that this may be a bit of a memory hog, but the convenience is nice. Just looking for some thoughts and ideas on this.
    Thanks
    sub select { my $s = shift; my $sql = shift; my $sth = $s->dbh->prepare( $sql ) || $s->write_log("Can't prepare select ($sql): ".$s->dbh->errstr) +; $sth->execute() || $s->write_log("Can't execute select ($sql): ".$sth->errstr); $s->fields = $sth->{NAME}; my $x = 0; my @ar = (); while ( my $hr = $sth->fetchrow_hashref() ) { $ar[$x++] = $hr; } $s->rows = $x; $s->ar = \@ar; $sth->finish(); }
      You can simplify most of this code:
      my $x = 0; my @ar = (); while ( my $hr = $sth->fetchrow_hashref() ) { $ar[$x++] = $hr; } $s->rows = $x; $s->ar = \@ar;
      to this:
      my @ar = (); while (my $hr = $sth->fetchrow_hashref() ) { push @ar, $hr; } $s->ar = \@ar; $s->rows = @ar;
      But I think there's a "fetchall_hashref" or something like that that could do this all in one fell swoop. Durn. Not enough window space on this speaker-lounge-area dumb PC to go looking up "man DBI". Sorry.

      -- Randal L. Schwartz, Perl hacker

        But I think there's a "fetchall_hashref" or something like that that could do this all in one fell swoop. Durn. Not enough window space on this speaker-lounge-area dumb PC to go looking up "man DBI". Sorry.

        Indeed there is:

        my @ar = (); while (my $hr = $sth->fetchrow_hashref() ) { push @ar, $hr; } $s->ar = \@ar; $s->rows = @ar;
        ... can be written ...
        $s->ar = $sth->fetchall_arrayref({}); $s->rows = @{$s->ar};

        On a side note, fetchall_arrayref() is one of my favorite methods because HTML::Template templates take an arrayref of hashrefs as a parameter for loops. You can output a table really easily like so:

        # Assume $sth is a valid statement handle my $params = $sth->fetchall_arrayref({}); my $template = new HTML::Template (filename => 'file.name'); $template->param(TABLENAME => $params); print $template->output();
        The template file looks like this:
        <TABLE> <TMPL_LOOP NAME=TABLENAME> <TR><TD><TMPL_VAR NAME=COLUMN1></TD><TD><TMPL_VAR NAME=COLUMN2></TD> +</TR> </TMPL_LOOP> </TABLE>
        The stuff between the <TMPL_LOOP> tags outputs for each row returned by the query, and it only takes two lines of perl to get the rows from the db to the template.

        -Matt