Channels ▼
RSS

Web Development

Programming Persistent Objects with Class::PObject


August, 2003: Programming Persistent Objects with Class::PObject

Programming Persistent Objects with Class::PObject

The Perl Journal August, 2003

By Sherzod B. Ruzmetov

Sherzod is a student at Central Michigan University. He is the author of several CPAN libraries, including Class::PObject. He can be reached at sherzodr@cpan.org.


Ever wished you could just forget about database-specific routines to manage data files, BerkelyDB, MySQL, PostgreSQL, and the like, and use pure Perl for managing your application data? Or have you ever written a program with a specific database in mind, and had to port the application to another database and wished there was an easier way?

The philosophy behind persistent objects addresses just this issue—to represent persistent data (otherwise stored on disk) as a software object, and provide methods and behaviors for manipulating its content.

What is a Persistent Object?

A persistent object can be thought of as another way of representing data on disk. It may help to think of a persistent object as a single row of a database table. It can look something like Example 1.

The whole record represents an object, and each column of the record represents attributes of the object. You use object methods from within your programs to create or access the data without having to run any database-specific queries.

What is the advantage? By treating the real data as an object, we achieve a higher level of database abstraction. This allows the development of platform-independent code, since our programs don't really care anymore how the data is stored and retrieved from the disk. It's up to the object drivers to perform these tasks transparently. Data, on the other hand, can be stored in plain files, BerkelyDB, Comma Separated Values (CSV), MySQL, or any other database system for which an object driver is available.

This design also makes development and maintenance easier, because maintainers will not need to learn implementation-specific database queries, but will only work with software objects. Class::PObject provides this framework.

Programming Style

The programming style of Class::PObject resembles that of standard Class::Struct, and allows us to create objects dynamically. Class::PObject imports a single function, pobject(), which takes arguments describing the class attributes.

In this section, we will tackle a real-world task: managing user accounts in a web project.

Let's try to think of a user as an object, and decide what attributes a single user can have. The minimum required attributes for a user might be name, password, and email. We'll stick to these three attributes for the sake of simplicity, and try to extend them later. The user can also have some behaviors. The most important of those behaviors is the ability to identify himself to our web site, a process also known as "login."

Declaring a Class

In this section, we will build a User class. There are two ways of declaring a class using Class::PObject: inside a dedicated .pm file or in an inline declaration. Inline declaration is the easiest, and you can declare it in anywhere, even inside your programs. Here is an example of declaring a User class using the inline syntax:

pobject User => {
  columns => ['id', 'name', 'password', 'email']
};

Unfortunately, objects created this way will not be accessible to other programs because they are embedded inside your program. A better way is to define objects in their dedicated class files. To do this, create a User.pm file, with the following content:

package User.pm
use strict;
use Class::PObject;
pobject {
  columns => ['id', 'name', 'password', 'email']
};
1;

