The 2002 Perl Advent Calendar
[about] | [archives] | [contact] | [home]

On the 24th day of Advent my True Language brought to me..
Pixie

Yesterday we looked at Class::DBI, a module that lets us create objects that represent the rows stored in a database. Today we're going to look at Pixie, a module that allows us to store data in rows of the database to represent objects. The distinction between the two is a subtle, but important one.

A relational database is a tightly constrained system, where the database knows exactly what is being stored in it and how it relates to other data in it. Using this knowledge it can do powerful searches for you and produce. On the other hand setting up all this takes quite a bit of effort on the programmers behalf as he must try and map each object from the database to one or more rows in the table.

This is in direct contrast to the way Pixie works. Pixie allows you, with no fuss, to simply store complex objects in the database and pull them out again. You don't have to worry about setting up mapping code like you do with Class::DBI, and you don't have to worry about creating tables for each different object you want to store. Pixie will simply serialise up the object, object by object and stores it in the database as binary data.

This does mean that Pixie can't perform complex searches like a true relational database, but what it does mean instead is that objects stored in Pixie don't have any artificial hierarchy instilled in them through their trip through the database. Suddenly any relationship that makes sense to you in code can instantly be stored in the database. This sudden escape from the constraints is truly liberating. I find this freeform storage system it particularly useful for storing session data for people between pages on a website. Because the session object isn't constrained by the database I can add new sections to it and store ad hoc data in it depending on the need of any particular page on my site.

Before we begin storing objects, we must first get Pixie to configure the database so that it has all the correct tables to store the Pixie records in it.

  #!/usr/bin/perl
  # turn on Perl's safety features
  use strict;
  use warnings;
  # setup the database in the file 'pixie.db' 
  # with no username or password
  use Pixie::Store::DBI;
  Pixie::Store::DBI->deploy("dbi:SQLite:pixie.db", '', '');

Obviously the user that you connect to the database must have sufficient privileges to create tables and so forth (which you always will have with a SQLite database at least.)

Once the database is created then inserting objects into it couldn't be simpler. All you need to do is create a pixie object and use it to insert any objects that you want to store:

  # create a pixie that's connected to the database
  use Pixie;
  my $pixie = Pixie->new();
  $pixie->connect("dbi:SQLite:pixie.db", '', '');
  
  # create an object, any old object, it doesn't need to be
  # anything special or know how to store itself or anything
  use Person;
  use Math::BigInt;
  use Time::Piece;
  my $person = Person->new(
     firstname => "Mark",
     lastname  => "Fowler",
     username  => "2shortplanks",
     email     => 'mark@perladvent.com',
     dob       => Time::Piece->strptime("4 Mar 1978",
                                       "%d %b %Y"),
     credit    => Math::BigInt->new("5000"),
  );
  # store the object and all objects it
  # contains in the database
  my $cookie = $pixie->insert($person);

The cookie we get back is a unique identifier for the object that we've just stored. If we want an object back from Pixie then all we need do is request the object for that cookie.

  # get the person's details back again
  my $mark = $pixie->get($cookie);
  # check we got an object back in $mark
  defined( $mark ) or die "Unknown cookie '$cookie'";
  # set the greeting string
  $greeting =  "Hello " . $person->firstname .
               " "      . $person->lastname;

If we make any changes an object that we've stored in the database then all we need to do is reinsert it. Pixie will be intelligent enough to realise that this object was stored before and that it should change the object in there not create a second instance of it.

  # actually, set my real email address not the
  # Perl advent calendar one
  $person->email('mark@twoshortplanks.com');
  # reinsert $mark to store it in the database again
  $pixie->insert($mark);

Pixie will also Do The Right Thing when it comes to storing changes to objects that are sub objects of what we're inserting, assuming that the sub object have been pulled out of Pixie too.

  # spend ten pounts, therefore lower my credit limit
  $person->credit->bsub(10);
  # store it again
  $pixie->insert($person);

I normally store the $cookie string for each person's session in their browser cookies so that whenever they show up at my site I can look up their session object. Sometimes however my users decide to user another computer or a different browser and I can't get this cookie back again. What I need to do then is offer them a way of getting at their data without their cookie.

Pixie can bind a name to object which can be used instead of the cookie to retrieve the object:

  # bind my username to my object
  $pixie->bind_name($mark->username => $mark);

Data can be extracted with the get_object_named method that's pretty much identical to get, but it uses a bound name rather than a cookie id

  # get me back again
  $pixie->get_object_named("2shortplanks");

