PPO Example

From TheSeed
Jump to navigation Jump to search

Introduction

The PPO allows you to use the object orientation of perl, with persistent store of the object in mySQL. The objects you create with the PPO are a direct representation of the database, meaning that changes to an object will immediately be stored. On this page, you will get a detailed example of the usage of the PPO. For a short overview, please refer to Persistent Perl Objects.

This guide is split into six parts:

  • setup of PPO
  • creation of an object schema
  • generation of the modules
  • using PPO in your scripts
  • customize access to objects
  • troubleshooting

Setup

For the course of this example, let us assume we want to create a little application that handles the management of users. We will call this application UserManagement. The programmer doing this will be called Joe User, his login is juser.

Since we are experimenting with new stuff, we want to do this in our sandbox. If your environment does not automatically do this, you must first source the config file, depending on your shell, either config.sh or config.csh.

cd ~/FIGdisk/config/
source fig-user-env.sh

or

cd ~/FIGdisk/config/
source fig-user-env.csh

The source code of PPO resides in CVS, so we now switch to the main source directory and check out PPO. Note that you only need the -d option, if you have not automatically done so in your environment. You must put your login in the string where it says juser.

cd ~/FIGdisk/dist/releases/current/
cvs -d :ext:juser@biocvs.mcs.anl.gov:/disks/cvs/bio checkout PPO

Something like the following should happen:

cvs checkout: Updating PPO
U PPO/.cvsignore
U PPO/DBMaster.pm
U PPO/DBObject.pm
U PPO/DBObjectCache.pm
U PPO/DBSQLArray.pm
U PPO/Makefile
U PPO/PPOBackend.pm
U PPO/PPOGenerator.pm
U PPO/generate.pl
cvs checkout: Updating PPO/PPOBackend
U PPO/PPOBackend/MySQL.pm
U PPO/PPOBackend/SQLite.pm

The PPOGenerator.pm contains the functionality to create the neccesary files from the object schema you create, so you can access your objects. The generate.pl script is called to generate the perl modules and setup the databases. The DBMaster.pm, DBObject.pm, DBSQLArray.pm and DBObjectCache.pm contain the core functionality of PPO. The PPOBackend.pm module and it's subdirectory provide the PPO with access to different database technologies (currently MySQL and SQLite).

Object Schema

Before we can create an object schema for our little application, we create a directory for it.

mkdir ~/FIGdisk/dist/releases/current/UserManagement

Within this directory we create the schema file with the text editor of your choice. In our example, the file will be called UserManagement.xml. To start off, we tell the xml, that we are using xml.

<?xml version="1.0" encoding="UTF-8"?>

Then we make an opening and closing tag for our project. PPO supports the integration of multiple schemata into one superspace. Our user management for example, might be part of a larger application. Other schemata might be referencing to objects in our UserManagement space. This larger scale concept is called Superspace. Since we have no large scale concept yet, we leave the superspace blank.

<?xml version="1.0" encoding="UTF-8"?>
<project_space label="UserManagement" superspace="">

</project_space>

Now we are ready to create the first object. Every object needs a name and since we want to create a user management, we need a User. The user object should also have some attributes, e.g. firstName, lastName, login and password. Each user must have a login, so we make that attribute mandatory to make sure a user object cannot be created without a login.

<?xml version="1.0" encoding="UTF-8"?>
<project_space label="UserManagement" superspace="">

  <object label="User">
    <scalar label="firstName" type="CHAR(250)" />
    <scalar label="lastName" type="CHAR(250)" />
    <scalar label="login" type="CHAR(250)" mandatory="1" />
    <scalar label="password" type="CHAR(250)" />
  </object>

</project_space>

Every attribute must have a type. In case of scalar attributes, this can be any valid mySQL data type. You can find information about mySQL data types here.

When choosing names for Objects and their attributes, bear in mind that you may not use words reserved by mySQL. A current list of these words can be found here.

