Building a Large-scale E-commerce Site with Apache and mod_perl
by Perrin Harkins
|
Pages: 1, 2, 3, 4, 5
Templates
Both the HTML and the formatting logic for merging application data into it is stored in the templates. They use a CPAN module called Template Toolkit, which provides a simple but powerful syntax for accessing the Perl data structures passed to them by the application. In addition to basics such as looping and conditional statements, it provides extensive support for modularization, allowing the use of includes and macros to simplify template maintenance and avoid redundancy.
We found Template Toolkit to be an invaluable tool on this project. Our HTML coders picked it up quickly and were able to do nearly all of the templating work without help from the Perl coders. We supplied them with documentation of what data would be passed to each template and they did the rest. If you have never experienced the joy of telling a project manager that the HTML team can handle his requested changes without any help from you, then you are seriously missing out!
Template Toolkit compiles templates into Perl bytecode and caches
them in memory to improve efficiency. When template files change on
disk they are picked up and re-compiled. This is similar to how other
mod_perl systems like Mason and Apache::Registry work.
By varying the template search path, we made it possible to assign templates to particular sections of the site, allowing a customized look and feel for specific areas. For example, the page header template in the bookstore section of the site can be different from the one in the video game store section. It is even possible to serve the same data with a different appearance in different parts of the site, allowing for co-branding of content.
This is a sample of what a basic loop looks like when coded in Template Toolkit:
[% FOREACH item = cart.items %]
name: [% item.name %]
price: [% item.price %]
[% END %]
Controller Example
Let's walk through a simple Hello World example that illustrates how the Model-View-Controller pattern is used in our code. We'll start with the controller code.
package ESF::Control::Hello;
use strict;
use ESF::Control;
@ESF::Control::Hello::ISA = qw(ESF::Control);
use ESF::Util;
sub handler {
### do some setup work
my $class = shift;
my $apr = ESF::Util->get_request();
### instantiate the model
my $name = $apr->param('name');
# we create a new Model::Hello object.
my $hello = ESF::Model::Hello-E<gt>new(NAME =E<gt> $name);
### send out the view
my $view_data{'hello'} = $hello->view();
# the process_template() method is inherited
# from the ESF::Control base class
$class->process_template(
TEMPLATE => 'hello.html',
DATA => \%view_data);
}
In addition to the things you see here, there are a few interesting
details about the ESF::Control base class. All requests are
dispatched to the ESF::Control->run() method first, wrapping
them in a try{} block before calling the appropriate handler()
method. It also provides the process_template() method, which runs
Template Toolkit and then sends the results with appropriate
HTTP headers. If the Controller specifies it, then the headers can include
Last-Modified and Expires for control of page caching by the
proxy servers.
Now let's look at the corresponding Model code.
package ESF::Model::Hello;
use strict;
sub new {
my $class = shift;
my %args = @_;
my $self = bless {}, $class;
$self{'name'} = $args{'NAME'} || 'World';
return $self;
}
sub view {
# the object itself will work for the view
return shift;
}
This is a simple Model object. Most Model objects would have some
database and cache interaction. They would include a load() method
that accepts an ID and loads the appropriate object state from the
database. Model objects that can be modified by the application would
also include a save() method.
|
Related Reading
Perl in a Nutshell, 2nd Edition |
Note that because of Perl's flexible OO style, it is not necessary to
call new() when loading an object from the database. The load()
and new() methods can both be constructors for use in different
circumstances, both returning a blessed reference.
The load() method typically handles cache management as well as
database access. Here's some pseudo-code showing a typical load()
method:
sub load {
my $class = shift;
my %args = @_;
my $id = $args{'ID'};
$self = _fetch_from_cache($id) ||
_fetch_from_database($id);
return $self;
}
The save method would use the same approach in reverse, saving first to the cache and then to the database.
One final thing to notice about our Model class is the view()
method. This method exists to give the object an opportunity to shuffle
it's data around or create a separate data structure that is easier for
use with a template. This can be used to hide a complex implementation
from the template coders. For example, remember the partitioning of the
product inventory data that we did to allow for separate cache
expiration times? The product Model object is really a façade
for several underlying implementation objects, but the view() method
on that class consolidates the data for use by the templates.
To finish our Hello World example, we need a template to render the view. This one will do the job:
<HTML>
<TITLE>Hello, My Oyster</TITLE>
<BODY>
[% PROCESS header.html %]
Hello [% hello.name %]!
[% PROCESS footer.html %]
</BODY>
</HTML>
Performance Tuning
Since Perl code executes so quickly under mod_perl, the performance
bottleneck is usually at the database. We applied all the documented
tricks for improving DBD::Oracle performance. We used bind
variables, prepare_cached(), Apache::DBI, and adjustments to the
RowCache buffer size.
The big win of course is avoiding going to the database in the first place. The caching work we did had a huge impact on performance. Fetching product data from the Berkeley DB cache was about 10 times faster than fetching it from the database. Serving a product page from the proxy cache was about 10 times faster than generating it on the application server from cached data. Clearly, the site would never have survived under heavy load without the caching.
Partitioning the data objects was also a big win. We identified several different subsets of product data that could be loaded and cached independently. When an application needed product data, it could specify which subset was required and skip loading the unnecessary data from the database.
Another standard performance technique we followed was avoiding
unnecessary object creation. The Template object is created the
first time it's used and then cached for the life of the Apache
process. Socket connections to search servers are cached in a way
similar to what Apache::DBI does for database connections. Resources
that are used frequently within the scope of a request, such as
database handles and session objects, were cached in mod_perl's
$r->pnotes() until the end of the request.


