Help - Search - Member List - Calendar
Full Version: Object persistence
WorkTheWeb Forums > Webmaster Resources > Perl Beginner Help
Support our Sponsors!
Scott R. Godin
This might be better suited for the DBI list, but primarily I'm
interested in learning more about the things necessary to enable me to
take an existing module's object and make its data persistent (via a
mysql database)

I've been giving thought to inheriting from the main module. Or not. in
no way am I sure what to do here, other than I know how I could do these
things in a more linear-programming fashion.

With Objects, though, I'm having a really tough time wrapping my brain
around the problem space. I mean, I grasp the concept of turning my
normally used functions, into the methods for the object... What I'm not
sure of is how to approach the problem.

The original object itself is at
http://www.webdragon.net/miscel/Subscriber.pm

One of the things I'm unclear on is that for the Subscriber::DB module,
should I be cloning the Subscriber object? Setting up multiple _init
methods to initialize their individual data-sets ?

(the Subscriber::DB object will need $database, $username, $password, a
DBI $dbh handle, and possibly a $hostname as well, in addition to the
data stored in the Subscriber object, that I wish to make persistent by
storing and retrieving it from the mysql database. )

I'm hoping to Module/object these functions so that I can re-use them
more efficiently in writing multiple programs for a client, towards a
particular goal. I've had other bits to work on while I studied this
portion of the problem, but now I'm at the point where I need to start
tackling it and am no closer to visualizing how it should work than I
was earlier. Can anyone offer some insight/examples from their own
experience with similar things? :)

I've read Damian Conway's "OOP" and while very good, it really
glosses-over the persistence thing with examples more suitable for
DB_File .. *sigh* .. And I still don't know whether I should be
inheriting from Subscriber or not..

Your insights would be greatly appreciated.

Jeff 'japhy' Pinyan
On Jul 13, Scott R. Godin said:

QUOTE
The original object itself is at http://www.webdragon.net/miscel/Subscriber.pm

(the Subscriber::DB object will need $database, $username, $password, a DBI
$dbh handle, and possibly a $hostname as well, in addition to the data stored
in the Subscriber object, that I wish to make persistent by storing and
retrieving it from the mysql database. )

The first thing you need to do is figure out the mapping from the old
methods to the new methods.

One gripe I have with Subscriber.pm is that you can't set fields after the
new() method, without knowing the construction of the object. It'd be
nice to have a set_XXX() method which set the field to a given value. You
could do this with AUTOLOAD and it'd be magic:

AUTOLOAD {
my $obj = shift;
(my $method = $AUTOLOAD) =~ s/.*:://;
if ($method =~ /^set(_w+)/) {
$obj->{$1} = shift;
}
else {
die "no such method '$method'"
}
}

Then you could do

my $who = Subscriber->new;
$who->set_zipcode('08536');

Or you could have a set() method:

sub set {
my $self = shift;

while (@_) {
my ($field, $value) = @_;
if (exists $self->{$field}) { $self->{$field} = $value }
else { die "no such field '$field'" }
}
}

Maybe you'd want to warn() there instead of die(), I don't know. The
point is, then you could write:

my $person = Subscriber->new;
$person->set(
_firstname => "Jeff",
_lastname => "Pinyan",
);

All Subscriber::DB objects would share the DBI object -- there's no need
for a billion database handles. I would also make Subscriber::DB inherit
from Subscriber... for a reason I'll show you in a moment.

If you're making a NEW object (that is, not loading from the DB), perhaps
you should make that the function of the 'new' method, and leave restoring
objects from the DB to a different method, such as 'load' or 'restore'.

my $x = Subscriber::DB->new(fname => "Jean", lname => "Valjean");

# vs.

my $y = Subscriber::DB->load(id => 24601);
# or
my $y = Subscriber::DB->new_from_id(24601);

These objects themselves should be hollow -- that is, they should only
contain as much as is needed to retrieve their data from the database. In
your case, I would guess that means just their id.

Then, when you do $y->get_greeting(), perhaps you want to do something
like:

# here's a global variable for all of Subscriber::DB
my $greeting_sql = $DBH->prepare(q{
SELECT salutation, firstname, lastname
FROM subscriber_db
WHERE id = ?
});