As way of a demonstration, here's a sample CGI script that will get the session of a person for a person based on either their cookie or their username and password, and will create a new session for them if they don't supply either.

  #!/usr/bin/perl
  # turn on Perl's safety features
  use strict;
  use warnings;
  # get the cookie back
  my $cgi = CGI->new();
  my $cookie   = $cgi->cookie("cookie");
  my $username = $cgi->param("username");
  my $password = $cgi->param("password");
  # my session
  my $session;
  # try and get the session with either the username or
  # cookie
  if ($username)
  {
    # get the object named the same as their username
    $session = $pixie->get_object_named($username);
  
    # don't let them get access to the session if they
    # got the password wrong
    if (defined($session) && $session->password ne $password)
    {
        # just print the invalid login page and exit
        print $cgi->header;
	print invalid_login();
        exit;
    }
  }
  else
  {
    # get with the cookie
    $session = $pixie->get($cookie);
  }
  # check we got a session out in the end
  unless ($session) 
  {
    # No session recreated?  Create a new session then!
    my $person = Person->new();
    # create 
    $cookie = $pixie->insert($person)
  }
  # print the header containing the cookie
  print $cgi->header(-type => 'text/html',
                     -cookie => $cgi->cookie(-name  => 'cookie'
                                             -value => $cookie,
                                             -expires => '+1M'));

Proxy objects

One of the cleverest things about Pixie is the way that it returns object trees from the database. Consider the situation where you have an object in the database and that object has references to another few objects, and each of those objects have a few more objects themselves and so on and so on. Suddenly pulling the top object of the database requires a few hundred objects to be pulled out of the database. This is pretty wasteful if you only needed to check a simple attribute of the topmost object.

To avoid this situation Pixie creates what are called "Proxy" objects. Whenever you pull an object out of the database the objects it references in turn are not pulled out of the database, but instead in the place in the object where they would have been stored one or more "Proxy" object are created. As soon as you call any method on these objects they are magically upgrade themselves to the real object by extracting themselves from the database (in a similar fashion to the way Object::Realize::Later objects upgrade themselves.) What this means is that Pixie is very efficient with database access, but you can't access the data structures of objects that haven't been extracted yet. For example:

  my $person = $pixie->get($cookie);
  # wont work, as shopping_basket is only a proxy object
  # you really should be using accessors methods!
  print $person->shopping_basket->{type};
  # works, as causes shopping_basket to be sucked in
  print $person->shopping_basket->value();
  # now works as shopping basket has been loaded and is
  # the real object now.
  print $person->shopping_basket->{type};

Complicity

Although Pixie can store an awful lot of objects nicely and automatically, it does have problems when the data that is part of an object is inaccessible to Perl itself. One such case is when Perl is interfacing with a C library. Some of the data Pixie needs to store for that object may be squirrled away by the library in some place that Perl can't find it.

For this reason you shouldn't expect Pixie to be infallible when it comes to storing objects, and you should see if a module is implemented in C before storing it with Pixie. However, even if it is all is not lost. Pixie has a system called "Complicity" that allows you to override the way it stores and retrieves objects.

Pixie declares a few methods in the UNIVERSAL class that all classes inherit from. These methods tell pixie how to store and retrieve normal objects, but any class can override these methods to provide a custom way of storing these objects.

As way of an example consider a GD Image. As the GD module is a wrapper to a C library Perl has no way of accessing the image data. However, we can instruct GD to dump out the image in the gd2 file format (the format that GD uses to store temporary copies of image on disk,) so we could store that in the database and recreate the original image object from that instead.

Firstly we need to convince Pixie that we can save this object to the database. To do this we create a fully qualified method subroutine. This declares the subroutine right in the GD::Image namespace meaning that when GD::Image->px_storable is called it will be executed.

  # yes yes, we *can* store it
  sub GD::Image::px_is_storable { return 1 };

Now we need to create the px_freeze method that must return a plain object that Pixie is capable of storing whenever $image->px_freeze is called. The resulting object needs to be in a separate class to GD::Image, so we choose the name Memento::GD::Image.

  sub GD::Image::px_freeze
  {
    my $gd = shift;
    # get the image as binary data
    my $bin = $gd->gd2;
    # return that data in a data structure blessed into a new class
    return bless [ $bin ], "Memento::GD::Image";
  }

Finally we must write a px_thaw method for Memento::GD::Image objects that will will recreate the original GD::Image for us.

  sub Memento::GD::Image::px_thaw
  {
    my $memento = shift;
    # get the binary data out of the object
    my $bin = $memento->[0];
    # create a new GD image from it and return it
    return GD::Image->newFromGD2Data($bin);
  }

Conclusion

Pixie presents a powerful and robust solution to storing objects that can easily be adapted to store any possible object. By choosing to use a relational and object database for the right tasks you can solve your data storage needs in a very much more efficient manner than with either solution alone.

  • Documentation on Pixie's Complcitiy