Search code examples
perldbix-class

Getting result rows as array with DBIx::Class?


Is it possible to get a plain arrayrefs as rows of a DBIx::Class search?

I can do this

my $rs = $db->resultset("SomeTable")->search({});

my $results = $dbh->selectall_arrayref("select * from " . $rs->as_query->$*->[0]);

and it works, but it looks barbarous.

The benefit is I have the columns in order, and it's column-name independent, which in case of a crosstab is nice.

Are there any cleaner approaches? and no, the HashRefInflator result class (or any hash-based approach) won't work, as I have (without some other trick I am not aware of) no guarantee of column order - as they are hashes, no guarantee of key ordering.


Solution

  • I recommend using a new method in your Result class here, rather than one in your ResultSet class. I say this for reasons of scalability: you probably don't want to inflate your entire resultset all at once because that would only be feasible for very tiny resultsets that all fit into memory at once.

    For example, let's assume there's an as_arrayref result class method that does the thing you're asking for and that $some_resultset holds your resultset object. Then if you really wanted to have all those arrayrefs at once, you could simply do:

    my @many_rows = map { $_->as_arrayref } $some_resultset->all; 
    

    Or

    my $ref_to_many_rows = [ map { $_->as_arrayref } $some_resultset->all ];
    

    More likely you will step through your resultset one result at a time, and produce an arrayref of values per result object one at a time as you go.

    For example, let's assume there's an as_arrayref method that does the thing you're asking for and that $some_resultset holds your resultset object.

    while (my $result = $some_resultset->next) {
        my $arrayref = $result->as_arrayref;
        printf "[%s],\n", join ", " => @$arrayref;
    }
    

    That still won't scale for large resultsets, but you can always "page" the resultset so that it does so:

    use constant MAX_INCREMENTAL_RESULTSET_LIMIT => 25;
    
    my $limit      = MAX_INCREMENTAL_RESULTSET_LIMIT;
    my $count      = $resultset->count;
    my $page_count = 1 + ($count / $limit);
    
    for (my $page = 0; $page < $page_count; $page++) {
        my $paged_rs = $resultset->search(undef, { page => $page, rows => $limit });
        while (my $result = $paged_rs->next) {
            my $arrayref = $result->as_arrayref;
            printf "[%s],\n", join ", " => map { qq("$_") } @arrayref;
        }
    }
    

    That version now scales no matter how large your resultset.

    So what does your as_arrayref result class method look like?

    Remember that the get_columns result class method returns a pairlist of key–value pairs. That makes something like as_hashref as simple as:

    sub as_hashref {
        my($db_result) = @_;
        my %hash = $db_result->get_columns;
        return \%hash;
    }
    

    STRONG WARNING! That's unsafe as written because it doesn't croak when invoked with incorrect arguments. So you should write that as:

    use Carp;
    
    sub as_hashref {
        croak "expected no arguments beyond db result invocant" unless @_ == 1;
        my($db_result) = @_;
        my %hash = $db_result->get_columns;
        return \%hash;
    }
    

    Or, if you're using subroutine signatures, then:

    use feature "signatures";
    
    sub as_hashref ($db_result) {
        my %hash = $db_result->get_columns;
        return \%hash;
    }
    

    or even just

    sub as_hashref ($db_result) {
        return { $db_result->get_columns };
    }
    

    Given that the columns result class method returns a list of that result object's column names in order, your new as_arrayref result class method could just be either this with croak:

    sub as_arrayref {
        croak "expected no arguments beyond db result invocant" unless @_ == 1;
        my($db_result) = @_;
        my @keys = $db_result->columns;
        my %hash = $db_result->get_columns;
        my @vals = @hash{@keys};
        return \@vals;
    }
    

    Or this with signatures:

    sub as_arrayref ($db_result) {
        my @keys = $db_result->columns;
        my %hash = $db_result->get_columns;
        my @vals = @hash{@keys};
        return \@vals;
    }
    

    One thing I've done here is assume that you want the literal column values from your db, not objects automatically inflated by DBIx. That's not necessarily a good assumption, but even if that's what you want, we could make it clearer by choosing better method names.

    So let's fix the name by letting you have an explicitly named as_deflated_arrayref method on the one hand or an as_inflated_arrayref method on the other. That removes all question of inflation that method names like as_hashref or as_arrayref would leave unresolved in the eyes of the reader.

    That means you'd have some pair like:

    sub as_deflated_hashref ($db_result) {
        return { $db_result->get_columns };
    }
    
    sub as_inflated_hashref ($db_result) {
        return { $db_result->get_inflated_columns };
    }
    

    Which leads to methods like these for the deflated version:

    sub as_deflated_values ($db_result) {
        my @keys    = $db_result->columns;
        my $hashref = $db_result->as_deflated_hashref;
        my @vals    = @$hashref{@keys};
        return wantarray ? @vals : \@vals;
    }
    
    sub as_deflated_arrayref ($db_result) {
        return scalar $db_result->as_deflated_values;
    }
    

    And these for the inflated versions:

    sub as_inflated_values ($db_result) {
        my @keys    = $db_result->columns;
        my $hashref = $db_result->as_inflated_hashref;
        my @vals    = @$hashref{@keys};
        return wantarray ? @vals : \@vals;
    }
    
    sub as_inflated_arrayref ($db_result) {
        return scalar $db_result->as_inflated_values;
    }
    

    I'm sure you get the general idea here, so you should now be able to adapt these various approaches to your own code.