How can I re-use WHERE clause logic with DBI? - mysql

Disclaimer: first time I've used DBI.
I have a MySQL table with a lot of indexed fields (f1, f2, f3, etc) that are used to generate WHERE clauses by long-running processes that iterate over chunks of the database performing various cleaning and testing operations.
The current version of this code works something like this:
sub get_list_of_ids() {
my ($value1, $value2, $value3...) = #_;
my $stmt = 'SELECT * FROM files WHERE 1';
my #args;
if (defined($value1)) {
$stmt .= ' AND f1 = ?';
push(#args, $value1);
}
# Repeat for all the different fields and values
my $select_sth = $dbh->prepare($stmt) or die $dbh->errstr;
$select_sth->execute(#args) or die $select_sth->errstr;
my #result;
while (my $array = $select_sth->fetch) {
push(#result, $$array[0]);
}
return \#result;
}
sub function_A() {
my ($value1, $value2, $value3...) = #_;
my $id_aref = get_list_of_ids($value1, $value2, $value3...);
foreach my $id (#$id_aref) {
# Do something with $id
# And something else with $id
}
}
sub function_B() {
my ($value1, $value2, $value3...) = #_;
my $id_aref = get_list_of_ids($value1, $value2, $value3...);
foreach my $id (#$id_aref) {
# Do something different with $id
# Maybe even delete the row
}
}
Anyway, I'm about to dump an awful lot more rows in the database, and am well aware that the code above wont scale up. I can think of several ways to fix it based on other languages. What is the best way to handle it in Perl?
Key points to note are that the logic in get_list_of_ids() is too long to replicate in each function; and that the operations on the selected rows are very varied.
Thanks in advance.

I presume by "scale up" you mean in maintenance terms rather than performance.
The key change to your code is to pass in your arguments as column/value pairs rather than a list of values with an assumed set of columns. This will allow your code to handle any new columns you might add.
DBI->selectcol_arrayref is both convenient and a bit faster, being written in C.
If you turn on RaiseError in your connect call, DBI will throw an exception on errors rather than having to write or die ... all the time. You should do that.
Finally, since we're writing SQL from possibly untrusted user input, I've taken care to escape the column name.
The rest is explained in this Etherpad, you can watch your code be transformed step by step.
sub get_ids {
my %search = #_;
my $sql = 'SELECT id FROM files';
if( keys %search ) {
$sql .= " WHERE ";
$sql .= join " AND ", map { "$_ = ?" }
map { $dbh->quote_identifier($_) }
keys %search;
}
return $dbh->selectcol_arrayref($sql, undef, values %search);
}
my $ids = get_ids( foo => 42, bar => 23 );
If you expect get_ids to return a huge list, too much to keep in memory, then instead of pulling out the whole array and storing it in memory you can return the statement handle and iterate with that.
sub get_ids {
my %search = #_;
my $sql = 'SELECT id FROM files';
if( keys %search ) {
$sql .= " WHERE ";
$sql .= join " AND ", map { "$_ = ?" }
map { $dbh->quote_identifier($_) }
keys %search;
}
my $sth = $dbh->prepare($sql);
$sth->execute(values %search);
return $sth;
}
my $sth = get_ids( foo => 42, bar => 23 );
while( my $id = $sth->fetch ) {
...
}
You can combine both approaches by returning a list of IDs in array context, or a statement handle in scalar.
sub get_ids {
my %search = #_;
my $sql = 'SELECT id FROM files';
if( keys %search ) {
$sql .= " WHERE ";
$sql .= join " AND ", map { "$_ = ?" }
map { $dbh->quote_identifier($_) }
keys %search;
}
# Convenient for small lists.
if( wantarray ) {
my $ids = $dbh->selectcol_arrayref($sql, undef, values %search);
return #$ids;
}
# Efficient for large ones.
else {
my $sth = $dbh->prepare($sql);
$sth->execute(values %search);
return $sth;
}
}
my $sth = get_ids( foo => 42, bar => 23 );
while( my $id = $sth->fetch ) {
...
}
my #ids = get_ids( baz => 99 );
Eventually you will want to stop hand coding SQL and use an Object Relation Mapper (ORM) such as DBIx::Class. One of the major advantages of an ORM is it is very flexible and can do the above for you. DBIx::Class can return a simple list of results, or very powerful iterator. The iterator is lazy, it will not perform the query until you start fetching rows, allowing you to change the query as needed without having to complicate your fetch routine.
my $ids = get_ids( foo => 23, bar => 42 );
$ids->rows(20)->all; # equivalent to adding LIMIT 20

Related

Processing results from fatfree DB SQL Mapper for json encoding

I'm having trouble processing the returned results from a DB SQL Mapper into a recognizable json encoded array.
function apiCheckSupplyId() {
/*refer to the model Xrefs*/
$supply_id = $this->f3->get('GET.supply_id');
$xref = new Xrefs($this->tongpodb);
$supply = $xref->getBySupplyId( $supply_id );
if ( count( $supply ) == 0 ) {
$this->logger->write('no xref found for supply_id=' .$supply_id);
$supply = array( array('id'=>0) );
echo json_encode( $supply );
} else {
$json = array();
foreach ($supply as $row){
$item = array();
foreach($row as $key => $value){
$item[$key] = $value;
}
array_push($json, $item);
}
$this->logger->write('xref found for supply_id=' .$supply_id.json_encode( $json ) );
echo json_encode( $json );
}
}
This is the method I am using but it seems very clunky to me. Is there a better way?
Assuming the getBySupplyId returns an array of Xref mappers, you could simplify the whole thing like this:
function apiCheckSupplyId() {
$supply_id = $this->f3->get('GET.supply_id');
$xref = new Xrefs($this->tongpodb);
$xrefs = $xref->getBySupplyId($supply_id);
echo json_encode(array_map([$xref,'cast'],$xrefs));
$this->logger->write(sprintf('%d xrefs found for supply_id=%d',count($xrefs),$supply_id));
}
Explanation:
The $xrefs variable contains an array of mappers. Each mapper being an object, you have to cast it to an array before encoding it to JSON. This can be done in one line by mapping the $xref->cast() method to each record: array_map([$xref,'cast'],$xrefs).
If you're not confident with that syntax, you can loop through each record and cast it:
$cast=[];
foreach ($xrefs as $x)
$cast[]=$x->cast();
echo json_encode($cast);
The result is the same.
The advantage of using cast() other just reading each value (as you're doing in your original script) is that it includes virtual fields as well.

MySQL query 2 tables, 1 insert

I'm trying to pull data from 2 tables and then insert result into a third table. My code follows but only does 1 correct entry, and the rest are blank. There are 348 entries total. What am I missing here?
$dbh = DBI->connect(
"DBI:mysql:$mysqldatabase:$mysqlhostname",
"$mysqlusername",
"$mysqlpassword"
);
if(!$dbh) { die("Error: could not get DBI handle\n"); }
$sqlConnect = 1;
$SQL =<<SQL;
SELECT * FROM oscmax2.info2
SQL
$sth = $dbh->prepare($SQL);
if(!$sth) { die("Error: " . $dbh->errstr . "\n"); }
if(!$sth->execute) { die("Error4: " . $sth->errstr . "\n"); }
while (my #row = $sth->fetchrow_array) {
$products_id = $FORM{'product_id'};
$affiliate_id = $FORM{'affiliate_id'};
$demo = $FORM{'demo'};
}
if($sth->rows != 0) {
$total_rows = $sth->rows;
for ($counter = 0; $counter < $total_rows; $counter++) {
$SQL =<<SQL;
SELECT products_attributes_id FROM oscmax2.products_attributes
WHERE products_id = '$products_id'
SQL
$sth = $dbh->prepare($SQL);
if(!$sth) { die("Error: " . $dbh->errstr . "\n"); }
if(!$sth->execute) { die("Error: " . $sth->errstr . "\n");}
while (my #row = $sth->fetchrow_array) {
$products_attributes_id = $FORM2{'products_attributes_id'};
}
$SQL =<<SQL;
INSERT INTO oscmax2.products_attributes_download(
products_attributes_id, products_attributes_filename,
products_attributes_maxdays, products_attributes_maxcount
)
VALUES
('$products_attributes_id', '$affiliate_id/$demo', '7', '1')
SQL
$dbh->do($SQL) || die("Error5: " . $dbh->errstr . "\n");
}
}
$sth->finish();
if($sqlConnect) { $dbh->disconnect();
The blocks
while (my #row = $sth->fetchrow_array) {
$products_id = $FORM{'product_id'};
$affiliate_id = $FORM{'affiliate_id'};
$demo = $FORM{'demo'};
}
and
while (my #row = $sth->fetchrow_array) {
$products_attributes_id = $FORM2{'products_attributes_id'};
}
are wrong. You use #row to accept the data from each row of the result, but never use it. The database fetch won't affect %FORM and %FORM2 so you are just collecting the same data from them several times over
Update
At a guess you want something like this. Please study it and use the techniques rather than copying and testing it as it is, as I have no way of knowing what the structure of your database is and I have made several guesses
You should note the following points
There is no need to test the status of each DBI operation and die if it failed. By default the PrintError option is enabled and DBI will raise a warning if there are any errors. If you want your program to die instead, which is wise, then you can enable RaiseError and disable PrintError and DBI will do it all for you
There is no need to fetch all of the data from a table into memory (which I think is what you are trying to do with your while loops. You should fetch each row into an array and process the data row by row unless you have a reason to do otherwise
You should always prepare your statement and use placeholders. Then you can pass the actual parameters to the execute call and DBI will correctly quote them for you. Furthermore you can move all the prepare calls top the top of the program, making your logic much clearer to read
There is almost never a reason to call finish or disconnect. Perl will do the right thing for you when your database or statement handles go out of scope or your program ends
I have named the statement handles $select1 and $select2. These are very poor names, but I don't know the structure of your database so I couldn't write anything better. That shouyldn't stop you from improving them
I have had to guess at the columns returned by the first SELECT statement. If the three variables don't correspond to the first three elements of #row then you need to correct that
You should avoid using capital letters in Perl lexical identifiers. They are reserved for globals like package names, and nasty clashes of purpose can be caused if you don't abide by this rule
use strict;
use warnings;
my ($mysqldatabase, $mysqlhostname, $mysqlusername, $mysqlpassword) = qw/ dbase host user pass /;
my $dbh = DBI->connect(
"DBI:mysql:$mysqldatabase:$mysqlhostname",
"$mysqlusername",
"$mysqlpassword",
{RaiseError => 1, PrintError => 0}
) or die "Unable to connect to database: $DBI::errstr";
my $select1 = $dbh->prepare('SELECT * FROM oscmax2.info2');
my $select2 = $dbh->prepare(<<__END_SQL__);
SELECT products_attributes_id FROM oscmax2.products_attributes\
WHERE products_id = ?
__END_SQL__
my $insert = $dbh->prepare(<<__END_SQL__);
INSERT INTO oscmax2.products_attributes_download (
products_attributes_id,
products_attributes_filename,
products_attributes_maxdays,
products_attributes_maxcount
)
VALUES ( ?, ?, ?, ? )
__END_SQL__
$select1->execute;
while ( my #row = $select1->fetchrow_array ) {
my ($products_id, $affiliate_id, $demo) = #row;
$select2->execute($products_id);
while ( my #row = $select2->fetchrow_array ) {
my ($products_attributes_id) = #row;
$insert->execute($products_attributes_id, "$affiliate_id/$demo", 7, 1 );
}
}

Saving the output of a Perl SQL query to a hash instead of an array

I'm trying to add an argument to the end of a command line, run that search through a MySQL database, and then list the results or say that nothing was found. I'm trying to do it by saving the query data as both hashes and arrays (these are exercises, I'm extremely new at PERL and scripting and trying to learn). However, I can't figure out how to do the same thing with a hash. I do want the SQL query to complete, and then write the output to a hash, so as not to invoke the While function. Any guidance would be appreciated.
#!/usr/bin/perl -w
use warnings;
use DBI;
use Getopt::Std;
&function1;
&function2;
if ($arrayvalue != 0) {
print "No values found for '$search'"."\n"};
sub function1 {
getopt('s:');
$dbh = DBI->connect("dbi:mysql:dbname=database", "root", "password")
or die $DBI::errstr;
$search = $opt_s;
$sql = $dbh->selectall_arrayref(SELECT Player from Players_Sport where Sport like '$search'")
or die $DBI::errstr;
#array = map { $_->[0] } #$sql;
$dbh->disconnect
or warn "Disconnection failed": $DBI::errstr\n";
}
sub function2 {
#array;
$arrayvalue=();
print join("\n", #array, "\n");
if(scalar (#array) == 0) {
$arrayvalue = -1
}
else {$arrayvalue = 0;
};
}
Please see and read the DBI documentation on selectall_hashref. It returns a reference to a hash of reference to hashes.
Use Syntax:
$dbh->selectall_hashref($statement, $key_field[, \%attri][, #bind_values])
So here is an example of what/how it would be returned:
my $dbh = DBI->connect($dsn, $user, $pw) or die $DBI::errstr;
my $href = $dbh->selectall_hashref(q/SELECT col1, col2, col3
FROM table/, q/col1/);
Your returned structure would look like:
{
value1 => {
col1 => 'value1',
col2 => 'value2',
col3 => 'value3'
}
}
So you could do something as follows for accessing your hash references:
my $href = $dbh->selectall_hashref( q/SELECT Player FROM
Players_Sport/, q/Player/ );
# $_ is the value of Player
print "$_\n" for (keys %$href);
You can access each hash record individually by simply doing as so:
$href->{$_}->{Player}
Cribbing from the documentation:
$sql = $dbh->selectall_hashef("SELECT Player from Players_Sport where Sport like ?", 'Players_Sport_pkey', $sport_like_value);
my %hash_of_sql = %{$sql};

Get the fetched values from the database

I need to store the fetched data from the MySQL database. So I used this code.
while (#row = $statement->fetchrow_array)
{
print "#row.\n"; # ------printing data
}
foreach $key(#row)
{
print $key."\n"; # ------empty data
}
In foreach loop the #row data is empty. How to solve this
UPDATE: It actually should be like this:
while (my #row = $statement->fetchrow_array) {
# print "#row.\n";
foreach my $key(#row) {
$query= "ALTER TABLE tablename DROP FOREIGN KEY $key;";
$statement = $connection->prepare($query);
$statement->execute()
or die "SQL Error: $DBI::errstr\n";
}
}
Well, it should be like this:
while (my #row = $statement->fetchrow_array) {
foreach my $key (#row) {
print $key."\n";
}
}
Otherwise all the result set will be consumed in the first loop.
As a sidenote, fetchrow_array returns a rowset as array of field values, not keys. To get the keys as well, you should use fetchrow_hashref:
while (my $row = $statement->fetchrow_hashref) {
while (my ($key, $value) = each %$row) {
print "$key => $value\n";
}
}
UPDATE: from your comments it looks like you actually need an array of column names. There's one way to do it:
my $row = $statement->fetchrow_hashref;
$statement->finish;
foreach my $key (keys %$row) {
# ... dropping $key from db as in your original example
}
But in fact, there are more convenient ways of doing what you want to do. First, there's a single method for preparing and extracting a single row: selectrow_hashref. Second, if you want to get just foreign keys information, why don't use the specific DBI method for that - foreign_key_info? For example:
my $sth = $dbh->foreign_key_info( undef, undef, undef,
undef, undef, $table_name);
my $rows = $sth->fetchall_hashref;
while (my ($k, $v) = each %$rows) {
# process $k/$v
}

Using Perl and MySql, how can I check for an empty result?

Way oversimplified example:
# Get Some data
$query = $db->prepare(qq{
select * from my_table where id = "Some Value"
});
$query->execute;
# Iterate through the results
if ( *THE QUERY HAS RETURNED A RESULT* ) {
print "Here is list of IDs ";
while ($query_data = $query->fetchrow_hashref) {
print "$query_data->{id}";
}
};
Looking for the code for "THE QUERY HAS RETURNED A RESULT" up there. I'd like to avoid using count(*) in my SQL if possible, since that will require a "group by".
my $sth = $dbh->prepare($stmt);
$sth->execute();
my $header = 0;
while (my $row = $sth->fetchrow_hashref) {
print "Here is list of IDs:\n" if !$header++;
print "$row->{id}\n";
}
Alternative:
my $sth = $dbh->prepare($stmt);
$sth->execute();
my $row = $sth->fetchrow_hashref;
print "Here is list of IDs:\n" if $row;
while ($row) {
print "$row->{id}\n";
$row = $sth->fetchrow_hashref;
}
Simpler code at the expense of memory:
my $ids = $dbh->selectcol_arrayref($stmt);
if (#$ids) {
print "Here is list of IDs:\n";
print "$_\n" for #$ids;
}
Looks to me like your check for the query result is redundant. Your while loop will evaluate 'false' if there is no row to fetch.
old/wrong answer
If you are using DBI with DBD::mysql then $query->rows; will return you the number of rows, selected (or affected on a writing statement) by your statement.
EDIT
Please don't use that and have a look at the comment to this answer by #Luke The Obscure