• Aucun résultat trouvé

DBI Methods and Attributes

Dans le document MySQL Enterprise Solutions (Page 189-200)

This section lists the most commonly used methods and attributes in the DBI module. You can find more detailed information by visiting http://

search.cpan.org/author/TIMB/DBI-1.30/DBI.pm. To learn more about the DBD::mysql module, which works underneath the DBI module to provide support for MySQL connectivity, visit http://search.cpan.org/author/JWIED/

DBD-mysql-2.1018/lib/DBD/mysql.pod.

The methods and attributes listed as follows can be divided into three cate-gories: methods and attributes global to the DBI module, database handle meth-ods and attributes, and statement handle methmeth-ods and attributes.

Global methods and attributes

available_drivers: Returns an array of available database drivers. Usage example: @drivers = DBI->available_drivers;

connect($datasource, $username, $password): Creates a connection to a database using the datasource information and supplied username/password and returns a database handle reference. The datasource argument will refer-ence a DBD source—for example, DBI:mysql:$database:$hostname:$port.

System Preparation 167

Usage example: $dbh = DBI->connect(“DBI:mysql:products:localhost”,

“spider”, “spider”);

err: Attribute containing the database error code from the last operation.

Usage example: die “Error code: $DBI::err\n” if ($DBI::err);

errstr: Attribute containing the database error message from the last opera-tion. Usage example: die “Error: $DBI::errstr\n” if ($DBI::errstr);

Database handle methods and attributes

AutoCommit: Attribute controlling whether each query that modifies data should be automatically committed. Usage example: $dbh->{AutoCommit} = 1;

RaiseError: Attribute controlling whether errors should be checked for after each database call. If enabled, an error would result in aborting the program with the appropriate error message. Usage example: $dbh->{RaiseError} = 1;

disconnect: Closes the connection to the database server associated with the database handle. Usage example: $result_code = $dbh->disconnect;

prepare($query): Prepares the query for execution and returns a statement handle. In MySQL, the call is more of a protocol formality because MySQL does not yet support prepared queries (as of this writing). Usage example: $sth =

$dbh->prepare($query);

quote($col): Quotes the value (by surrounding it with quotes), escaping potentially problematic characters that may confuse the query parser. Usage example, $quoted_col = $dbh->quote($col);

Statement handle methods and attributes

bind_columns(column variable): Binds all of the columns in a result row to a specific variable. Subsequent calls to fetch() will set the variables to the query values fetched from the result set. Usage example: $sth->bind_columns($var1,

$var2, undef, $var3);

do($query): Prepares and executes the query in the same fashion as the pre-pare/execute combination. Usage example: $rows = $dbh->do($query);

execute: Executes a prepared statement returned by the prepare() function and returns the result value. The result is usually a row count. For example,

$rows = $sth->execute;

fetch: Retrieves a row from the statement handle and places it in the variables that have previously been bound to result set columns with a bind_columns() call.

fetchall_arrayref: Fetches all data from the statement handle and returns a reference to an array of references. Each element of the array represents a row.

Usage example: $all_rows_ref->fetchall_arrayref;

fetchrow_array: Fetches the next row from the statement handle and returns it as an array. Usage example: @rowData = $sth->fetchrow_array;

fetchrow_arrayref: Fetches the next row from the statement handle and returns it as an array reference. For example, $row_ref = $sth->fetchrow_

arrayref;

fetchrow_hashref: Fetches the next row from the statement handle and returns a hash reference. Usage example: $row_hash_ref = $sth->fetchrow_

hashref;

finish: Destroys the statement handle after the work has finished freeing the system resources allocated by it. Usage example: $sth->finish;

rows: Returns the number of rows affected by the last query. Meaningful only on a query that modifies the table. Usage example: $affected_rows =

$sth->rows;

NUM_OF_FIELDS: Returns the number of fields in the query result. Usage example: $fieldCount = $sth->(NUM_OF_FIELDS);

NULLABLE: Returns a reference to an array of values that indicate which columns can contain NULLs. The values in the array have the following mean-ing: 0 or empty string—no, 1—yes, 2—unknown.

API Overview

The Perl API is a DBI wrapper around MySQL C API calls. While a detailed dis-cussion of DBI is beyond the scope of this book, we provide a quick primer to get you started and be able to get the job done. (If you’re interested, dbi.perl.org is a good start for an in-depth DBI discussion.)

DBI is a database-independent Perl interface to SQL databases. It allows the programmer to write portable code that will run with a large number of data-bases as a backend while requiring only a very minimal code change: the name of the driver in the connect call. DBI accomplishes this by abstracting the data-base communication concepts of connecting, preparing a query, executing it, processing the result, and disconnecting. Each concept has a corresponding abstract method, which in turn goes down to a lower-level interface driver that is able to talk to the given database using the specific protocol that it understands.

