Sign In/My Account | View Cart  
advertisement


Listen Print

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

Perl in a Nutshell, 2nd Edition
By Stephen Spainhour, Ellen Siever, Nate Patwardhan

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.

Pages: 1, 2, 3, 4, 5

Next Pagearrow