# Subscriber::DB::get_greeting
sub get_greeting {
my ($self) = @_;
my $id = $self->{_id};

$greeting_sql->bind_columns(
$self->{_salutation},
$self->{_firstname},
$self->{_lastname},
);
$greeting_sql->execute($id);

# now here is the magic!
return $self->SUPER::get_greeting;
}

This is where the inheritence counts. Subscriber::DB::get_greeting merely
populates enough of the hash for Subscriber::get_greeting to operate
correctly. The bind_columns() call tells the SQL statement to populate
those values (values in a hash) when it gets executed.

Now, if you had your Subscriber::AUTOLOAD set up, we could rewrite this
as:

sub get_greeting {
my ($self) = @_;
my $id = $self->{_id};

$greeting_sql->bind_columns(my ($salut, $fname, $lname));
$greeting_sql->execute($id);
$self->set_salutation($salut);
$self->set_firstname($fname);
$self->set_lastname($lname);

# magic as usual
return $self->SUPER::get_greeting;
}

*OR*, if you had Subscriber::set() like I showed, you could do:

sub get_greeting {
my ($self) = @_;
my $id = $self->{_id};

$greeting_sql->bind_columns(my ($salut, $fname, $lname));
$greeting_sql->execute($id);
$self->set(
_salutation => $salut,
_firstname => $fname,
_lastname => $lname,
);

# magic as usual
return $self->SUPER::get_greeting;
}

If you want to hear more, I can give you more. Let me know if this has
gone over your head though. I have a system in mind that would greatly
reduce the amount of code in each of Subscriber::DB's methods that link to
Subscriber's methods.

--
Jeff "japhy" Pinyan % How can we ever be the sold short or
RPI Acacia Brother #734 % the cheated, we who for every service
http://japhy.perlmonk.org/ % have long ago been overpaid?
http://www.perlmonks.org/ % -- Meister Eckhart

Jeff 'japhy' Pinyan
On Jul 13, Jeff 'japhy' Pinyan said:


QUOTE
Or you could have a set() method:

sub set {
my $self = shift;

while (@_) {
my ($field, $value) = @_;

That should be:

my ($field, $value) = (shift, shift);

QUOTE
if (exists $self->{$field}) { $self->{$field} = $value }
else { die "no such field '$field'" }
}
}

--
Jeff "japhy" Pinyan % How can we ever be the sold short or
RPI Acacia Brother #734 % the cheated, we who for every service
http://japhy.perlmonk.org/ % have long ago been overpaid?
http://www.perlmonks.org/ % -- Meister Eckhart

Jeff 'japhy' Pinyan
On Jul 13, Jeff 'japhy' Pinyan said:

QUOTE
# Subscriber::DB::get_greeting
sub get_greeting {
my ($self) = @_;
my $id = $self->{_id};

$greeting_sql->bind_columns(
$self->{_salutation},
$self->{_firstname},
$self->{_lastname},
);
$greeting_sql->execute($id);

It turns out I had these in the wrong order -- you should execute() FIRST,
and then bind_columns().

QUOTE
# now here is the magic!
return $self->SUPER::get_greeting;
}

If you want to hear more, I can give you more.  Let me know if this has gone
over your head though.  I have a system in mind that would greatly reduce the
amount of code in each of Subscriber::DB's methods that link to Subscriber's
methods.

I've written the code I mentioned just above. It seems (to me, at least)
to be pretty stream-lined. I will produce it if requested. I use a
couple tricks throughout, so I'll explain them, of course.

--
Jeff "japhy" Pinyan % How can we ever be the sold short or
RPI Acacia Brother #734 % the cheated, we who for every service
http://japhy.perlmonk.org/ % have long ago been overpaid?
http://www.perlmonks.org/ % -- Meister Eckhart

Jeff 'japhy' Pinyan
On Jul 13, Scott R. Godin said:

QUOTE
The first thing you need to do is figure out the mapping from the old
methods to the new methods.

I'm not quite certain what you're getting at, here. You mean, which methods
will get re-mapped to do things slightly differently for the DB version?

Right. What needs to be done in the DB methods? And I sort of answered
that: update a local copy of the object from the DB, and then call the
parent method.

QUOTE
I also added an $obj->set() method (again slightly different from the above so
you'd get warnings about unbalanced pairs) for handling multiple field/value
pairs which again checks to make sure the fields are allowed and accessible.
I'm pondering whether I should be doing the same thing twice (in new() and
set()), and whether it makes any sense to have new call (a slightly different
variant of) set.

