DBIx::Class and overloading accessors - perl

(Similar to, but with more concrete details that, #11526999)
My Result Classes have been built using dbicdump, however I wish to overload the default accessor for a date field.
Works, but a bodge
To hackytest my idea, I simply added an accessor attribute to the created date key of the add_columns call:
__PACKAGE__->add_columns(
"stamp_id",
{
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
sequence => "timestamp_stamp_id_seq",
},
"date",
{ data_type => "date", is_nullable => 0, accessor => '_date' },
);
... and created my accessor routine below the Schema::Loader checksum line:
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:nB5koMYAhBwz4ET77Q8qlA
sub date {
my $self = shift;
warn "overloaded date\n"; # Added for debugging
my $date;
# The date needs to be just the date, not the time
if ( #_ ) {
$date = shift;
if ( $date =~ /^([\d\-]+)/ ) {
$date = $1
}
return $self->_date($date)
}
# Fetch the column value & remove the time part.
$date = $self->_date;
if ( $date =~ /^([\d\-]+)/ ) {
$date = $1
}
return $date;
}
This works, as it returns an expected 2014-10-04, but is a bodge.
Do it the right way
The problem is that I've hacked the checksum'd code, so I can't neatly re-generate my Class objects.
Reading ResultSource and the CookBook the correct approach appears to be:
Have the ResultSource built by dbicdump as standard:
__PACKAGE__->add_columns(
"stamp_id",
{
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
sequence => "timestamp_stamp_id_seq",
},
"date",
{ data_type => "date", is_nullable => 0 },
);
.... add a change the accessor below the line, using the + to indicate it's an alteration to an existing definition:
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:nB5koMYAhBwz4ET77Q8qlA
__PACKAGE__->add_columns(
"+date", { accessor => '_date' },
);
.... use the overload method as before
Not working.
I've double-checked my spelling, I've tried add_column rather than add_columns, and I've tried putting the second add_columns to directly below the first - all to now avail.... the code uses the default accessor, and returns 2014-10-04T00:00:00
How do I over-ride the default accessor, so I can use my own method?
Thankee...

What you need here is a col_accessor_map passed in as a loader option.
col_accessor_map => {
table_name => {
date => _date,
}
}
You can pass loader options to dbicdump with -o.
$ dbicdump -o col_accessor_map="{ table_name => { date => _date } }" ... other options ...
(Replace table_name above with the name of your table - that's obvious, right?)
Update: This was posted untested, and when I finally got round to testing it, I found it didn't work. After a conversation with the author on IRC I was told that the col_accessor_map option doesn't support this nested hash approach, so if you wanted to use this approach you would need to use a coderef.
However, the author also agreed that adding this support would be a good idea and I've just got back from lunch to find this Github commit which adds the feature. I don't know how soon it will get to CPAN though.
This may be the first time that CPAN has been updated to make a SO answer correct :-)

At a different level of abstraction I believe you could use a method modifier
use Class::Method::Modifier; # or Moose/Moo
around date => sub {...};

Related

Join attempt throwing exceptions

I'm sure I'm overlooking something glaringly obvious and I apologize for the newbie question, but I've spent several hours back and forth through documentation for DBIx::Class and Catalyst and am not finding the answer I need...
What I'm trying to do is automate creation of sub-menus based on the contents of my database. I have three tables in the database to do so: maps (in which sub-menu items are found), menus (contains names of top-level menus), maps_menus (assigns maps to top-level menus). I've written a subroutine to return a hash of resultsets, with the plan of using a Template Toolkit nested loop to build the top-level and sub-menus.
Basically, for each top-level menu in menus, I'm trying to run the following query and (eventually) build a sub-menu based on the result:
select * FROM maps JOIN maps_menus ON maps.id_maps = maps_menus.id_maps WHERE maps_menus.id_menus = (current id_menus);
Here is the subroutine, located in lib/MyApp/Schema/ResultSet/Menus.pm
# Build a hash of hashes for menu generation
sub build_menu {
my ($self, $maps, $maps_menus) = #_;
my %menus;
while (my $row = $self->next) {
my $id = $row->get_column('id_menus');
my $name = $row->get_column('name');
my $sub = $maps_menus->search(
{ 'id_maps' => $id },
{ join => 'maps',
'+select' => ['maps.id_maps'],
'+as' => ['id_maps'],
'+select' => ['maps.name'],
'+as' => ['name'],
'+select' => ['maps.map_file'],
'+as' => ['map_file']
}
);
$menus{$name} = $sub;
# See if it worked...
print STDERR "$name\n";
while (my $m = $sub->next) {
my $m_id = $m->get_column('id_maps');
my $m_name = $m->get_column('name');
my $m_file = $m->get_column('map_file');
print STDERR "\t$m_id, $m_name, $m_file\n";
}
}
return \%menus;
}
I am calling this from lib/MyApp/Controller/Maps.pm thusly...
$c->stash(menus => [$c->model('DB::Menus')->build_menu($c->model('DB::Map'), $c->model('DB::MapsMenus'))]);
When I attempt to pull up the page, I get all sorts of exceptions, the top-most of which is:
[error] No such relationship maps on MapsMenus at /home/catalyst/perl5/lib/perl5/DBIx/Class/Schema.pm line 1078
Which, as far as I can tell, originates from the call to $sub->next. I take this as meaning I'm doing my query incorrectly and not getting the results I think I should be. However, I'm not sure what I'm missing.
I found the following lines, defining the relationship to maps, in lib/MyApp/Schema/Result/MapsMenus.pm
__PACKAGE__->belongs_to(
"id_map",
"MyApp::Schema::Result::Map",
{ id_maps => "id_maps" },
{ is_deferrable => 1, on_delete => "CASCADE", on_update => "CASCADE" },
);
...and in lib/MyApp/Schema/Result/Map.pm
__PACKAGE__->has_many(
"maps_menuses",
"MyApp::Schema::Result::MapsMenus",
{ "foreign.id_maps" => "self.id_maps" },
{ cascade_copy => 0, cascade_delete => 0 },
);
No idea why it's calling it "maps_menuses" -- that was generated by Catalyst. Could that be the problem?
Any help would be greatly appreciated!
I'd suggest using prefetch of the two relationships which form the many-to-many relationship helper and maybe using HashRefInflator if you don't need access to the row objects.
Note that Catalyst doesn't generate a DBIC (which is btw the official abbreviation for DBIx::Class, DBIx is a whole namespace) schema, SQL::Translator or DBIx::Class::Schema::Loader do. Looks at the docs of the module you've used to find out how to influence its naming.
Also feel free to change the names if they don't fit you.

How to call Postgres function in DBIx::Class

I'm currently playing around with DBIx::Class and I'm wondering how to call an existing Postgres function in a certain db schema using DBIx.
My DBI code:
my $table = $self->{dbh}->quote_identifier(
undef,
'foo',
'myFunction'
);
my $sqlst = qq{ SELECT foobar FROM $table($some_data); };
The things I found so far, would be to call said function using the dbh object retrieved from my DBIx::Class::Schema object:
my $return_data = {};
my $sql = qq{SELECT foobar FROM "foo"."myFunction"($some_data)};
$self->{schema}->storage->dbh_do( sub {
my ($storage, $dbh) = #_;
$menu_list = $dbh->selectrow_hashref(
$sql,
{ slice => {} }
);
});
Is there a better/easier solution than this?
I also stumbled upon DBIx::ProcedureCall, but I couldn't get it to work when using a DB schema.
Any help is much appreciated!
If you want to use SQL Functions as Table Sources, it should be possible to create a virtual DBIx::Class::ResultSource::View like this:
package MyApp::Schema::Result::MyFunction;
use base qw/DBIx::Class::Core/;
__PACKAGE__->table_class('DBIx::Class::ResultSource::View');
__PACKAGE__->table('myFunction');
__PACKAGE__->result_source_instance->is_virtual(1);
__PACKAGE__->result_source_instance->view_definition(
'SELECT foobar FROM "foo"."myFunction"(?)'
);
__PACKAGE__->add_columns(
'foobar' => {
data_type => 'varchar',
},
);
The view can be used like this:
my $rs = $schema->resultset('MyFunction')->select({}, {
bind => [ 'arg' ],
});
This will create a subquery that isn't really necessary:
SELECT me.foobar FROM (SELECT foobar FROM "foo"."myFunction"(?)) me
But I think it should work.

DateTime compare issues with InflateColumn::DateTime

I am having trouble comparing DateTime objects in my Catalylst. I have an end_date column which is being inflated by DBIx::Class::InflateColumn::DateTime, and I am inflating it with my timezone:
__PACKAGE__->add_columns(
end_date => { data_type => 'datetime', time_zone => 'America/Chicago' },
);
I have a function that is supposed to tell me if my event has closed or not, this is defined in my Schema for this class:
sub closed {
my ($self) = #_;
my $now = DateTime->now(time_zone => 'America/Chicago');
warn DateTime->compare($now, $self->end_date);
warn $now;
warn $self->end_date;
return DateTime->compare($now, $self->end_date) == 1;
}
However, it is not working properly. It is telling me that events have closed before they actually have. Here is an example output from the warns:
1
2014-06-29T12:20:48
2014-06-29T12:20:50
As you can see, it is saying that the first date is greater than end_date, even though it is not. I haven't been able to figure out why this is. However, whenever I convert them and create new DateTime objects:
sub closed {
my ($self) = #_;
my $now = DateTime::Format::ISO8601->parse_datetime(DateTime->now(time_zone => 'America/Chicago'));
my $end_date = DateTime::Format::ISO8601->parse_datetime($self->end_date);
return DateTime->compare($now, $end_date) == 1;
}
Then they compare correctly, and compare returns -1. Does anyone know why this could be?
Your debugging information is useless since you didn't include the time zone offsets (e.g. by using ->strftime('%FT%T%z')). If you did, I bet you'll find the first date is indeed greater than the end date, and I bet it's using UTC for your inflated column.
Looking at the docs, the time zone is to be provided by the timezone attribute, but you used time_zone.
{ data_type => 'datetime', timezone => "America/Chicago", locale => "de_DE" }
(That was a poor, confusing choice on D::C::IC::DT's behalf.)

Perl Catalyst DBIx Cursor Cache - how to clear?

apologises and thanks in advance for what, even as I type, seems likely silly question, but here goes anyway.
I have basic Catalyst application using DBIx::Class with an 'Author' and associated 'Book' table. In addition I also use DBIx::Class::Cursor::Cached to cache data as appropriate.
The issue is that, following an edit, I need to clear cached data BEFORE it has actually expired.
1.) Author->show_author_and_books which fetchs and caches resultset.
2.) the Book->edit_do which needs to clear the cached data from the Author->show_author_and_books request.
See basic/appropriate setup below.
-- MyApp.pm definition including backend 'Cache::FileCache' cache.
__PACKAGE__->config(
name => 'MyApp',
...
'Plugin::Cache' => { 'backend' => { class => 'Cache::FileCache',
cache_root => "./cache",
namespace => "dbix",
default_expires_in => '8 hours',
auto_remove_stale => 1
}
},
...
-- MyApp::Model::DB definition with 'Caching' traits set using 'DBIx::Class::Cursor::Cached'.
...
__PACKAGE__->config(
schema_class => 'MyApp::Schema',
traits => [ 'Caching' ],
connect_info => { dsn => '<dsn>',
user => '<user>',
password => '<password>',
cursor_class => 'DBIx::Class::Cursor::Cached'
}
);
...
-- MyApp::Controller::Author.pm definition with 'show_author_and_books' method - resultset is cached.
...
sub show_author_and_books :Chained('base') :PathPart('') :Args(0)
{
my ( $self, $c ) = #_;
my $author_id = $c->request->params->{author_id};
my $author_and_books_rs = $c->stash->{'DB::Author'}->search({ author_id => $author_id },
{ prefetch => 'book' },
cache_for => 600 } ); # Cache results for 10 minutes.
# More interesting stuff, but no point calling $author_and_books_rs->clear_cache here, it would make no sense:s
...
}
...
-- MyApp::Controller::Book.pm definition with 'edit_do' method which updates book entry and so invalidates the cached data in show_author_and_books.
...
sub edit_do :Chained('base') :PathPart('') :Args(0)
{
my ( $self, $c ) = #_;
# Assume stash contains a book for some author, and that we want to update the description.
my $book = $c->stash->{'book'}->update({ desc => $c->request->params->{desc} });
# How do I now clear the cached DB::Author data to ensure the new desc is displayed on next request to 'Author->show_author_and_books'?
# HOW DO I CLEAR CACHED DB::Author DATA?
...
}
Naturally I'm aware that $author_and_books_rs, as defined in Author->show_author_and_books, contains a method 'clear_cache', but obviously this is out of scope in Book->edit_do ( not to mention another problem there might be).
So, is the correct approach to make the DBIx request again , as per ...show_author_and_books and then call the 'clear_cache' again that or is there a more direct way where I can just say something like this $c->cache->('DB::Author')->clear_cache?
Thank you again.
PS. I'm sure when I look at this tomorrow, the full silliness of the question will hit me:s
Try
$c->model( 'DB::Author' )->clear_cache() ;
The solution I went for in the end was to NOT use 'DBIx::Class::Cursor::Cached', but instead directly use the Catalyst Cache plugin defining multiple
backend caches to handle the different namespaces I trying to manage in the real-world scenario.
I backed away from D::C::Cursor::Cached as all data was/is held in the same namespace plus there doesn't appear to be a method to expire data in advance of
time already set.
So for completeness, from the code above, the MyApp::Model::DB.pm definition would lose the 'traits' and 'cursor_class' key/values.
Then...
The MyApp.pm Plugin::Cache' would expand to contain multiple cache namespaces...
-- MyApp.pm definition including backend 'Cache::FileCache' cache.
...
'Plugin::Cache' => { 'backends' => { Authors => { class => 'Cache::FileCache',
cache_root => "./cache",
namespace => "Authors",
default_expires_in => '8 hours',
auto_remove_stale => 1
},
CDs => { class => 'Cache::FileCache',
cache_root => "./cache",
namespace => "CDs",
default_expires_in => '8 hours',
auto_remove_stale => 1
},
...
}
...
-- MyApp::Controller::Author.pm definition with 'show_author_and_books' method - resultset is cached.
...
sub show_author_and_books :Chained('base') :PathPart('') :Args(0)
{
my ( $self, $c ) = #_;
my $author_id = $c->request->params->{author_id};
my $author = $c->get_cache_backend('Authors')->get( $author_id );
if( !defined($author) )
{
$author = $c->stash->{'DB::Author'}->search({ author_id => $author_id },
{ prefetch => 'book', rows => 1 } )->single;
$c->get_cache_backend('Authors')->set( $author_id, $author, "10 minutes" );
}
# More interesting stuff, ...
...
}
...
-- MyApp::Controller::Book.pm definition with 'edit_do' method which updates book entry and so invalidates the cached data in show_author_and_books.
...
sub edit_do :Chained('base') :PathPart('') :Args(0)
{
my ( $self, $c ) = #_;
# Assume stash contains a book for some author, and that we want to update the description.
my $book = $c->stash->{'book'}->update({ desc => $c->request->params->{desc} });
# How do I now clear the cached DB::Author data to ensure the new desc is displayed on next request to 'Author->show_author_and_books'?
# HOW DO I CLEAR CACHED DB::Author DATA? THIS IS HOW, EITHER...
$c->get_cache_backend('Authors')->set( $c->stash->{'book'}->author_id, {}, "now" ); # Expire now.
# ... OR ... THE WHOLE Authors namespace...
$c->get_cache_backend('Authors')->clear;
...
}
NOTE : as you'll expect from the use of Author and CDs, this isn't the real world scenario I'm working, but should serve to show my intent.
As I'm relatively new to the wonder of DBIx and indeed Catalyst, I'd be interested to hear if there a better approach to this (I very much expect there is), but it will serve for the moment as I'm attempting to update a legacy application.
The plugin could probably be patched to make per result set caches easy to namespace and clear independently, and alternatively it would probably not be so hard to add a namespace to the attributes. If you want to work on that hit #dbix-class and I'd be willing to mentor you - jnap

DBIx::Class::ResultSet problems

I've got the following code:
package MyPackage::ResultSet::Case;
use base 'DBIx::Class::ResultSet';
sub cases_last_fourteen_days {
my ($self, $username) = #_;
return $self->search({
username => $username,
date => { '>=' => 'DATE_SUB(CURDATE(),INTERVAL 14 DAY)' },
});
};
But when I try to use it this way:
$schema->resultset('Case')->cases_last_fourteen_days($username)
I always get zero results, can anyone tell what I'm doing wrong?
Thanks!
The way you use the SQL::Abstract condition would result in this where condition:
WHERE username = ? AND date >= 'DATE_SUB(CURDATE(),INTERVAL 14 DAY)'
When you wish to use database functions in a where clause you need to use a reference to a scalar, like this:
date => { '>=' => \'DATE_SUB(CURDATE(),INTERVAL 14 DAY)' },
ProTip: if you set the environment variable DBIC_TRACE to 1, DBIx::Class will print the queries it generates to STDERR ... this way you can check if it really does what you wish.