Now we also want to make sure no two users get the same login. To achieve this we need to create an index. To create an index, include all attributes that should be contained in the index. If we assume for example that every user is unique if you take a combination of first and last name, include both firstName and lastName inside the index tag. Since there might to two people with the same first and last name, we rather use the login as the index. This will make sure no two users will get the same login. Also we want to introduce a default value. We will default the password for every user to 'secret'. Of course you know that in a real application you would a) encrypt the password and b) never set a default password.

<?xml version="1.0" encoding="UTF-8"?>
<project_space label="UserManagement" superspace="">

  <object label="User">
    <scalar label="firstName" type="CHAR(250)" />
    <scalar label="lastName" type="CHAR(250)" />
    <scalar label="login" type="CHAR(250)" mandatory="1" />
    <scalar label="password" type="CHAR(250)" default="secret" />
    <index>
      <attribute label="login" />
    </index>
  </object>

</project_space>

Multiple indices require multiple index tags. However, it is unlikely you will need more than one index for one object type.

To make things a little more complex, we would like to store the organization of every user. Since multiple users are likely to share the same organization, we want to create a new object type Organization to maintain normalization. The User object will then receive an attribute which is a reference to an Organization object. In this case, the type of the attribute is the name of the referenced object type. To make sure no two organizations have the same name, we index the name of the organization. This will also allow us to init an Organization object via its name.

<?xml version="1.0" encoding="UTF-8"?>
<project_space label="UserManagement" superspace="">

  <object label="User">
    <scalar label="firstName" type="CHAR(250)" />
    <scalar label="lastName" type="CHAR(250)" />
    <scalar label="login" type="CHAR(250)" mandatory="1" />
    <scalar label="password" type="CHAR(250)" />
    <index>
      <attribute label="login" />
    </index>
    <object label="organisation" type="Organisation" />
  </object>

  <object label="Organisation">
   <scalar label="name" type="CHAR(255)" mandatory="1" />
   <scalar label="abbreviation" type="CHAR(255)" />
   <scalar label="url" type="CHAR(255)" />
   <index>
     <attribute label="name" />
   </index>
 </object>

</project_space>

The last thing we do is to introduce an array attribute. We want every user to have a list of rights, so we can differentiate which user may do what in our little application.

<?xml version="1.0" encoding="UTF-8"?>
<project_space label="UserManagement" superspace="">

  <object label="User">
    <scalar label="firstName" type="CHAR(250)" />
    <scalar label="lastName" type="CHAR(250)" />
    <scalar label="login" type="CHAR(250)" mandatory="1" />
    <scalar label="password" type="CHAR(250)" />
    <index>
      <attribute label="login" />
    </index>
    <object label="organisation" type="Organisation" />
    <array>
     <scalar label="rights" type="CHAR(255)" />
    </array>
  </object>

  <object label="Organisation">
   <scalar label="name" type="CHAR(255)" mandatory="1" />
   <scalar label="abbreviation" type="CHAR(255)" />
   <scalar label="url" type="CHAR(255)" />
   <index>
     <attribute label="name" />
   </index>
 </object>

If you want to have multiple array attributes within one object, put them all in the same array tag. You may also have array attributes which are references to objects. Be careful though, usually object array attributes can be easily substituted by refining the schema and they are computationally very expensive.

Module Generation

Now that the object schema is complete, we can generate the modules with the generate script. The script is located in the PPO directory and must be called with a set of parameters.

perl ~/FIGdisk/dist/releases/current/PPO/generate -f schemafilename -t targetdirectory -d databasename

In our case the schemafilename will be UserManagement.xml, the target directory will be ~/FIGdisk/dist/releases/current/UserManagement/ and the database name will be UserManagement. This will produce the following files:

UserManagement.pm
UserManagement.sql
UserManagement/ObjectBase.pm
UserManagement/User.pm
UserManagement/Organization.pm

Now you can setup the mySQL database by just calling

mysql -u root < UserManagement.sql