AP I Over view 169

A typical program utilizing the DBI module will have a use DBI; statement at the beginning. The first call is DBI->connect(), which attempts to establish a con-nection to the database loading the appropriate DBD driver. Following the motto of Perl (“There is more than one way to do it”), there are many ways to pass arguments to DBI->connect(). Rather than discuss all of them, we mention just one that works well for MySQL in most cases: The first argument is a DSN in the form of dbi:mysql:$db_name:host=$host;port=$port;socket=$socket.

The second argument is the MySQL username, and the third is the password of that user. The fourth argument is a hash of options, in which you may want to set PrintError to 0 when you are writing a serious application.

DBI->connect() will return a connection handle object on success; otherwise, it returns undef. If you did not disable PrintError, an error message also will be printed to stderr. You will use the returned handle for all the subsequent opera-tions. For simplicity, we refer to the handle returned from DBI->connect() as

$dbh in the discussion that follows.

If a query does not return any results, you should use $dbh->do(). On success, the return value is the number of affected rows. On error, undef is returned. In order to allow statements like $dbh->do($query) || die “Query failed”; where we directly evaluate the return value for truth, to in case of success that neverthe-less affected no rows 0E0 is returned, which evaluates to a true value in the boolean sense.

If the query returns a result set, it is performed in two stages. First, you call

$dbh->prepare(), which returns a statement handle ($sth in the future context) and then call $sth->execute().The dual-stage prepare/execute mechanism is designed for databases that support prepared statements. MySQL does not, but the low-level DBD driver has to comply with the DBI interface requirements in this regard. $sth->execute() completes the execution of the query and returns a true value on success and undef on error.

Once the statement has been executed, the result can be retrieved one row at a time with $sth->fetchrow_array(), $sth->fetchrow_arrayref(), $sth->fetch(),or sth->fetchrow_hashref(). To retrieve all rows, call one of those methods in a loop. When all rows have been retrieved, undef will be returned. Upon finishing the work with $sth, you must call $sth->finish(). When you are done with the connection handle, you must call $dbh->disconnect().

Sample Code

In Listing 10.1, we use a script that updates bank accounts and prints reports for our sample code. For convenience, we also add a special option on the com-mand line to initialize the database.

To run the script, download sample.pl from the book Web site, and first type

perl sample.pl i

If all is well, you will see a message telling you that the database has been ini-tialized. After that, you can run perl sample.pl several times in a row and watch how those sample customers get richer and richer with every run. As an exer-cise, you can add another customer to the database, either through the com-mand-line interface or by adding another call to add_customer_with_deposit() in init_db().

The comments in the code should explain quite well what is happening and pro-vide some instruction on how to interface with MySQL using Perl. In addition to the comments, we provide a review following the listing.

Sample Code 171

#! /usr/bin/perl

# Ask Perl to make it more difficult for us to write sloppy code use strict;

# Need the DBI driver use DBI;

# These variables are going to be global - tell 'strict' about it

# Explanation of the cryptic names: dbh - Database Handle, DSN - Data Source

# Name, and drh - Driver Handle

use vars qw($dbh $user $host $db $pass $dsn $drh);

#Now declare our global variables my $dbh;

my ($user,$host,$db,$pass,$dsn,$drh);

# configuration settings for database connectivity

$host = "localhost";

$user = "root";

$db = "test";

$pass = "";

$dsn = "dbi:mysql:$db:host=$host";

# Error handling routines are as important for a good program as a goalie for

# a good soccer team. That is why before we do anything else we protect ourselves

# by writing this generic emergency exit function.

sub dbi_die {

Listing 10.1 Perl MySQL sample code. (continues)

my $msg = shift;

#We need to save the error message since the cleanup will destroy it.

#Alternatively, we could have printed the message, done the cleanup, and

#then exited.

my $dbi_msg = $DBI::errstr;

do_cleanup();

die("FATAL ERROR: $msg: DBI message: $dbi_msg}\n");

}

# Roll back the transaction and disconnect from the database sub do_cleanup

{

if (defined($dbh)) {

$dbh->rollback();

$dbh->disconnect();

} }

# Wrapper connection function that allows us to not worry about error handling.

# It also provides flexibility because, for example, it can be modified to connect

# to an alternative server if the primary one is not available, and we would

# not have to change the rest of the code.

sub safe_connect {

($dbh = DBI->connect($dsn,$user,$pass,{PrintError => 0,AutoCommit => 0}))

|| dbi_die("Could not connect to MySQL");

}

# Wrapper query function. Again, the same idea as with safe_connect() - ease of

# use and flexibility.

sub safe_do {

if (!$dbh->do(@_)) {

my $query = shift;

dbi_die("Error running '$query'");

} }

# Another wrapper query function. This one combines $dbh->do() and

# $sth->execute() in one in addition to error checking.

sub safe_query {

Listing 10.1 Perl MySQL sample code. (continues)

Sample Code 173

my $sth;

my $query = shift;

$sth = $dbh->prepare($query);

$sth->execute(@_) || dbi_die("Could not execute '$query'");

return $sth;

}

# Helper function to insert a customer record and create one account

# for the customer. Overall, a rather boring function, other than the

# fact that we count money in cents to be able to stick to integer

# arithmetic, which is where money conceptually belongs. How would you

# like it if you came to the store and they told you that the can of

#orange juice you want to buy will cost you the square root of 2

# dollars, and with tax it is going to be PI/2? You round it to $1.58,

# and they file off a piece of a penny to give you for change.

#

# Another noteworthy thing in this function is storing the interest

# rate as an integer. We assume that our interest rate precision is

# %0.01, and therefore multiply the percentage value by 100. Do not do

# such tricks if you plan to have users connect to the database

# directly and look at tables - they will be thoroughly confused. But

# if all access to the data goes through your application, this is

# invisible to the user and helps reduce storage requirements.

sub add_customer_with_deposit {

my $query;

my ($fname,$lname,$ssn,$street,$city,$state,$zip,$amount) = @_;

$query = "INSERT INTO customer (fname,lname,ssn,street,city,state,zip)".

" VALUES (?,?,?,?,?,?,?)";

# Note the arguments of safe_do(). It will replace ? with

# respective values and quote them with $dbh->quote(). Also note

# that the second argument is an attribute hash and is usually set

# to undef.

safe_do($query,undef,$fname,$lname,$ssn,$street,$city,$state,$zip);

my $interest_rate = 375; # 3.75%

$query = "INSERT INTO account (customer_id,interest_rate,amount) VALUES (LAST_INSERT_ID(),$interest_rate,$amount * 100)";

safe_do($query);

}

# Creates the tables initially when the program is run with the i

# argument and populates it with dummy data. Should be run only once.

#

# We have two tables - customer and account. A customer is allowed to

# have more than one account. Each customer and account are identified

# with a surrogate unique id. To demonstrate the use of transactions

# and to remind you that MySQL does support transactions Listing 10.1 Perl MySQL sample code. (continues)

# nowadays, we make both tables transactional - TYPE=INNODB.

#

# Note the partial key on only the first 5 letters of

# the first name and only the first 5 letters of the last name. This

# is a heuristical decision - the reasoning that leads us to this

# decision is that if two last names have the same first 5 letters

# there is a high likelihood they are either identical or belong to a

# class of similar names of small variety (e.g., {Anderson,Andersen}),

# and the same is true for the first names. Therefore, this way of

# partial column indexing will help us reduce the size of the key

# without significantly reducing the uniqueness of key values.

#

# Note also that we store money as the number of cents and percentage

# rate as the actual percentage rate multiplied by 100. With the

# assumption that we only need %0.01 precision we can store it as an

# integer. We could optimize even further. Since the interest rate is

# usually attached to the account type, we could have account_type_id

# tinyint unsigned not null, and have in a separate small table a full

# description of the account, including the rate.

#

# For simplicity, we omit a lot of columns you will actually need if

# this was a real bank application.

