home | O'Reilly's CD bookshelfs | FreeBSD | Linux | Cisco | Cisco Exam  


Book HomeManaging and Using MySQLSearch this book

9.3. A General Model for Maintainable Perl Programs

Set aside all the wisecracks about Perl being unmaintainable or useful only for small scripts. Also be prepared to renounce the widely held assumption that it's not a truly object-oriented language. In this section we will develop a general Perl framework that will let you quickly add new features to applications and adapt applications to changing database structures.

The code that follows is complex and requires an in-depth knowledge of Perl. Among the features of Perl we exploit are references, recursion, variable type checking, function argument manipulation (the $_[0] syntax), dynamic array building through push statements, and multidimensional hashes—and of course, lots of object-oriented techniques. These features are amply explained in other Perl books, notably Programming Perl by Larry Wall, Tom Christiansen, and Jon Orwant (O'Reilly).

9.3.1. A Model for Relational Data

Because the topic of this book is database interaction, the rest of this chapter focuses on the Model layer of the MVC methodology. We will show you how to design abstract methods that can be used and reused in a variety of applications. We'll also show how to weave persistence into your Model with minimal duplication of code, and—for good measure—speed up access by implementation a caching layer.

The Model contains abstractions of all concrete things used within the application. Therefore, a solid Model is an important foundation for the rest of the application.

Luckily for us, designing a Model for a database-driven application is straightforward. That is because the work of discovering the relevant abstractions in a system is done when the database scheme is created, as we described in Chapter 7.

Usually, each table in the database corresponds to one class in the Model. The fields of the tables correspond to the attributes of the class. Relationships between tables can usually be expressed in the following manner:

One to One
If two tables have a one-to-one relationship, their relationship can involve either containment or aggregation, concepts that are familiar to object-oriented programmers. If one contains the other, the contained object should be defined as an attribute of the container class. For instance, if every ISBN corresponds to one and only one title, one of them can be an attribute of the other. If the relationship is one of aggregation, the more specific class should be a subclass of the less-specific class.

One to Many
If two tables have a one-to-many relationship, the "One" class should contain an array of "Many" objects. Thus, a corporation object could contain an array of objects representing products, because most corporations sell multiple products.

Many to Many
If two tables have a many-to-many relationship, each class can contain an array of objects from the other class. That is, a Person table can exist in a many-to-many relationship with an Employer table (with a many-to-many join table in the middle), because a Person usually has had more than one Employer and each Employer has more than one Person. In the Model, the Person class contains an array of Employer objects, and the Employer class contains an array of Person objects. This type of construct can be very challenging to implement, because of the complexities of recursion. When you create a Person, you create all his Employer s, each of which then contains the Person, which contains the Employer s, etc. Because of this, many designers avoid many-to-many relationships when possible. If they are necessary, however, it is possible to pull off with careful implementation.

Like all classes, a Model class is comprised of attributes and methods. An attribute is simply a variable that describes something about the class. As mentioned above, the attributes of a Model class represent the fields of the underlying table and objects from related tables. But what about the methods?

In object-oriented programming, classes have two kinds of methods: instance and static. Instance methods are only called on actual objects created from the class. Because of this, they have access to the attribute data within the object. Static methods (also known as class methods) are called on the class itself. They have no knowledge of individual objects of that class. For instance, in the first section of this chapter, we saw a static DBI method called connect( ).

Persistence requires each Model class to implement three instance methods, which we'll call update, remove, and create. These methods parallel the SQL UPDATE, DELETE, and INSERT statements that, along with SELECT, make up the vast majority of SQL statements.

update
This method issues an UPDATE statement to save the current state of the object to the database. When an attribute of a Model object is altered somewhere in the application, the application issues its update method so that the change is reflected in the database and is visible to other applications.

remove
This method issues a DELETE statement to remove the row representing the object from the database. Whenever an object is destroyed by the program, it must make sure to call this method. Destruction can occur through garbage collection or at the termination of the program, as well as through explicit requests. Unfortunately, delete is a keyword in Perl, so we can't use it is as the method name. We'll use remove in this chapter. Other common names include destroy, Delete, and deleteObject.

create
This method issues an INSERT statement to create a new row of data in the database. Not all objects in the Model need to be in the database, but anything that you want to have persist beyond a single run of the application needs to be saved through a create method. We chose the name create because "creating" an object is a more logical term than "inserting" an object.

While these methods are the only required ones for a Model class, a common object-oriented practice is to use accessor or getter/setter methods to retrieve and change the values of attributes. If you do this, each attribute of the object should have two instance methods: a get method that retrieves the value of the attribute and a set method that sets the attribute to a new value. They can be named anything, but a common practice is to simply prepend "get" and "set" to the name of the attribute. So an attribute called firstName would have the methods getFirstName and setFirstName.

The instance methods described above cover three of the four basic SQL commands. This leaves SELECT unimplemented. To implement it, we turn to static methods. Unlike the others, the SELECT command does not operate on existing objects. The point of a SELECT query is to retrieve data from the database. In an object-oriented application, you must create new objects to represent data selected from the database. Therefore, it is necessary to use static methods that do not rely on instance data.

Therefore, we'll write a static method that sends SELECT queries to a database and creates new Model objects from the data returned. Unlike the other methods considered so far, there are often several methods within a Model class that need to select data. This is because there are usually different contexts in which to create new objects. We'll implement the two methods that almost every application needs: Generic Where and Primary Key. For better reuse of code, we'll implement Primary Key in terms of Generic Where. Only the latter needs to issue SQL.

Generic Where
This is the most versatile and common select method. An SQL WHERE clause is passed into it as a parameter (or generated from other parameters) and it sends a SELECT query to the database containing this WHERE. Out of the resulting data, an array of Model objects is created. Because of the flexibility of the WHERE clause, this method can be leveraged by more specialized select methods, such as the Primary Key select that follows.

Primary Key
Well-designed relational tables almost always have a primary key. If you know a primary key value, you can retrieve a single row of data from the table. A Model class uses the method to create a single object corresponding to a row of data. We implement this method by creating an SQL WHERE clause containing the value of the primary key and then calling the Generic Where select, previously described, to execute the query. Since we are sending in the value of the primary key, we know we will get an array containing a single row in return. This method then returns this single object.

You might consider Primary Key to be a utility or convenience function built on top of Generic Where.

9.3.2. Implementing the Model

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:

getByPrimaryKey( )
A static method that creates a single object based on a primary key. This method calls the generic get method to perform the actual query.

The following are constructors:

new( )
A generic constructor that simply creates an empty object.

populate_publisher( )
A static method that creates a new Publisher object based on data from a result set. This is a utility method used by get. The advantage of making it a separate method is that it can be used externally by other Model classes that need to create new Publisher objects.

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.

DB
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.

mysql
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.

Cache
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.

Figure 9-1

Figure 9-1. Function hierarchy

9.3.2.2. Methods that build and execute SQL

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.

Example 9-6. Methods that build and execute SQL

#################################################
# 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);
  
}

