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'));
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};
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); }
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.