sub db_init {

# First, get the old tables out of the way

safe_do("DROP TABLE IF EXISTS customer,account");

# Now create tables

my $query = q{CREATE TABLE customer (

id int unsigned not null auto_increment primary key,

$query = q{CREATE TABLE account (

id int unsigned not null auto_increment primary key, customer_id int unsigned not null,

Listing 10.1 Perl MySQL sample code. (continues)

Sample Code 175

interest_rate smallint unsigned not null, amount int unsigned not null,

key(customer_id), key(amount) ) TYPE=INNODB };

safe_do($query);

# Populate the created tables. For the curious, I have made sure

# that the street names and the zips are real with the help of

# www.mapquest.com. Social Security numbers and names are, of

# course, made up.

add_customer_with_deposit("Tim", "Jones", "111234567", "1400 Maple",

add_customer_with_deposit("Jim", "Marble", "678456788", "230 N Pioneer",

# Adds annual interest to all accounts. Note the computation of the

# increase - we divide the interest rate by 10000 instead of 100

# because we store it multiplied by 100 to make it an integer.

sub update_account {

my $query = q{UPDATE account SET amount = amount + amount * (interest_rate/10000)};

safe_do($query);

$dbh->commit();

}

# Prints the summary reports. There are a few subtle details to take

# care of. Look in the comments for details.

sub print_report {

# Report header

print "Account Summary Report\n";

print "—————————————————————-\n";

# Get the sum and the maximum

Listing 10.1 Perl MySQL sample code. (continues)

my $query = "SELECT SUM(amount)/100,MAX(amount) FROM account";

my $sth = safe_query($query);

my ($total, $max_amount,$id,$fname,$lname);

# bind_columns() + fetch() is the most efficient way to read

# the rows from a statement handle, according to the DBI

# documentation. After the following invocation of bind_columns(),

# the first column from the query will be put into $total, while

# the second will be in $max_amount.

$sth->bind_columns(\$total,\$max_amount);

# There is only one row in the result, so we call fetch() only

# once.

$sth->fetch();

# Need to test for NULL. If the value is SQL NULL we get undef in

# the corresponding variable from DBI if (!defined($total))

{

print "There are no accounts\n";

# Note the cleanup with $sth->finish() before return.

$sth->finish();

return;

}

# Now we print the total, but hold off on the maximum amount. We

# will print later when we retrieve the name(s) of the customer(s)

# with that amount.

print "Accounts Total is \$$total\n";

# clean-up of the old statement handle

$sth->finish();

# Now let us retrieve the names of the customers who had the

# maximum amount.

$query = "SELECT customer.id,account.id,fname,lname,amount/100 FROM customer,account WHERE

customer.id = account.customer_id AND amount = $max_amount";

$sth = safe_query($query);

# This trick allows us to keep our English grammar straight.

# Instead of the ugly Account(s), we print Account if there was

# only one and Accounts otherwise. The number of rows is in

# $sth->rows my $plural = "" ;

$plural = "s" if ($sth->rows > 1);

print "Top Account$plural:\n";

Listing 10.1 Perl MySQL sample code. (continues)

Sample Code 177

# Now we proceed with the names of the customers having the top

# deposit amounts. We begin with a column heading.

print "Customer ID\tAccount ID\tFirst Name\tLast Name\tAmount\n";

my ($customer_id,$account_id,$fname,$lname,$amount);

# Repeat the bind_columns() manuever

$sth->bind_columns(\$customer_id,\$account_id,\$fname,\$lname,

\$amount);

# This time we fetch() in a loop since there can be more than one

# customer with the maximum amount.

while ($sth->fetch()) {

# Print each row, tab-delimited

print "$customer_id\t$account_id\t$fname\t$lname\t$amount\n";

}

# Cleanup

$sth->finish();

}

# Now the code for main

# First make sure that the MySQL DBD driver is sufficiently recent

# This is needed because we are using transactions.

#

# Note that there is a discrepancy between the version number in the

# variable# and the official version of the driver, but luckily, they

# correlate and we can do version checks.

($drh = DBI->install_driver("mysql")) ||

dbi_die("Failed to load MySQL driver");

if ($drh->{"Version"} < 2.0416) {

dbi_die("Please install MySQL driver version 1.22.16 or newer\n");

}

# Connect to the database. The earlier work is rewarded - we do not

# need to check for errors and everything is done in one neat line.

# All ugliness has already been put into the implementation of

# safe_connect().

safe_connect();

# Check to see if the first argument is i. If yes, initialize the

# database; otherwise, update the accounts with the interest earned

# and print the report.

#

# This is a rather ugly interface hack. In a real-world application,

# we would have a separate script that would create the tables, plus Listing 10.1 Perl MySQL sample code. (continues)

# an interface for updating them. However, to make everything work

# from one file in order to simplify the setup process of this demo,

# we do it this way.

if ($ARGV[0] eq "i") {

db_init();

} else

{

update_account();

print_report();

}

# Disconnect before exit.

$dbh->disconnect();

Listing 10.1 Perl MySQL sample code. (continued)

Now let’s examine a few significant points in the code:

■■ We use wrappers around databases that perform error checking and could be extended to do other things, such as error logging, performance mea-surement, or load-balancing.

■■ Because we use transactions, we need to make sure that DBD::mysql is the sufficiently recent version.

■■ Because we have chosen to perform optimizations in our schema to con-vert the attributes (which are usually given the types float or decimal) to integer (amount and interest_rate), we perform multiplications in the queries to adjust for the difference between internal representation and input/display values. As stated in the comments, this is not recommended

■■ Because we have chosen to perform optimizations in our schema to con-vert the attributes (which are usually given the types float or decimal) to integer (amount and interest_rate), we perform multiplications in the queries to adjust for the difference between internal representation and input/display values. As stated in the comments, this is not recommended

Dans le document MySQL Enterprise Solutions (Page 189-200)