-
Notifications
You must be signed in to change notification settings - Fork 13
Description
Quick disclaimer: I don't think we need to tackle this for the dev release, but I think it might be crucial for the CPAN release.
Creating even a very simplistic repository wrapping a SQL table can take significant amounts of time, and requires in-depth knowledge of PONAPI -- or at least, copypasting from another repo and cargo-culting code.
For example, this is a simple retrieve_all implementation:
sub retrieve_all {
my ($self, %args) = @_;
my $type = $args{type};
my $document = $args{document};
my $dbh = $self->dbh;
my $sth = $dbh->prepare(qq{ SELECT * FROM $type });
$sth->execute;
while ( my $row = $sth->fetchrow_hashref ) {
my $id = delete $row->{id};
my $resource = $doc->add_resource( type => $type, id => $id );
$resource->add_attribute( $_ => $row->{$_} ) for keys %{$row};
$resource->add_self_link();
}
return;
}
Which seems sane enough, until you notice that it's missing a ton of features. If we want to also return the resource's relationships, we need at the bare minimum:
my %relationships => (
articles => {
comments => {
type => 'comments',
one_to_many => 1,
rel_table => 'rel_articles_comments',
id_column => 'id_articles',
rel_id_column => 'id_comments',
},
authors => ...,
},
...
);
sub _fetch_relationships {
my ($self, %args) = @_;
my ($type, $id) = @args{qw/ type id /};
my $type_relationships = $relationships{$type} || {};
return unless %$type_relationships;
my $dbh = $self->dbh;
my %rels;
foreach my $rel_type ( keys %$type_relationships ) {
my $relationship_info = $type_relationships->{$rel_type};
my $table = $relationship_info->{rel_table};
my $id_col = $relationship_info->{id_column};
my $rel_id_col = $relationship_info->{rel_id_column};
my $sth = $dbh->prepare(
"SELECT $rel_id_col FROM $table WHERE $id_col = ?"
);
return unless $sth->execute($id);
while ( my $row = $sth->fetchrow_hashref ) {
push @{ $rels{$rel_type} ||= []}, $row->{$rel_id_col};
}
}
return \%rels;
}
sub _add_relationships {
my ($self, %args) = @_;
my $resource = $args{resource};
my $all_relationships = $self->_fetch_relationships(%args);
foreach my $rel_type ( keys %$all_relationships ) {
my $relationships = $all_relationships->{$rel_type} || [];
next unless @$relationships;
my $one_to_many = $self->has_one_to_many_relationship($type, $rel_type);
foreach my $rel_id ( @$relationships ) {
$resource->add_relationship( $rel_type, $rel_id, $one_to_many )
->add_self_link
->add_related_link;
}
}
}
sub retrieve_all {
...
# Inside the while loop:
$self->_add_relationships(
%args,
resource => $resource,
);
...
}
Add to that code for include, fields, page, sort, filter, and the complexity quickly gets out of hand, not to mention the amount of places to forget some bit of code (like adding the self links).
There must be a better way!
My current thought is to add two new roles for the common SQL case, PONAPI::DAO::Repository::Database and PONAPI::DAO::Repository::Table.
::Database handles a collection of tables, similar to what our current MockDB does.
It would implement all the methods required by PONAPI::DAO::Repository ( has_type, retrieve, etc ) and handle the building of the document, but it delegates the fetching/handling of the data to smaller methods provided by the ::Table-composed clases.
So its retrieve_all might look something like this:
sub retrieve_all {
my ($self, %args) = @_;
my $type = $args{type};
my $document = $args{document};
my $table_obj = $self->table_object_for_type($type);
my $rows = $table_obj->search_where(
columns => $self->fields_to_columns(%args),
where => $self->filter_to_where(%args),
limit => $self->page_to_limit(%args),
# etc
);
my @includes;
foreach my $row ( @$rows ) {
my $relationships = $table_obj->fetch_relationships(%args, ...);
my $resource = $document->add_resource(...)->add_attributes(...)->add_self_link(...);
$resource->add_relationships(...);
push @includes, $relationships if ...;
}
# etc etc
}
Meanwhile, the class consuming the role will just need to provide a handful of methods to implement has_type and friends; the class consuming the ::Table role would likewise just need a handful of lines of meta info (what the columns are, where the relationships are).
I'm not particularly attached to the specifics of this idea, but if we can spare people from using the low-level ::Repository and implementing everything on their own, I think that would be a huge gain, both in sanity and in our ability to ensure spec-conformant responses.