In
the
rest of this chapter, we’ll lay out the code that
implements a robust Model for a class we’ll call
Publisher
. For
the sake of simplicity, each publisher has just two attributes: an ID
and a name. The id
field is the primary key and
uniquely identifies each row of the table. The
Name
field is the name of the publisher.
In this class, you will recognize all the methods we discussed in the previous section. Of the 13 methods, 4 correspond directly to SQL activities:
create( )
An instance method that inserts a new row
into the table to hold the data from this object. The primary key for
this table is a MySQL
AUTO_INCREMENT
field that automatically creates a new value for that field.
Therefore, this method inserts only the value of the
name
field. The id
field is
passed in by this method as NULL
, set in the
database by MySQL, and then retrieved and assigned to the
object’s id
attribute by this
method.
get( )
A static method that creates an SQL
SELECT
statement based on WHERE
parameters passed to the function. For each row of the result set, a
new object is created and an array of these objects is returned. This
method represents the Generic Where method described in the previous
section.
remove( )
An instance method that removes the row
of data corresponding to this object in the database. It issues an
SQL DELETE
command to accomplish the removal.
After this method is called, the program should destroy the object,
because its underlying data is gone.
update( )
An instance method that updates the data
in the database with the attribute data in the object. In effect,
this method “saves” the current
state of the object into the database. For this method to work, the
object must already have a row in the database, because the method
uses the SQL UPDATE
statement.
Some of these calls invoke lower-level functions to handle SQL
WHERE
clauses. These WHERE
clauses can be very complex,
especially when multiple tables are involved. Clauses may even be
nested. The information stored within an SQL WHERE
clause can also be stored in a Perl multilevel hash without losing
any information. We allow a WHERE
clause of any
complexity by encapsulating the processing of the clause in the
following two functions:
make_where( )
A method that flattens the multilevel
hash into a regular SQL WHERE
clause that can be
used in a SQL query.
bind_where( )
A method that inserts the
WHERE
clause into a statement handle. While the
make_where( )
method creates the actual SQL
WHERE
clause, the values of the parameters still
need to be bound to the statement once the statement is prepared.
This method calls the bind_param(
)
method shown earlier in this chapter to
insert each parameter value into the statement handle. After this
method is called, the SELECT
statement can be
executed.
The following functions get and set the two attributes:
getId( )
An accessor method that retrieves the
current value of the id
attribute.
setId( )
An accessor method that sets the value of
the id
attribute. Because the
id
field of the table is the primary key, this
method will rarely be called.
getName( )
An accessor method that retrieves the
current value of the name
attribute.
setName( )
An accessor method that sets the value of
the name
attribute.
The following is the Primary Key select function described in the previous section:
The following are constructors:
This Publisher.pm module must be located in a directory we’ve called CBDB. In addition to this class, which you can generalize and apply to other applications, we use three helper classes. These are entirely generic and can be reused in any application empoying our Model.
This class handles
the creation of the connection and a couple of other low-level
operations on the database that are required by some
Publisher
methods. The code must be placed in a
module named DB.pm
in the CBDB
directory.
This class implements other low-level functions that must be implemented by functions specific to MySQL. These functions are isolated here so that you can easily port the Model code to another database and just replace this class. The mysql.pm module must be placed in a directory named BM that is a sibling of the CBDB directory.
This class provides a very simple mechanism for storing objects in memory and retrieving them later, to minimize the amount of traffic to and from the database. The Cache.pm module must be placed in the CBDB directory.
If you like to learn code from the bottom up, read the sections on
the helper classes, then return to read the code for the
Publisher
class.
Finally, you may understand the wealth of functions better by seeing them organized into a hierarchy and seeing which functions are called by others. Figure 9-1 shows the main ways functions are called during major database operations.
In the following subsections, we’ll introduce each method of this class in the order in which we’ve previously described it. The file begins with the following initializations.
package CBDB::Publisher; our $VERSION = '1.0'; use strict; use DBI qw(:sql_types); use CBDB::DB; use CBDB::Cache; our @ISA = qw( CBDB::DB );
The
create( )
,
get( )
,
remove( )
,
and
update( )
methods are
fairly short and simple. They check the types of their input
arguments, build SQL queries or updates in the ways shown earlier in
this chapter, and execute the SQL. They also interact with the cache.
Example 9-6 demonstrates this.
################################################# # create() - Inserts the object into the database. # Parameters: None. # Returns: A Publisher object (redundantly, because # this method is called on that same object). sub create { my $self = shift; my $dbh = CBDB::DB::getDB( ); my $query = "INSERT INTO publisher ( name, id ) VALUES ( ?, ? )"; my $sth = $dbh->prepare($query); my $pk_id = undef; $sth->bind_param(1, $self->getName( ), {TYPE=>1}); $sth->bind_param(2, undef, {TYPE=>4}); $sth->execute; $sth->finish; $pk_id = CBDB::DB::get_pk_value($dbh, 'publisher_id'), $self->setId( $pk_id); $dbh->disconnect; CBDB::Cache::set('publisher', $self->getId( ), $self); return $self; } ################################################# # get() - Retrieves objects from the database. # Parameters: Optional WHERE clause. # Returns: Array of Publisher objects. sub get { my $wheres = undef; my $do_all = 1; if (ref($_[0]) eq 'ARRAY') { $wheres = shift; $do_all = shift if @_; } else { $do_all = shift; } my $dbh = CBDB::DB::getDB( ); my $where .= ' WHERE ' . make_where( $wheres ); my $query = qq{ SELECT publisher.name as publisher_name, publisher.id as publisher_id FROM publisher $where }; my $sth = $dbh->prepare($query); bind_where( $sth, $wheres ); $sth->execute; my @publishers; while (my $Ref = $sth->fetchrow_hashref) { my $publisher = undef; if (CBDB::Cache::has('publisher', $Ref->{publisher_id})) { $publisher = CBDB::Cache::get('publisher', $Ref->{publisher_id}); } else { $publisher = CBDB::Publisher::populate_publisher( $Ref ); CBDB::Cache::set('publisher', $Ref->{publisher_id}, $publisher); } push(@publishers, $publisher); } $sth->finish; $dbh->disconnect; return @publishers; } ################################################# # remove( ) - Removes an object from the database. # This method can be called on an object to delete # that object, or statically, with a WHERE clause, # to delete multiple objects. # Parameters: An optional where clause. # Returns: Nothing. sub remove { my $self = undef; my $where = undef; my $is_static = undef; if ( ref($_[0]) and $_[0]->isa("CBDB::Publisher") ) { $self = shift; $where = "WHERE id = ?"; } elsif (ref($_[0]) eq 'HASH') { $is_static = 1; $where = 'WHERE ' . make_where($_[0]); } else { die "CBDB::Publisher::remove: Unknown parameters: " . join(' ', @_); } my $dbh = CBDB::DB::getDB( ); my $query = "DELETE FROM publisher $where"; my $sth = $dbh->prepare($query); if ($is_static) { bind_where($sth, $_[0]); } else { $sth->bind_param(1, $self->getId( ), {TYPE=>4}); } $sth->execute; $sth->finish; $dbh->disconnect; } ################################################# # update( ) - Updates this object in the database. # Parameters: None. # Returns: Nothing. sub update { my $self = shift; my $dbh = CBDB::DB::getDB( ); my $query = "UPDATE publisher SET name = ?, id = ? WHERE id = ?"; my $sth = $dbh->prepare($query); $sth->bind_param(1, $self->getName( ), {TYPE=>1}); $sth->bind_param(2, $self->getId( ), {TYPE=>4}); $sth->bind_param(3, $self->getId( ), {TYPE=>4}); $sth->execute; $sth->finish; $dbh->disconnect; CBDB::Cache::set('publisher', $self->getId( ), $self); }
The make_where()
and bind_where(
)
methods are the most complex in our
Model, because they must unpack and process complex data structures:
Perl hashes, sometimes containing nested hashes. The
make_where( )
method takes the Perl hash as input
and converts its contents to a string containing a valid
WHERE
clause. The bind_where( )
method is even more complicated. It takes a
statement handle and an array (sometimes
containing nested arrays) of bind variables. It issues
bind_param(
)
calls to bind values to the proper places
in the statement handle.
The WHERE
clause used by the
get
and remove
methods is in
the form of a array reference. Each element of the array is either a
single WHERE
element or a reference to another
array. If it is a reference to another array, the elements in that
array are recursively embedded into the WHERE
clause to allow clauses such as:
element
AND (element
OR (element
ANDelement
))
A single WHERE
element is a hash reference that
has at least the keys column
and
value
. These contain the column name and value of
the WHERE
element. Other optional keys include
type
, which is the SQL operator used to join this
element with the next element (it defaults to AND
)
and operator
, which is the SQL operator used
between the column name and the value (it defaults to an equals
sign). Example 9-7 shows methods that handle
WHERE
clauses.
################################################# # make_where( ) - Construct a WHERE clause from a well-defined hash ref. # Parameters: WHERE clause reference. # Returns: WHERE clause string. sub make_where { my $where_ref = shift; if ( ref($where_ref) ne 'ARRAY' ) { die "CBDB::Publisher::make_where: Unknown parameters: " . join(' ', @_); } my @wheres = @$where_ref; my $element_counter = 0; my $where = ""; for my $element_ref (@wheres) { if (ref($element_ref) eq 'ARRAY') { $where .= make_where($element_ref); } elsif (ref($element_ref) ne 'HASH') { die "CBDB::Publisher::make_where: malformed WHERE parameter: " . $element_ref; } my %element = %$element_ref; my $type = 'AND'; if (not $element_counter and scalar keys %element == 1 and exists($element{'TYPE'})) { $type = $element{'TYPE'}; } else { my $table = "publisher"; my $operator = "="; if (exists($element{'table'})) { $table = $element{'table'}; } if (exists($element{'operator'})) { $operator = $element{'operator'}; } if ($element_counter) { $where .= " $type "; } else { $element_counter = 1; } for my $term ( grep !/^(table|operator)$/, keys %element ) { $where .= "$table.$term $operator ?"; } } } return $where; } ################################################# # bind_where( ) - Executes the handle->bind method that binds the # where element. # Parameters: WHERE clause array ref and a scalar # ref to a counter number that tells the method # which parameter to bind to. # Returns: Nothing. sub bind_where { my $sth = shift; my $where_ref = shift; my $counter_ref = shift || undef; my $counter = (ref($counter_ref) eq 'Scalar')? $$counter_ref : 1; if ( not $sth->isa('DBI::st') or ref($where_ref) ne 'ARRAY' ) { die "CBDB::Publisher::make_where: Unknown parameters: " . join(' ', @_); } my @wheres = @$where_ref; for my $element_ref (@wheres) { if (ref($element_ref) eq 'ARRAY') { bind_where($sth, $element_ref, $counter); } elsif (ref($element_ref) ne 'HASH') { die "CBDB::Publisher::make_where: malformed WHERE parameter: " . $_; } my %element = %$element_ref; unless (not $counter and scalar keys %element == 1 and exists($element{'TYPE'})) { my $table = "publisher"; if (exists($element{'table'})) { $table = $element{'table'}; } for my $term ( grep !/^(table|operator)$/, keys %element ) { $sth->bind_param($counter, $element{$term}, {TYPE=>CBDB::DB::getType($table,$term)}); $counter++; } } } }
The
getId( )
,
setId( )
,
getName( )
, and
setName( )
methods are
typical object-oriented methods for accessing properties of the
object. Example 9-8 demonstrates this.
################################################# # getId( ) - Return Id for this publisher. # Parameters: None. # Returns: ID. sub getId { my $self = shift; return $self->{Id}; } ################################################# # setId( ) - Set Id for this publisher. # Parameters: An Id number. # Returns: Nothing. sub setId { my $self = shift; my $pId = shift or die "publisher.setId( Id ) requires a value."; $self->{Id} = $pId; } ################################################# # getName( ) - Return Name for this publisher. # Parameters: None. # Returns: Name. sub getName { my $self = shift; return $self->{Name}; } ################################################# # setName( ) - Set Name for this publisher. # Parameters: A name. # Returns: Nothing. sub setName { my $self = shift; my $pName = shift || undef; $self->{Name} = $pName; }
The
getByPrimaryKey( )
method retrieves data by its primary key, invoking the get(
)
method previously shown. This is shown in Example 9-9.
################################################# # getByPrimaryKey() - Retrieves a single object from # the database based on a primary key. # Parameters: An Id. # Returns: A Publisher object. sub getByPrimaryKey { my $pId = shift or die "publisher.get( )"; my $where = [ {'id' => $pId } ]; return ( get( $where, 1 ) )[0]; }
The
new( )
method is a
typical, generic constructor, while the
populate_publisher( )
method is used by the get( )
method to
create a Publisher
object. Example 9-10 shows constructors.
################################################# # new( ) - Constructor. # Example: CBDB::Publisher->new( ); # Returns: blessed hash. sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = {}; bless($self, $class); return $self; } ################################################# # populate_publisher() - Return a publisher object # populated from a result set. # Parameters: Data from a DBI fetch. # Returns: A Publisher object. sub populate_publisher { my $Ref = shift; my $publisher = CBDB::Publisher->new( ); $publisher->setName($Ref->{publisher_name}); $publisher->setId($Ref->{publisher_id}); return $publisher; } 1; # This always terminates a class definition.
This class provides
utility functions related to databases. It also invokes a
database-specific class, mysql
. It contains, as a
static variable, a hash that lists all the columns in all the
application’s tables along with their data types.
getDB( )
This
method creates a connection to the database and returns the
database handle. This method is
used by the Publisher
methods that build and
execute SQL.
get_pk_value( )
This method returns the most recent
value assigned to a primary key through the auto-increment feature of
the database. It invokes a method by the same name from
mysql
.
get_type( )
This method returns the SQL type of a column within a table, using the class’s hash of columns.
Example 9-11 demonstrates the DB class.
package CBDB::DB; use strict; use BM::mysql; my $VERSION = '0.1'; use constant DSN => "dbi:mysql:database=Books;host=localhost"; use constant USER => "andy"; use constant PASSWORD => "ALpswd"; my $types = { 'creator' => { 'name' => 1, 'id' => 4 }, 'book' => { 'title' => 1, 'publisher_id' => 4, 'date' => 11, 'id' => 4 }, 'book_creator' => { 'book_id' => 4, 'creator_id' => 4, 'role_id' => 4 }, 'publisher' => { 'name' => 1, 'id' => 4 }, 'role' => { 'name' => 1, 'id' => 4 }, }; ##################################################################### # getDB() - Returns a database handle connection for the database. # Parameters: None. # Returns: DBH Connection Handle. sub getDB { my $dbh = DBI->connect(DSN,USER,PASSWORD,{PrintError => 1,RaiseError => 1}); return $dbh; } ##################################################################### # get_pk_value() - Returns the most recent auto_increment value for a PK. # Parameters: Database Handle. # Returns: Primary key value. sub get_pk_value { my $dbh = shift or die "DB::get_pk_value needs a Database Handle..."; my $dbd = BM::mysql->new( ); return $dbd->get_pk_value( $dbh ); } ##################################################################### # getType() - Returns the type of a column within a table. # Parameters: Table name and column name. # Returns: DBI Type code. sub getType { my $table = shift; my $col = shift; return $types->{$table}{$col}; } 1;
In this class, we have tried to extract and isolate calls to methods that are specific to a particular database server.
new( )
is_pk( )
This method determines whether a field is part of the primary key of the table.
is_auto_increment( )
This
method determines whether the primary key
of the table is an AUTO_INCREMENT
field.
get_pk_value( )
This method returns the value of the
most recently inserted AUTO_INCREMENT
field.
Example 9-12 demonstrates the mysql class.
package BM::mysql; use strict; ############### # CONSTRUCTOR # ############### sub new { my $proto = shift; my $class = ref($proto) || $proto; my $self = { }; bless($self, $class); return $self; } ################################################## # is_pk() - Determines if a column is a primary key. # Parameters: DBI statement handle and a column # number from that handle. # Returns: true or false. sub is_pk ($$$) { my $self = shift; my $sth = shift; my $i = shift; return 1 if $$sth->{mysql_is_pri_key}->[$i]; return 0; } ################################################### # is_auto_increment() - Determines if a column is an # AUTO_INCREMENT column. # Parameters: DBI statement. # handle and a column number from that handle. # Returns: true or false. sub is_auto_increment($$$) { my $self = shift; my $sth = shift; my $i = shift; return 1 if $$sth->{mysql_is_auto_increment}->[$i]; return 0; } ################################################ # get_pk_value() - Returns the last AUTO_INCREMENT # value for this connection. # Paramaters: DBI database handle. # Returns: PK value. sub get_pk_value { my $self = shift; my $dbh = shift or die "mysql::get_pk needs a Database Handle..."; my $mysqlPk = "Select last_insert_id( ) as pk"; my $mysqlSth = $dbh->prepare($mysqlPk); $mysqlSth->execute( ); my $mysqlHR = $mysqlSth->fetchrow_hashref; my $pk = $mysqlHR->{"pk"}; $mysqlSth->finish; return $pk; } 1;
This class contains a static hash of all the Model objects used by the application. The cache is organized by class name (the first-order hash) and primary key (the second-order, or nested, hash).
Besides increasing performance, the cache also allows multiple objects to exist that represent the same row of data in the underlying table. Because each object’s code uses references to the cached objects, the object automatically reflects changes made to other objects.
Example 9-13 demonstrates the Cache class.