I don't see why not.

sub new {
my $class = shift;
my $self = bless {}, $class;
$self->set(@_);
return $self;
}

There's no need for that '%' prototype on your set() function, by the way.
Methods don't pay attention to prototypes, anyway.

--
Jeff "japhy" Pinyan % How can we ever be the sold short or
RPI Acacia Brother #734 % the cheated, we who for every service
http://japhy.perlmonk.org/ % have long ago been overpaid?
http://www.perlmonks.org/ % -- Meister Eckhart

Scott R. Godin
Jeff 'japhy' Pinyan wrote:
QUOTE
On Jul 13, Scott R. Godin said:

The original object itself is at
http://www.webdragon.net/miscel/Subscriber.pm

Now updated some, as mentioned below :)

QUOTE
(the Subscriber::DB object will need $database, $username, $password,
a DBI $dbh handle, and possibly a $hostname as well, in addition to
the data stored in the Subscriber object, that I wish to make
persistent by storing and retrieving it from the mysql database. )


The first thing you need to do is figure out the mapping from the old
methods to the new methods.

I'm not quite certain what you're getting at, here. You mean, which
methods will get re-mapped to do things slightly differently for the DB
version?

QUOTE
One gripe I have with Subscriber.pm is that you can't set fields after
the new() method, without knowing the construction of the object.  It'd
be nice to have a set_XXX() method which set the field to a given
value.  You could do this with AUTOLOAD and it'd be magic:

True, I'd been meaning to get around to that, but hadn't quite. So, just
for you, I did. The weblink above has the current version.

QUOTE
AUTOLOAD {
my $obj = shift;
(my $method = $AUTOLOAD) =~ s/.*:://;
if ($method =~ /^set(_w+)/) {
$obj->{$1} = shift;
}
else {
die "no such method '$method'"
}
}

Although I did it slightly differently than this :)

QUOTE
Then you could do

my $who = Subscriber->new;
$who->set_zipcode('08536');

Or you could have a set() method:

sub set {
my $self = shift;

while (@_) {
my ($field, $value) = @_;
if (exists $self->{$field}) { $self->{$field} = $value }
else { die "no such field '$field'" }
}
}

I also added an $obj->set() method (again slightly different from the
above so you'd get warnings about unbalanced pairs) for handling
multiple field/value pairs which again checks to make sure the fields
are allowed and accessible. I'm pondering whether I should be doing the
same thing twice (in new() and set()), and whether it makes any sense to
have new call (a slightly different variant of) set.

QUOTE
Maybe you'd want to warn() there instead of die(), I don't know.  The
point is, then you could write:

my $person = Subscriber->new;
$person->set(
_firstname => "Jeff",
_lastname => "Pinyan",
);

yup, makes sense.

[snip]

QUOTE
If you want to hear more, I can give you more.  Let me know if this has
gone over your head though.  I have a system in mind that would greatly
reduce the amount of code in each of Subscriber::DB's methods that link
to Subscriber's methods.

I'd definitely like to discuss this further.. Great response, Jeff. I
really appreciate it! What I'll do is address different portions of your
response in different replies so we can have separate sub-threads on
smaller bits of the topic, so I can keep it all straight in my head :)

Scott R. Godin
Jeff 'japhy' Pinyan wrote:
QUOTE
On Jul 13, Scott R. Godin said:

The first thing you need to do is figure out the mapping from the old
methods to the new methods.


I'm not quite certain what you're getting at, here. You mean, which
methods will get re-mapped to do things slightly differently for the
DB version?


Right.  What needs to be done in the DB methods?  And I sort of answered
that:  update a local copy of the object from the DB, and then call the
parent method.

there will actually need to be a number of things handled by the
database module, but primarily I need to wrap my brain around how I'm
intending to handle the whole picture. I'm getting closer now, (thanks!)
but still can't 'visualize' the whole thing, yet.

QUOTE
I also added an $obj->set() method (again slightly different from the
above so you'd get warnings about unbalanced pairs) for handling
multiple field/value pairs which again checks to make sure the fields
are allowed and accessible. I'm pondering whether I should be doing
the same thing twice (in new() and set()), and whether it makes any
sense to have new call (a slightly different variant of) set.


I don't see why not.

sub new {
my $class = shift;
my $self = bless {}, $class;
$self->set(@_);
return $self;
}

true but this way, one doesn't get the initializing behaviour that new()
currently has which presets undef values to "" instead. (which is why I
was pondering this change moreso than usual :) I don't want to
re-_init() all the fields when merely settting one or two differently,
nor do I want to have it such that there are undef values in the object.
Tricky, but not impossible. Works fine now, I just don't know whether
the concept merits refactoring now that I have a separate set() method.

QUOTE

There's no need for that '%' prototype on your set() function, by the
way. Methods don't pay attention to prototypes, anyway.


oops, I left that in when I was testing it -- I'd forgotten something,
and couldn't figure out why it wasn't working like I expected..

I had done

my ($self, %_args);

instead of

my ($self, %_args) = @_;

eep =8)

Scott R. Godin
Jeff 'japhy' Pinyan wrote:
QUOTE
On Jul 13, Scott R. Godin said:

The original object itself is at
http://www.webdragon.net/miscel/Subscriber.pm

[snip]

Additionally uploaded my skeleton starting ideas on Subscriber::DB at

http://www.webdragon.net/miscel/DB.pm

QUOTE
All Subscriber::DB objects would share the DBI object -- there's no need
for a billion database handles.

ok, so possibly one should do it differently than I have, in my example.

QUOTE
I would also make Subscriber::DB
inherit from Subscriber... for a reason I'll show you in a moment.

If you're making a NEW object (that is, not loading from the DB),
perhaps you should make that the function of the 'new' method, and leave
restoring objects from the DB to a different method, such as 'load' or
'restore'.

right, now that at least makes good sense to me, and clearly separates
the two functions.

QUOTE
my $x = Subscriber::DB->new(fname => "Jean", lname => "Valjean");

# vs.

my $y = Subscriber::DB->load(id => 24601);
# or
my $y = Subscriber::DB->new_from_id(24601);

These objects themselves should be hollow -- that is, they should only
contain as much as is needed to retrieve their data from the database.
In your case, I would guess that means just their id.

Maybe.. this might be where my understanding of these things has gone
agley, but I'd envisioned it somewhat differently..

One thing I'd thought of recently would be to have a __DATA__ section
populated with a number of SQL queries (with placeholders) and a
shortname to identify it with, that I could call on at will.

But what I had thought of doing would be to populate the object via the
database and fill it entire, thus being able to call any of the
Subscriber methods on it without generating multiple calls to the
database for each one. (this could prove important in a template-driven
environment. Additionally the ultimate target of this will be a website
interface (user/admin) so that affects part of the picture too -- should
I even be considering at this point whether or not to plan ahead towards
mod_perl ? )

QUOTE
Then, when you do $y->get_greeting(), perhaps you want to do something
like:

# here's a global variable for all of Subscriber::DB
my $greeting_sql = $DBH->prepare(q{
SELECT salutation, firstname, lastname
FROM subscriber_db
WHERE id = ?
});

# Subscriber::DB::get_greeting
sub get_greeting {
my ($self) = @_;
my $id = $self->{_id};

$greeting_sql->bind_columns(
$self->{_salutation},
$self->{_firstname},
$self->{_lastname},
);
$greeting_sql->execute($id);

# now here is the magic!
return $self->SUPER::get_greeting;
}

This is where the inheritence counts.  Subscriber::DB::get_greeting
merely populates enough of the hash for Subscriber::get_greeting to
operate correctly.  The bind_columns() call tells the SQL statement to
populate those values (values in a hash) when it gets executed.

Admittedly that is a neat idea (and now I see how the SUPER:: bit works
into the picture, but as my comment above suggested, I'd like to avoid
too many calls to the database if at all possible.

QUOTE
Now, if you had your Subscriber::AUTOLOAD set up, we could rewrite this as:

sub get_greeting {
my ($self) = @_;
my $id = $self->{_id};

$greeting_sql->bind_columns(my ($salut, $fname, $lname));
$greeting_sql->execute($id);
$self->set_salutation($salut);
$self->set_firstname($fname);
$self->set_lastname($lname);
# magic as usual
return $self->SUPER::get_greeting;
}

*OR*, if you had Subscriber::set() like I showed, you could do:

[snip]
$self->set(
_salutation => $salut,
_firstname => $fname,
_lastname => $lname,
);

# magic as usual
return $self->SUPER::get_greeting;
}


