Notes from a Newbie Experiment 02: DBIx::Class
Notes from a Newbie document the creation and deployment of yardbirdfanclub.org with Perl Catalyst on shared hosting. They are intended for a Perl Catalyst Newbie who would like to study the creation and deployment of a Perl Catalyst application.
In these notes I continue to explore what it might take to create a blog application, focusing on how to use a database.
DBIx::Class Vocabulary
- result class == DBIx::Class::ResultSource == lib/MyApp/Schema/Result == table
- query class == DBIx::Class::ResultSet == lib/MyApp/Schema/ResultSet == query (prepare to retrieve zero or more rows)
- row class == DBIx::Class::Row == lib/MyApp/Schema/Result (add Row methods to Result classes) == row (retrieve one row)
Result Class
- A Result Class Corresponds To A Table
In my applications, each Result class will define one table, which defines the columns it has, along with any relationships it has to other tables, and the methods that will be available in the Row objects created using that source.
Result classes are also known as Result sources.
Result classes are defined by calling methods proxied to DBIx::Class::ResultSource.
ResultSources do not need to be directly created, a ResultSource instance is created for each Result class in your Schema, by the proxied methods table and add_columns.
Notice we have one Result class corresponding to our blog table:
ResultSet Class
- A ResultSet Corresponds To A SQL Query
Any time you want to query a table, you'll be creating a DBIx::Class::ResultSet from its ResultSource.
This is an object representing a set of conditions to filter data. It can either be an entire table, or the results of a query. The actual data is not held in the ResultSet, it is only a description of how to fetch the data.
Setting up a ResultSet does not execute the query; retrieving the data does. Search is like "prepare," the query won't execute until you use a method that wants to access the data - such as "next", or "first" and search results are blessed into DBIx::Class::Row objects.
Notice we don't have any ResultSet classes in our application yet, but we will soon create a couple of them.
Row Class
- Row objects contain your actual data. They are returned from ResultSet objects.
See:
- DBIx::Class::Manual::Glossary
- DBIx::Class::ResultSet
- DBIx::Class::ResultSource
- DBIx::Class::Row
stash()
Though not previously mentioned, there are some subtle, important nuances to code we already created.
Pay attention to what our Root.pm controller actions put into the stash, and how these objects are used in the templates.
Yardbird/lib/Yardbird/Controller/Root.pm:
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->stash(blog_entries => [$c->model('DB::Blog')->search({})]);
$c->detach($c->view("TT"));
}
sub blog :Path('blog') :Args(1) {
my ( $self, $c, $id ) = @_;
$c->stash(blog_entry => $c->model('DB::Blog')->search({id => $id}));
$c->detach($c->view("TT"));
}
Yardbird/root/index.tt:
<h3>Take A Ride...</h3>
[% FOREACH blog_entry IN blog_entries %]
<p><a href="[% c.uri_for_action('/blog', blog_entry.id) %]">[% blog_entry.title %]</a></p>
<p>[% blog_entry.content %]</p>
[% END %]
Yardbird/root/blog.tt:
<h3>On The Wild Side.</h3>
<p>blog_entry.id: [% blog_entry.id %]</p>
<p>blog_entry.title: [% blog_entry.title %]</p>
<p>blog_entry.content: [% blog_entry.content %]</p>
ResultSet vs. Row
It is common to put the results of a search into the stash, and a search returns either a ResultSet or Row objects depending on context, so it is something to pay careful attention to.
It matters to templates what controllers put into the stash, so you must know the difference between ResultSets and Row objects and when and how they are used.
DBIx::Class::ResultSet
Method: search
Return Value: $resultset (scalar context) || @row_objs (list context)
The DBIx::Class::ResultSet doc says the ResultSet search method returns a ResultSet in scalar context, else a list of Row objects in list context.
What Is Our Code Doing?
In what context, scalar or list, are we searching?
What are my Root.pm controller actions putting into the stash, ResultSet or Row objects?
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->stash(blog_entries => [$c->model('DB::Blog')->search({})]);
$c->detach($c->view("TT"));
}
sub blog :Path('blog') :Args(1) {
my ( $self, $c, $id ) = @_;
$c->stash(blog_entry => $c->model('DB::Blog')->search({id => $id}));
$c->detach($c->view("TT"));
}
A hash is a list, and this is a hash assignment, so to my way of thinking, search is being evaluated in list context and returns a Row object:
$c->stash(blog_entry => $c->model('DB::Blog')->search({id => $id}));
In our index action, even before the hash assignment, we evaluate search inside an anonymous array operator. Thus to my way of thinking search is again evaluated in list context, this time returning a list of Row objects:
$c->stash(blog_entries => [$c->model('DB::Blog')->search({})]);
If I understand this correctly, my index action puts an array of Row objects and my blog action a single Row object into the stash. Compare how their respective templates use the stash, it seems to support my explanation.
Yardbird/lib/Yardbird/Controller/Root.pm:
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->stash(blog_entries => [$c->model('DB::Blog')->search({})]);
$c->detach($c->view("TT"));
}
sub blog :Path('blog') :Args(1) {
my ( $self, $c, $id ) = @_;
$c->stash(blog_entry => $c->model('DB::Blog')->search({id => $id}));
$c->detach($c->view("TT"));
}
Yardbird/root/index.tt:
<h3>Take A Ride...</h3>
[% FOREACH blog_entry IN blog_entries %]
<p><a href="[% c.uri_for_action('/blog', blog_entry.id) %]">[% blog_entry.title %]</a></p>
<p>[% blog_entry.content %]</p>
[% END %]
Yardbird/root/blog.tt:
<h3>On The Wild Side.</h3>
<p>blog_entry.id: [% blog_entry.id %]</p>
<p>blog_entry.title: [% blog_entry.title %]</p>
<p>blog_entry.content: [% blog_entry.content %]</p>
What Was Our Code Doing?
Recall what our index method was doing before we improved the code:
Yardbird/lib/Yardbird/Controller/Root.pm:
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
my $rs = $c->model('DB::Blog')->search({});
my $blog = $rs->next;
$c->stash->{t1} = $blog->title;
$c->stash->{c1} = $blog->content;
$c->stash->{id1} = $blog->id;
$blog = $rs->next;
$c->stash->{t2} = $blog->title;
$c->stash->{c2} = $blog->content;
$c->stash->{id2} = $blog->id;
$blog = $rs->next;
$c->stash->{t3} = $blog->title;
$c->stash->{c3} = $blog->content;
$c->stash->{id3} = $blog->id;
$c->detach($c->view("TT"));
}
The search method was evaluated in scalar context, so a ResultSet was returned from search. The Row method 'next' was then used on the ResultSet to put Row object values into the stash for the template to use.
search()
In this section we will experiment with Result and ResultSet search methods to find and use data. We will:
1) Recreate a database with new test data, adding a user table with id, username, firstname, and lastname. Make userid a foreign key in the blog table.
2) Create a user Result class search method to return a name:
Users will be required to create a username when becoming members of yardbirdfanclub.org. Firstname and lastname will be optional, but will be displayed instead of username if either or both are provided.
- Returns username if neither firstname or lastname are given. Returns firstname if firstname is given but lastname is not given. Returns lastname if lastname is given but firstname is not given. Returns a concatenated "firstname lastname" if both are given.
3) Create blog ResultSet search methods to return:
- A particular blog entry.
- All recent blog entries, sorted with most recent first.
- All blog entries of a particular user, sorted with most recent first.
Recreate Database, Model and Schema
Recreate a database with new test data, adding a user table with id, username, firstname, and lastname. Make userid a foreign key in the blog table.
Create Database
yardbird01.sql:
--
CREATE TABLE `user` (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(30) NOT NULL,
firstname VARCHAR(30),
lastname VARCHAR(30),
PRIMARY KEY (id)
) ENGINE=InnoDB;
--
CREATE TABLE blog (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
userid INT UNSIGNED NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (userid) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB;
--
INSERT INTO `user` (id, username) VALUES (1, 'webmaster');
INSERT INTO `user` (id, username, firstname) VALUES (2, 'sally1', 'Mustang');
INSERT INTO `user` (id, username, lastname) VALUES (3, 'sally2', 'Sally');
INSERT INTO `user` (id, username, firstname, lastname) VALUES (4, 'sally3', 'Mustang', 'Sally');
INSERT INTO `user` (id, username, firstname, lastname) VALUES (5, 'billyt', 'Billy T.', 'Barfoe');
--
INSERT INTO blog (id, userid, title, content) VALUES (1, 1, 'Welcome webmaster',
'Congratulations, you are now a member of yardbirdfanclub.org.');
--
INSERT INTO blog (id, userid, title, content) VALUES (2, 2, 'Welcome sally1',
'Congratulations, you are now a member of yardbirdfanclub.org.');
--
INSERT INTO blog (id, userid, title, content) VALUES (3, 3, 'Welcome sally2',
'Congratulations, you are now a member of yardbirdfanclub.org.');
--
INSERT INTO blog (id, userid, title, content) VALUES (4, 4, 'Welcome sally3',
'Congratulations, you are now a member of yardbirdfanclub.org.');
--
INSERT INTO blog (id, userid, title, content) VALUES (5, 5, 'Welcome billyt',
'Congratulations, you are now a member of yardbirdfanclub.org.');
Create the database:
> mysql -uusername -ppassword
mysql> create database yardbird01;
mysql> quit;
> mysql -uusername -ppassword yardbird01 < yardbird01.sql
> mysql -uusername -ppassword
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| yardbird00 |
| yardbird01 |
+--------------------+
mysql> use yardbird01;
mysql> show tables;
+----------------------+
| Tables_in_yardbird01 |
+----------------------+
| blog |
| user |
+----------------------+
mysql> describe blog;
+---------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| userid | int(10) unsigned | NO | MUL | NULL | |
| title | text | NO | | NULL | |
| content | text | NO | | NULL | |
+---------+------------------+------+-----+---------+----------------+
mysql> describe user;
+-----------+------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+------------------+------+-----+---------+----------------+
| id | int(10) unsigned | NO | PRI | NULL | auto_increment |
| username | varchar(30) | NO | | NULL | |
| firstname | varchar(30) | YES | | NULL | |
| lastname | varchar(30) | YES | | NULL | |
+-----------+------------------+------+-----+---------+----------------+
mysql> select * from blog;
+----+--------+-------------------+---------------------------------------------------------------+
| id | userid | title | content |
+----+--------+-------------------+---------------------------------------------------------------+
| 1 | 1 | Welcome webmaster | Congratulations, you are now a member of yardbirdfanclub.org. |
| 2 | 2 | Welcome sally1 | Congratulations, you are now a member of yardbirdfanclub.org. |
| 3 | 3 | Welcome sally2 | Congratulations, you are now a member of yardbirdfanclub.org. |
| 4 | 4 | Welcome sally3 | Congratulations, you are now a member of yardbirdfanclub.org. |
| 5 | 5 | Welcome billyt | Congratulations, you are now a member of yardbirdfanclub.org. |
+----+--------+-------------------+---------------------------------------------------------------+
mysql> select * from user;
+----+-----------+-----------+----------+
| id | username | firstname | lastname |
+----+-----------+-----------+----------+
| 1 | webmaster | NULL | NULL |
| 2 | sally1 | Mustang | NULL |
| 3 | sally2 | NULL | Sally |
| 4 | sally3 | Mustang | Sally |
| 5 | billyt | Billy T. | Barfoe |
+----+-----------+-----------+----------+
mysql> quit;
Recreate Model and Schema
script/yardbird_create.pl model DB DBIC::Schema \
Yardbird::Schema create=static dbi:mysql:yardbird01 username password
We now have an additional Result class for our new user table, and a new model. We will need to rename or delete DB.pm and rename DB.pm.new to DB.pm:
Run the application, you should get the same page as you had before creating the new database, this time with new data.
> script/yardbird_server.pl -r
Create Result Class (Row) Search Methods
Create a user Result class search method to return a name:
Users will be required to create a username when becoming members of yardbirdfanclub.org. Firstname and lastname will be optional, but will be displayed instead of username if either or both are provided.
You want any new code or comments you add to Result classes preserved whenever you update your schema. This will automatically occur, but you must add it to the area provided:
Schema/Result/User.pm:
# You can replace this text with custom code or comments, and it will be preserved on regeneration.
sub name {
my ($self) = @_;
my $bestname;
if ($self->firstname) {
$bestname = $self->firstname;
if ($self->lastname) {
$bestname .= ' ' . $self->lastname;
}
}
elsif ($self->lastname) {
$bestname = $self->lastname;
}
else {
$bestname = $self->username;
}
return $bestname;
}
__PACKAGE__->meta->make_immutable;
1;
We need to add a line of code to our blog.tt template to put our new User Row method to work:
<p>blog_entry.userid.name: [% blog_entry.userid.name %]</p>
Notice that the template is accessing user table data through the blog table's foreign key. This shows us that a foreign key allows a table to access data in a table it references. Since it cascade updates and deletes, once we get around to removing users from the database, any blog rows referencing a user will automatically be deleted when a user is deleted from the user table.
Run the application and you will see user's names output to specification.
Create ResultSet Class (Query) Search Methods
A Particular Blog Entry
We have already written code to search for a particular blog entry, in our blog action:
Yardbird/lib/Yardbird/Controller/Root.pm:
sub blog :Path('blog') :Args(1) {
my ( $self, $c, $id ) = @_;
$c->stash(blog_entry => $c->model('DB::Blog')->search({id => $id}));
$c->detach($c->view("TT"));
}
Let's take the search code out of the controller and put it into the model where it belongs.
As previously mentioned, we don't have any ResultSet classes in our application yet. Unlike Result classes, Catalyst didn't automatically create them for me but that is no problem, we will easily create them ourselves.
We need to create a ResultSet directory under our Schema class, and create our ResultSet classes there.
Yardbird/lib/Yardbird/Schema/ResultSet/Blog.pm:
package Yardbird::Schema::ResultSet::Blog;
use strict;
use warnings;
use base 'DBIx::Class::ResultSet';
sub specific {
my ($self, $bID) = @_;
return $self->search({id => $bID});
}
1;
Now we can use our new search method in our controller:
Yardbird/lib/Controller/Root.pm:
sub blog :Path('blog') :Args(1) {
my ( $self, $c, $id ) = @_;
$c->stash(blog_entry => $c->model('DB::Blog')->specific($id));
$c->detach($c->view("TT"));
}
Run the application and you should see no difference from how it ran before, i.e. the blog action continues to put the correct Row object into the stash to view.
> script/yardbird_server.pl -r
All Recent Blog Entries, Sorted With Most Recent First
Have you noticed our blog entries are listed in order with the oldest blog entry at the top of the list?
I want them ordered with the newest blog entry at the top of the list! Furthermore, I only want to see the most recent blog entries.
Create a method to sort and show the most recent blog entries:
Yardbird/lib/Yardbird/Schema/ResultSet/Blog.pm:
sub most_recent {
my ($self) = @_;
return $self->search({}, {order_by => {-desc => ['id']}, rows => 4});
}
Since the values of our blog table id column increase as new blog entries are created, we need to sort by descending order to get the newest ones at the top of the list. Since we only have 5 blog entries at this time, I will set the number of recent entries to display to be 4.
Let's use it in our index action, so that we only see the most recent four blog entries sorted with the newest (Billy T. Barfoe's) at the top.
Yardbird/lib/Controller/Root.pm:
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->stash(blog_entries => [$c->model('DB::Blog')->most_recent]);
$c->detach($c->view("TT"));
}
Now when we run the application we no longer see the webmaster's blog entry, and the entries are ordered with the most recent at the top:
All Blog Entries Of A Particular User, Sorted With Most Recent First
Yardbird/lib/Yardbird/Schema/ResultSet/Blog.pm:
sub all_user {
my ($self, $uID) = @_;
return $self->search({userid => $uID}, {order_by => {-desc => ['id']}});
}
Creating this search method may be easier to understand than what I will show you to use it. We will edit our blog action and corresponding template, our biggest challenge being how to find a user's id in the controller, and we will need a Row object to do that.
Instead of putting our Row object in the stash like we have been:
Yardbird/lib/Controller/Root.pm:
sub blog :Path('blog') :Args(1) {
my ( $self, $c, $id ) = @_;
$c->stash(blog_entry => $c->model('DB::Blog')->specific($id));
$c->detach($c->view("TT"));
}
Let's change the code to give us the same outcome, with a $row object we may then use to identify a user whose entries we will search for:
sub blog :Path('blog') :Args(1) {
my ( $self, $c, $id ) = @_;
my $row = $c->model('DB::Blog')->specific($id)->first;
$c->stash(blog_entry => $row);
$c->detach($c->view("TT"));
}
Now with this $row object, we have access to the id of the user who's blog entries we want to search for.
sub blog :Path('blog') :Args(1) {
my ( $self, $c, $id ) = @_;
my $row = $c->model('DB::Blog')->specific($id)->first;
$c->stash(blog_entry => $row);
$c->stash(all_user_entries => [$c->model('DB::Blog')->all_user($row->userid->id)]);
$c->detach($c->view("TT"));
}
We can show a user's entries in a loop:
[% FOREACH user_entry IN all_user_entries %]
<p>[% user_entry.title %]</p>
<p>[% user_entry.content %]</p>
[% END %]
Do something like this with it:
Yardbird/root/blog.tt:
<h3>On The Wild Side.</h3>
<p>blog_entry.id: [% blog_entry.id %]</p>
<p>blog_entry.title: [% blog_entry.title %]</p>
<p>blog_entry.content: [% blog_entry.content %]</p>
<p>blog_entry.userid.name: [% blog_entry.userid.name %]</p>
<p>--------------------------------------</p>
[% FOREACH user_entry IN all_user_entries %]
<p>[% user_entry.title %]</p>
<p>[% user_entry.content %]</p>
[% END %]
Unfortunately we only have one blog entry in our database per user, so you will need to add blog entries for a particular user to see if our new search method is working correctly. Fortunately you can do this very easily with AutoCRUD.
AutoCRUD
All it takes to use AutoCRUD is two things:
1) Use AutoCRUD in your application class:
Yardbird/lib/Yardbird.pm:
use Catalyst qw/
-Debug
ConfigLoader
Static::Simple
AutoCRUD
/;
2) Give your browser the location to use AutoCRUD:
http://0.0.0.0:3000/autocrud
Summary
yardbird01.sql:
--
CREATE TABLE `user` (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(30) NOT NULL,
firstname VARCHAR(30),
lastname VARCHAR(30),
PRIMARY KEY (id)
) ENGINE=InnoDB;
--
CREATE TABLE blog (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
userid INT UNSIGNED NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
FOREIGN KEY (userid) REFERENCES `user`(id) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY (id)
) ENGINE=InnoDB;
--
INSERT INTO `user` (id, username) VALUES (1, 'webmaster');
INSERT INTO `user` (id, username, firstname) VALUES (2, 'sally1', 'Mustang');
INSERT INTO `user` (id, username, lastname) VALUES (3, 'sally2', 'Sally');
INSERT INTO `user` (id, username, firstname, lastname) VALUES (4, 'sally3', 'Mustang', 'Sally');
INSERT INTO `user` (id, username, firstname, lastname) VALUES (5, 'billyt', 'Billy T.', 'Barfoe');
--
INSERT INTO blog (id, userid, title, content) VALUES (1, 1, 'Welcome webmaster',
'Congratulations, you are now a member of yardbirdfanclub.org.');
--
INSERT INTO blog (id, userid, title, content) VALUES (2, 2, 'Welcome sally1',
'Congratulations, you are now a member of yardbirdfanclub.org.');
--
INSERT INTO blog (id, userid, title, content) VALUES (3, 3, 'Welcome sally2',
'Congratulations, you are now a member of yardbirdfanclub.org.');
--
INSERT INTO blog (id, userid, title, content) VALUES (4, 4, 'Welcome sally3',
'Congratulations, you are now a member of yardbirdfanclub.org.');
--
INSERT INTO blog (id, userid, title, content) VALUES (5, 5, 'Welcome billyt',
'Congratulations, you are now a member of yardbirdfanclub.org.');
Yardbird/lib/Controller/Root.pm:
package Yardbird::Controller::Root;
use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller' }
__PACKAGE__->config(namespace => '');
sub index :Path :Args(0) {
my ( $self, $c ) = @_;
$c->stash(blog_entries => [$c->model('DB::Blog')->most_recent]);
$c->detach($c->view("TT"));
}
sub blog :Path('blog') :Args(1) {
my ( $self, $c, $id ) = @_;
my $row = $c->model('DB::Blog')->specific($id)->first;
$c->stash(blog_entry => $row);
$c->stash(all_user_entries => [$c->model('DB::Blog')->all_user($row->userid->id)]);
$c->detach($c->view("TT"));
}
sub default :Path {
my ( $self, $c ) = @_;
$c->response->body( 'Page not found' );
$c->response->status(404);
}
sub end : ActionClass('RenderView') {}
__PACKAGE__->meta->make_immutable;
1;
Yardbird/root/index.tt:
<h3>Take A Ride...</h3>
[% FOREACH blog_entry IN blog_entries %]
<p><a href="[% c.uri_for_action('/blog', blog_entry.id) %]">[% blog_entry.title %]</a></p>
<p>[% blog_entry.content %]</p>
[% END %]
Yardbird/root/blog.tt:
<h3>On The Wild Side.</h3>
<p>blog_entry.id: [% blog_entry.id %]</p>
<p>blog_entry.title: [% blog_entry.title %]</p>
<p>blog_entry.content: [% blog_entry.content %]</p>
<p>blog_entry.userid.name: [% blog_entry.userid.name %]</p>
<p>--------------------------------------</p>
[% FOREACH user_entry IN all_user_entries %]
<p>[% user_entry.title %]</p>
<p>[% user_entry.content %]</p>
[% END %]
Yardbird/lib/Yardbird/Schema/Result/User.pm:
sub name {
my ($self) = @_;
my $bestname;
if ($self->firstname) {
$bestname = $self->firstname;
if ($self->lastname) {
$bestname .= ' ' . $self->lastname;
}
}
elsif ($self->lastname) {
$bestname = $self->lastname;
}
else {
$bestname = $self->username;
}
return $bestname;
}
Yardbird/lib/Yardbird/Schema/ResultSet/Blog.pm:
package Yardbird::Schema::ResultSet::Blog;
use strict;
use warnings;
use base 'DBIx::Class::ResultSet';
sub specific {
my ($self, $bID) = @_;
return $self->search({id => $bID});
}
sub most_recent {
my ($self) = @_;
return $self->search({}, {order_by => {-desc => ['id']}, rows => 4});
}
sub all_user {
my ($self, $uID) = @_;
return $self->search({userid => $uID}, {order_by => {-desc => ['id']}});
}
1;
1) Use AutoCRUD in your application class:
Yardbird/lib/Yardbird.pm:
use Catalyst qw/
-Debug
ConfigLoader
Static::Simple
AutoCRUD
/;
2) Give your browser the location to use AutoCRUD:
http://0.0.0.0:3000/autocrud
Hi Joe,
I love your article series - they are *precisely* what Perl needs these days. I noticed[1] a number of inconsistencies in your grasp of some *concepts* of DBIx::Class. I would like to have a chat to address them in a way that is most convenient to you - irc, email, something else? Let me know if you are interested either here or at ribasushi@cpan.org
Cheers
[1] Not trying to nitpick, I just happen to know too much about the framework https://github.com/dbsrgits/dbix-class/contributors
Hello Peter,
Thank you for your observation and for offering to help me learn what I need to learn, I greatly appreciate and welcome it very much.
Let's have the conversation here, so that others may learn from this too.
Sorry I didn't respond sooner, I didn't realize you made your comment until now.
Hi, and sorry myself for not replying earlier - life happened inbetween.
Due to the amazing generosity of perl folk I will be attending YAPC::NA next month. I suggest we take care of this life. Looking forward to meeting you either way ;)
Cheers!
Yes!