Although it might be hard to believe, the aforementioned examples create a fully functional User class. The columns in the aforementioned class definitions are attributes of the object that will be stored into disk. In other words, think of it as columns of a record stored in a database table. (Another example User.pm file is available online at http://www.tpj.com/source/.)

The Generated pobject

The generated pobject provides the following methods:

  • new()--Constructor for creating a new object. Calling this method without any arguments will create an empty User object.
  • load([$id] | [\%terms] [,\%args])—Constructor for loading object(s) from disk.
  • save()—For storing the changes made to the object back to disk.
  • remove()—Removing the object from the disk.
  • remove_all()—Static class method for removing all the objects of the same type from the disk.

It also creates accessor methods for each of the declared columns. These methods carry the same name as column names. You can change the value of the column by passing an argument. To access the current value of the column, you call it without an argument. For example, to get the user's name, we can say:

my $name = $user->name();

To rename the user, we say:

$user->name("Sherzod");

Create a New Object

Now let's create a new user account:

use strict;
require User;
my $user = new User();
$user->name('Sherzod');
$user->password('secret');
$user->email('sherzodr@handalak.com');
$user->save();

Notice we're loading the User class and creating an instance of a User object using the new() constructor. This will create an empty user. In consecutive lines, we are defining the user's attributes. When we're done, we save the object. save() will flush all the in-memory data to disk.

At this point, don't worry about how and where the above data is being stored.

Loading Previously Stored Objects

All the objects should have a unique ID that distinguishes them from other objects of the same type. This ID is a primary key attribute of the object, called "id" by default. Class::PObject will make sure that every object will be assigned a unique, autoincrementing ID.

The most efficient way of loading an object is through its ID. We pass it to the load() method:

my $user = User->load($id);

You can also load objects by specifying a set of terms. These terms can be passed to load() as the first argument in the form of hash reference. Consider the following example, which is loading a user account with name "Sherzod."

my $user = User->load({name=>"Sherzod"}); 

We can now perform some modifications on this user. Say, we want to change the password:

$user->password('top_secret');
$user->save();

None of the modifications to the object will be flushed to disk unless we call save().

Loading Multiple Objects

If you call the load() method in array context, it will return a list of all the results matching your terms. If no terms are given, all the objects of the same type will be returned:

my @users = User->load();

Elements of @users hold all the User objects. Suppose we wanted to generate a list of all the users in our database:

for my $user ( @users ) {
  printf("[%03d] - %s <%s>\n", $user->id, $user->name, $user->email);
}

In addition to terms, you can pass the second argument to load() to perform such actions as sorting on a specific column, limiting the number of results returned, or returning spliced result sets:

# to return first 5 results ordered by 'name' column:
my @users = User->load(undef, {sort=>'name', limit=>5});
# to return results 10 through 25, sorting by 'name' in
# descending order:
my @users = User->load(undef, {sort   => 'name', direction=> 'desc',
                               offset => 10,     limit    => 25 });

Removing the Object

To delete the data an object is associated with, you can call the remove() method on that object. It is a nonreversible action, so use it only when you really mean it. The following example removes the user account with id 10:

my $user = User->load(10);
$user->remove();

and the following sample of code removes all the available Users:

my @users = User->load();
for my $user ( @users ) {
  $user->remove();
}

For most object drivers, removing all the data at once is a lot more efficient than removing them one by one, as we did in the aforementioned example. For this reason, Class::PObject also provides a remove_all() method, which does exactly what its name claims:

User->remove_all();

Note that it is a static class method, and is not associated with any specific object. So saying something like this is not very intuitive:

my $user = User->load();  # <-- returns any one User object
$user->remove_all();      # <-- huh?

Extending the Object's Interface

For some objects, accessor methods are not all you need. Many objects also need behaviors. A behavior is an action that an object can perform on its data. For example, an imaginary Window object could support behaviors such as open(), close(), move(), and so on. authenticate(), on the other hand, makes perfect sense for our User object. It's a procedure to be performed when a user submits a login form on a web page. The task of the authenticate() method is to validate the user's submitted name and password fields to the ones stored in the database. If they match, the user can enter.

To support this behavior, we need to open our User.pm file we created earlier, and define the authenticate() method:

package User;
use strict;
use Class::PObject;
pobject {
  columns => [
    'id', 'name', 'password', 'email'
  ]
};
sub authenticate {
  my $class = shift;  # <-- removing Class name from @_
  my ($name, $psswd) = @_;
  return $class->load({name=>$name, password=>$psswd});
}
1;
__END__;

Our authenticate() accepts two arguments, $name and $psswd, and returns a matching object. If no such account can be found, it will return undef. We now can use this new feature from within our programs like so:

# we take the form data using standard CGI.pm
my $name      = $cgi->param('name');
my $password  = $cgi->param('password');
# we then attempt to authenticate the user:
my $user = User->authenticate($name, $password);
unless ( defined $user ) {
  die "couldn't authenticate. Username and/or password are incorrect\n";
}

authenticate() is a class method, which makes sense, since we use it to retrieve a valid user object.

There is much more than that Class::PObject offers. For the details, you should always consult the latest library manual, available from your CPAN mirror.

On Object Drivers

So far we have only discussed the programming syntax of Class::PObject. Now let's talk about how and where all the data is stored. Objects created using Class::PObject rely on object drivers for mapping objects into physical data.

The User class we have been working with so far was built using the most basic declaration. We didn't even tell it what driver to use, where and how the data should be stored, and so forth. In such cases, Class::PObject falls back to default driver, "file." To use any other drivers, you should define the driver class attribute. datasource can be defined to pass arguments for drivers:

package User;
pobject {
  columns => ['id', 'name', 'password', 'email'],
  driver  => 'mysql',
  datasource => {
        DSN    => 'DBI:mysql:users',
        UserName => 'sherzodr',
        Password => 'secret'
  }
};

In the following section, we'll give a brief overview of various drivers available for Class::PObject, and how they map the object data to disk files.

File Driver

The file driver is a default driver used by Class::PObject. If datasource is provided, the driver will interpret it as a directory that objects will be stored in. If it's missing, it will create a folder in your system's temporary directory. The name of the folder will be the lowercased version of the class name, with nonalphanumerics replaced with underscores ("_").

The driver stores each object as a separate file, and uses Data::Dumper to serialize the data. The name of the file is in the form of "obj%05d.cpo," where "%05d" will be replaced with the object ID, with zeros padded if necessary.

As the number of objects in your database grows big, it will get less efficient and slower to manipulate objects in this form, since there will be lots of open/close and eval() calls.

CSV Driver

csv stores objects of the same type in a single file in CSV (Comma Separated Values) format. These objects can easily be loaded to your favorite spreadsheet application, such as Excel, or loaded to an RDBMS such as MySQL or PostgreSQL. It uses the DBI and DBD::csv libraries.

csv creates all the objects in your system's temporary folder unless you explicitly define datasource to be a hashref with the following keys:

  • Dir—Directory where the object will be stored.
  • Table—Name of the file that will hold this particular object. If you omit the Table name (recommended), it will default to the name of the object, nonalphanumerics underscored, and lowercased.

Here's an example using the csv driver:

package User;
pobject {
    columns => ['id', 'name', 'password', 'email'],
    driver  => 'csv',
    datasource => {
        Dir  => 'data/'
    }
};

MySQL Driver

The mysql driver can be used for storing objects in MySQL tables. Unlike the aforementioned database drivers, you first need to set up the table structure to be able to use the mysql driver. In other words, each pobject represents a single database table. Each column of the table represents an attribute of the object and is declared in the columns array.

Consider the following object configuration for our User class using the mysql driver:

package User;
pobject {
    columns => ['id', 'name', 'password', 'email'],
    driver  => 'mysql',
    datasource => {
        DSN => "dbi:mysql:users",
        UserName => "sherzodr",
        Password => "secret"
    }
};

You can optionally provide Table inside datasource if you want to store the user data in a different table.

Now, let's create a table for storing the above object. Remember, you should create at least all the columns mentioned in the columns class attribute. Another thing to remember is to make id as an AUTO_INCREMENT column. Consider the following table schema, which is designed for storing the just defined User objects:

CREATE TABLE user (
        id INT UNSIGNED NOT NULL AUTO_INCREMENT,
        name VARCHAR(30) NOT NULL,
        password CHAR(32) NOT NULL,
        email VARCHAR(80) NOT NULL,
        PRIMARY KEY(id)
);

Now, when we say:

my $u = new User();
$u->name('sherzodr');
$u->email('sherzodr@handalak.com');
$u->password('secret');
$u->save();

our MySQL table would then contain the following data as shown in Example 2.

Conclusion

Persistent objects allow you to forget the messy details of data storage, and to treat your data like you would any other software object. I hope this article gives you enough of an understanding of creating persistent objects with Class::PObject for you to get started using them in your own code. (For more example code for this article, see http::/www.tpj.com/source/.)

References

perlobj—The Standard Perl manual of Perl objects.

Class::PObject—The official manual for Class::PObject. The latest features will be available through your CPAN mirror. http://search.cpan.org/perldoc?Class::PObject/.

Class::DBI—Another utility for programming persistent objects.

Class::Struct—The Standard Perl library for automating creation of simple classes using C++'s struct-like syntax.

TPJ


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV