tags:

views:

80

answers:

3

Using Moose, is it possible to create a builder that builds multiple attributes at once?

I have a project in which the object has several 'sets' of fields - if any member of the set is requested, I want to go ahead and populate them all. My assumption is that if I need the name, I'll also need the birthdate, and since they're in the same table, it's faster to get both in one query.

I'm not sure if my question is clear enough, but hopefully some sample code will make it clear.

What I have:

Package WidgetPerson;
use Moose;

has id => (is => 'ro', isa => 'Int' );
has name => (is => 'ro', lazy => 1, builder => '_build_name');
has birthdate => (is => 'ro', lazy => 1, builder => '_build_birthdate');
has address => (is => 'ro', lazy => 1, builder => '_build_address');

sub _build_name {
 my $self = shift;
 my ($name) = $dbh->selectrow_array("SELECT name FROM people WHERE id = ?", {}, $self->id);
 return $name;
}
sub _build_birthdate {
 my $self = shift;
 my ($date) = $dbh->selectrow_array("SELECT birthdate FROM people WHERE id = ?", {}, $self->id);
 return $date;
}
sub _build_address {
 my $self = shift;
 my ($date) = $dbh->selectrow_array("SELECT address FROM addresses WHERE person_id = ?", {}, $self->id);
 return $date;
}

But what I want is:

has name => (is => 'ro', isa => 'Str', lazy => 1, builder => '_build_stuff');
has birthdate => (is => 'ro', isa => 'Date', lazy => 1, builder => '_build_stuff');
has address => (is => 'ro', isa => 'Address', lazy => 1, builder => '_build_address');
sub _build_stuff {
 my $self = shift;
 my ($name, $date) = $dbh->selectrow_array("SELECT name, birthdate FROM people WHERE id = ?", {}, $self->id);
 $self->name($name);
 $self->birthdate($date);
}
sub _build_address { 
 #same as before 
}
+4  A: 

No, an attribute builder can only return one value at a time. You could build both by having each builder set the value of the other attribute before returning, but that gets ugly pretty quickly...

However, if you generally have two pieces of data that go together in some way (e.g. coming from the same DB query as in your case), you can store these values together in one attribute as an object:

has birth_info => (
    is => 'ro', isa => 'MyApp::Data::BirthInfo',
    lazy => 1,
    default => sub {
         MyApp::Data::BirthInfo->new(shift->some_id)
    },
    handles => [ qw(birthdate name) ],
);

package MyApp::Data::BirthInfo;
use Moose;
has some_id => (
    is => 'ro', isa => 'Int',
    trigger => sub {
        # perhaps this object self-populates from the DB when you assign its id?
        # or use some other mechanism to load the row in an ORMish way (perhaps BUILD)
    }
);
has birthdate => (
    is => 'ro', isa => 'Str',
);
has name => (
    is => 'ro', isa => 'Str',
);
Ether
Hm. That may work, although it seems like it's adding complexity where I'm trying to reduce it.
RickF
@RickF: building two attributes with one builder is going to be more complicated than storing two attributes on a single object.
Ether
You're probably right. My hesitation is largely due to gut-reaction level resistance to simply recreating the complex table structure in Moose form.
RickF
On a book-keeping note, I'd ask you to clean up the sample code a bit before I mark the answer as accepted. Stray comma in qw(), missing } in _trigger_ sub, one of the attributes in sub-object should be "name" instead of "birthdate"
RickF
@RickF: done; you have a keen eye :)
Ether
It's always easier to proofread someone else's stuff :)
RickF
edited my reply again to remove the bald-faced lie I tried to sneak in there. "can't" is not the same as "shouldn't" :)
Ether
A: 

I use this Moose::Role in my projects. It is a lite version of DBx::Class:

http://pastebin.com/m7WLPEEz

xGhost
+1  A: 

What I do in this case, when I don't want to have a separate object as in Ether's answer, is have a lazily built attribute for the intermediate state. So, for example:

has raw_row => (is => 'ro', init_arg => undef, lazy => 1, builder => '_build_raw_row');
has birthdate => (is => 'ro', lazy => 1, builder => '_build_birthdate');

sub _build_raw_row {
   $dbh->selectrow_hashref(...);
}

sub _build_birthdate {
    my $self = shift;
    return $self->raw_row->{birthdate};
}

Repeat the same pattern as birthdate for name, etc.

Reading any of the individual attributes will try to get data from raw_row, whose lazy builder will only run the SQL once. Since your attributes are all readonly, you don't have to worry about updating any object state if one of them changes.

This pattern is useful for things like XML documents, too -- the intermediate state you save can be e.g. a DOM, with individual attributes being lazily built from XPath expressions or what-have-you.

hdp