9.3.2.3. Methods that handle WHERE clauses

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 AND element))

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.

Example 9-7. 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++;
        }
    }
  }

}

9.3.2.7. The DB class

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.

Example 9-11. 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;

9.3.2.8. The mysql class

In this class, we have tried to extract and isolate calls to methods that are specific to a particular database server.

new( )
This is the generic object constructor.

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.

9.3.3. Example of the Model's Use

Now that we've seen the Model class and all its supporting classes, let's look at how it is used in practice. The following snippets of code would be part of the Controller layer of the application: the layer that performs all the logic. Note how all the actual database calls are hidden from this layer. It uses just calls to the Model class.

Each file in the Controller layer indicates that it will use the Publisher class:

use CBDB::Publisher;
my $VERSION = 1.0;

A new object is created as follows:

my $pub = new CBDB::Publisher( );

Now let's set some data. The following creates a new publisher in our program (but not the database). We're in a hurry, so we can't be bothered with good spelling.

$pub->setName("Joe's Boks");

Note that we didn't set the id field of the publisher. The ID is an auto-increment field taken care of automatically by the database. A more abstract way of understanding our approach is that the ID is not a real-world property of this object. It exists only because the object-relational model we're using internally requires it. Therefore, at the controller level, we don't have to worry about assigning it or making sure it's unique.

We'll see what we just created:

print 'Our new publisher is ' . $pub->getName( ) . "\n";

If the program were to terminate at this point, this object's data would be lost. To make it persistent, we need to save it to the database:

$pub->create( );

Now the object has been created in the database and can be retrieved by another application, or by our application during a subsequent run. The database has assigned a primary key, which we can store for later use to find the object again:

my $new_id = $pub->getId( );

Suppose we want to retrieve the object in another part of our program. We'll use the getByPrimaryKey( ) call with the ID we stored earlier. The getByPrimaryKey( ) is static; we call it on the Publisher class itself instead of on an object from that class.

my $pub2 = CBDB::Publisher::getByPrimaryKey($new_id);

Because of the caching mechanism, $pub2 is the same object in memory as $pub. We can check this identity, if we're curious.

if ($pub2 != $pub) {
   print "Whoops! Something isn't working right!\n"
}

Let's change some data. For instance, we can fix the typo we introduced at the beginning of this section.

$pub2->setName("Joe's Books");

At this point, the data has been changed in the object only, not in the underlying database. However, because all active instances of this object in our program are references to the same object, this change takes place everywhere in the application immediately. Thus, the following statement will print Joe's Books, not Joe's Boks, even though we didn't explicitly touch the data to which $pub points.

print "The publisher's name is now " . $pub->getName( ) . "\n";

Now let's save these changes to the database. We can use either $pub or $pub2 to indicate the data.

$pub2->update( );

Later, we decide we're finished with this data and need to delete the row from the database.

$pub->remove( );

The underlying row in the database has now been deleted. However, this object (as well as $pub2) still contains the data until the program terminates or destroys the objects. This may be useful in case we want to refer to some property that the publisher used to have, as we do in the informational message below.

print "The publisher " . $pub->getName( ) . " was just erased.\n";

We hope that this little tour has shown the value of planning your program's structure so it is flexible and maintainable. Underneath DBI and CGI and the MVC methodology, it's just a bunch of INSERT, SELECT, and other SQL statements.



Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.