One problem-space I'm still trying to wrap into the picture is the need
to be able to grab data from the database based on different criterion..
for example "everyone who has requested info by e-mail only", or the
opposite -- people who want their info snail-mailed to them.

so while iterating through the data-set resulting from the sql search,
the subscriber object gets populated with each, and then used to
generate the mailing labels, or the list of e-mails to be wrapped up and
used by the mailing list manager (NOT spamming but a special use list,
akin to Mail::SimpleList or that used by mailman (which is yet another
problem space of the project -- a Subscriber::Mailer thingy I'm still
pondering, but that's a later thing -- first the Database stuff. :)

Jeff 'japhy' Pinyan
On Jul 13, Scott R. Godin said:

QUOTE
http://www.webdragon.net/miscel/DB.pm

I'll check it out.

QUOTE
All Subscriber::DB objects would share the DBI object -- there's no need for
a billion database handles.

ok, so possibly one should do it differently than I have, in my example.

Well, look at it this way: either the Subscriber::DB object holds the
database, or it holds a subscriber. Choose.

If it holds the database, then it needs a method to lookup, create, and
return a Subscriber object from the database.

If it holds the subscriber, it needs to CALL the database to populate
itself.

QUOTE
But what I had thought of doing would be to populate the object via the
database and fill it entire, thus being able to call any of the Subscriber
methods on it without generating multiple calls to the database for each one.

Ah, so by "object persistence" you mean that you want to create the
objects from the state they had in the database, and then when you're done
with them, save them back to the database? That seems reasonable, if
you're not going to have multiple processes accessing and changing this
information.

The way I envisioned it was very similar to a tie() mechanism. Every
access or setting of a value triggers a database action. Your method is
certainly less of a strain on the database.

QUOTE
One problem-space I'm still trying to wrap into the picture is the need to be
able to grab data from the database based on different criterion..  for
example "everyone who has requested info by e-mail only", or the opposite --
people who want their info snail-mailed to them.

That's just SQL.

SELECT *
FROM subscribers
WHERE email IS NOT NULL

Something like that would get all the records that had email addresses
filled in.

Here is my new take on what you want Subscriber::DB to do:

1. you create a connection to the database
2. you get all the records from the database (in the form of Subscriber
objects) which match a certain criteria
3. you fiddle with them
4. if you make changes, they get reflected in database when you're done

Does that sound appropriate?

use Subscriber::DB;

my $email_dbh = Subscriber::DB->new; # makes a new connection
$email_dbh->filter('email IS NOT NULL'); # sets up criteria
my @email_subs = $email_dbh->subscribers; # gets matches

my $snail_dbh = Subscriber::DB->new; # makes a new connection
$snail_dbh->filter('email IS NULL'); # sets up criteria
my @snail_subs = $email_dbh->subscribers; # gets matches

Is that what you envision? The storage of the Subscriber objects
somewhere in the Subscriber::DB object? I would change it to needing only
ONE S::DB object:

my $dbh = Subscriber::DB->new;
my @email_subs = $dbh->filter('email IS NOT NULL'); # runs the search
my @snail_subs = $dbh->filter('email IS NULL'); # ditto

The other idea I have is to return not Subscriber objects, but objects
that inherit from Subscriber, for this reason: you can override their
DESTROY handler to update the database when they die!

package Subscriber::DB;

sub filter {
my ($dbh, @how) = @_;
my @results = ...; # DBI magic here
my @obj;

for my $rec (@results) {
# the object needs to know the DBI handle it came from
my $s = Subscriber::Updater->new($dbh, $rec);
push @obj, $s;
}

return @obj;
}

package Subscriber::Updater;

use base 'Subscriber';

sub new {
my ($class, $dbh, $data) = @_;
# fill it in...
}

DESTROY {
my ($self) = @_;
my $db = $self->{_dbh}; # or however you'd prefer
# do some updating of the DB based on the contents of the object
}

How's that sound?

--
Jeff "japhy" Pinyan % How can we ever be the sold short or
RPI Acacia Brother #734 % the cheated, we who for every service
http://japhy.perlmonk.org/ % have long ago been overpaid?
http://www.perlmonks.org/ % -- Meister Eckhart


PHP Help | Linux Help | Web Hosting | Reseller Hosting | SSL Hosting
This is a "lo-fi" version of our main content. To view the full version with more information, formatting and images, please click here.
Invision Power Board © 2001-2005 Invision Power Services, Inc.