Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I build multiple attributes with a single builder in Moose?

Tags:

perl

moose

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 
}
like image 434
RickF Avatar asked Oct 20 '10 14:10

RickF


2 Answers

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.

like image 155
hdp Avatar answered Nov 16 '22 11:11

hdp


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',
);
like image 36
Ether Avatar answered Nov 16 '22 10:11

Ether