This will generate all necessary tables for you. You are now ready to use the object schema you created. However, the modules User.pm and Organization.pm allow you to customize all calls to your objects. You can read more about this in the Customize section.

Example Script

Now we will create a little script called user_management.pl which will use our freshly created object schema. To be nice we use strict and warnings and for the PPO we need to use DBMaster. Then the first thing we do is get a DBMaster object. This is our general access mechanism to all features of PPO. The only argument the DBMaster needs is the database name.

use strict;
use warnings;
use DBMaster;

my $dbmaster = DBMaster->new('UserManagement');

Using the DBMaster object, we can now create a new User object. We pass all attributes to the creator using a hash structure. We can omit any attributes we wish, except for the madatory attribute login. Also, we created and index on login, so we may not create objects with a value for login already posessed by another object. To ensure this does not happen, we try to init a User object with the login we create.

my $login = 'juser';
my $firstName = 'Joe';
my $lastName = 'User';
my $password = 'secret';

my $user;
unless ( $user = $dbmaster->User->init( { login => $login } ) ) {
 $user = $dbmaster->User->create( { firstName => $firstName,
                                    lastName  => $lastName,
                                    login     => $login,
                                    password  => $password } );
}

Now the new_user variable will contain a reference to the newly created object. If the object creation failed, the variable will be undef. Now let's create another object, this time of the type Organization.

my $org_name = 'Acme Industries';
my $org_abbrev = 'AI';
my $org_url = 'www.acme.com';

unless ( $dbmaster->Organization->init( { name => $org_name } ) ) {
 my $new_organization = $dbmaster->Organization->create( { name         => $org_name,
                                                           abbreviation => $org_abbrev,
                                                           url          => $org_url } );
}

Joe User is part of Acme Industries, so let us add this information to his user object.

$new_user->organization( $new_organization );

That was easy. Now let's examine if it all worked correctly and print the information in the user object.

print "User Summary\n";
print "First Name   : " . $new_user->firstName() . "\n";
print "Last Name    : " . $new_user->lastName() . "\n";
print "Login        : " . $new_user->login() . "\n";
print "Password     : " . $new_user->password() . "\n";
print "Organization : " . $new_user->organization->name() . "\n";

That will print out the data stored in our newly created objects. Let us imagine we have created a number of users and we want to get them all.

my $all_users = $dbmaster->User->get_objects( {} );
foreach my $user (@$all_users) {
 print $user->firstName . " " . $user->lastName . "\n";
}

Now imagine that Acme Industries went bankrupt and all their users should be deleted from our database.

my $acme = $dbmaster->Organization->init( { name => 'Acme Industries' } );
my $acme_users = $dbmaster->User->get_objects( { organization => $acme } );
foreach my $acme_user ( @$acme_users ) {
 $acme_user->delete();
}

This concludes the PPO example. The next chapter talks about customization of your object modules.

Customize

The PPO allows you to customize the access methods for your objects, you can even create new methods. Let's pick up the example from above about the UserManagement. Imagine you always want to retrieve the list of users, ordered by name. The normal retrieval method get_objects would retrieve the data in the order of creation by default. The following example shows how this could be handled by overwriting the get_objects method.

UserManagement/User.pm

package WebApplicationServer::User;

use strict;
use warnings;

1;

# this class is a stub, this file will not be automatically regenerated
# all work in this module will be saved

sub get_objects {
 # get the parameters given to the function
 my ($self, $values) = @_;
 
 # call the original method
 my $objects = $self->SUPER::get_objects($values);
 
 # do the sorting
 my @sorted_objects = sort { $a->lastName cmp $b->lastName || $a->firstName cmp $b->firstName } @$objects;
 
 # return the result
 return \@sorted_objects;
}

Troubleshooting

Problem: I call generate.pl and it tells me 'cannot open mysql_reserved'.

Solution: You must call generate.pl from the PPO directory, since mysql_reserved is expected to be in the directory you call generate.pl from.