[Catalyst] CatalystX::CRUD::Controller::RHTMLO: related database tables and nested forms

Adam Mackler nabble at mackler.org
Wed Jan 13 02:05:12 GMT 2010


Greetings:

In brief: I'm using CatalystX::CRUD::Controller::RHTMLO; that class
has a method called form_to_object that wants me to have a method in
my Form called <object>_from_form, where <object> is the object type
and that method name is stored in my controller config as the value of
"init_object".  That so-named method is magically created for me in my
Rose::HTML::Form-descended objects by using the amazing plant() method
of Rose::DBx::Garden(::Catalyst).

Question: What do I have to do to my
Rose::DBx::Garden::Catalyst-created application to create a form that
does CRUD operations on data in multiple related tables correctly?
Does the answer to this question have anything to do with the
<object>_from_form method I mentioned above?  I am feeling like what I
have to do includes changing that method.

That was the short version.

I'm a relative beginner at using Catalyst, and impairingly I'm more of
a user than a programmer, so I apologize in advance for being behind
the curve.  At this point I'm still learning basics.  Hopefully this
post may be helpful to anyone trying to do the same thing.

That said, alls I wanna do is have a webpage with an HTML form that
will update related tables in my database simultaneously and
correctly, and populate my multi-table-based form with the correct
data from the related database tables.  And my controllers are based
on Rose::DBx::Garden::Catalyst::Controller.

I used plant() from Rose::DBx::Garden::Catalyst to create all the
files of my app.  That plant() method calls the plant() in its
superclass, Rose::DBx::Garden, which does most of the work, and then
adds Catalyst-specific files.  It's all pretty cool, but finding my
way through everything it made is a bit daunting.  I think these files
that plant() made--please correct me--we call scaffolding.  Basically
it's my starting point for customizing the application into it's
specified goal.

Right now, my next baby step is to connect two related tables that
share a one-to-one relationship. It's for Big Kelly's Collection
Agency.  Big Kelly has a database with two tables: one for debtors,
one for their names.  A debtor can have more than one name, but
exactly one name is the current name.  I want a form-containing
webpage that Big Kelly can use to update table-column values in both a
debtor's row of the debtors table as well as that same debtor's
current name row in the names table.

My exact procedure leading up to calling the plant() method of
CatalystX::CRUD::Controller::RHTMLO is here in moderate detail:

Firsty, I made my database tables.  Here's what part of my postgresql
database for Big Kelly's Collection Agency looks like.  (Sorry I don't
know how to get SQL out of postgres.) First the table of debtors:

bigk=# \d debtors;
                                         Table "public.debtors"
      Column       |            Type             | Modifiers                       
-------------------+-----------------------------+--------------------------------------
 id                | integer                     | not null default nextval('debtors_id_seq'::regclass)
 email             | character varying           | 
 current_name      | integer                     | 
Indexes:
    "debtors_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "debtors_current_name_fkey" FOREIGN KEY (current_name) REFERENCES names(id)
Referenced by:
    TABLE "names" CONSTRAINT "names_debtor_fkey" FOREIGN KEY (debtor) REFERENCES debtors(id)

bigk=# \d names
                               Table "public.names"
   Column    |     Type      |                     Modifiers                      
-------------+---------------+----------------------------------------------------
 id          | integer       | not null default nextval('names_id_seq'::regclass)
 debtor      | integer       | 
 last_name   | character(21) | 
 middle_name | character(24) | 
 first_name  | character(24) | 
Indexes:
    "names_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "names_debtor_fkey" FOREIGN KEY (debtor) REFERENCES debtors(id)
Referenced by:
    TABLE "debtors" CONSTRAINT "debtors_current_name_fkey" FOREIGN KEY
(current_name) REFERENCES names(id)

bigk=# 



Then I created a Catalyst app:

catalyst.pl BK
cd BK
perl Makefile.PL

>From the new app directory I made a file called lib/BKDB.pm to connect
to the database:

package BKDB;
use strict;
use warnings;
use base 'Rose::DB';
                                                                 
BKDB->register_db (
    database => 'bigk',
    driver => 'Pg',
    username => 'sysop'
    );                                                                                                                  
1;

Next I created this script file to do the planting:

#!/usr/bin/perl
use BKDB;
use Rose::DBx::Garden::Catalyst;

my $garden = Rose::DBx::Garden::Catalyst->new(
    garden_prefix               => 'RDBO',
    catalyst_prefix             => 'BK',
    controler_prefix            => 'mycontroller'
    db                          => BKDB->new,
    debug                       => 1,
    tt                          => 1,
    include_autoinc_form_fields => 0,
    column_to_label             => 
        sub {
            my ($garden_obj, $col_name) = @_;
           return join(' ', map { ucfirst($_) } split(m/_/, $col_name));
        },
    );
$garden->plant('lib');


And I ran that script from the application directory, including the
lib directory on the perl command line.  It created a bunch of files.

At that point I edited the new application configuration file, bk.conf
and added the line:

default_view RDGC

Next I copied from my installed perl libraries the file called 

CatalystX/CRUD/YUI/TT/crud/tt_config.tt

and put a copy of that file with the same filename into root/crud.
>From that file I removed the line that reads:

ThisIsDefTTConfig = 1;

(I have an eventual goal of installing the javascript libraries on my
server, but so far have not been successful.)

I found a plant()-created file called lib/RDBO/Bk/Debtor.pm.  This
seems to be how the ORM knows about my debtors database table.

__PACKAGE__->meta->setup(
    table   => 'debtors',
    columns => [
        id                => { type => 'serial', not_null => 1 },
        email             => { type => 'varchar' },
        current_name      => { type => 'integer' },
    ],
    primary_key_columns => [ 'id' ],
    foreign_keys => [
        name => {
            class       => 'RDBO::Bk::Name',
            key_columns => { current_name => 'id' },
        },
    ],
    relationships => [
        names => {
            class      => 'RDBO::Bk::Name',
            column_map => { id => 'debtor' },
            type       => 'one to many',
        },
    ],
);

And in Name.pm I have:

_PACKAGE__->meta->setup(
    table   => 'names',
    columns => [
        id          => { type => 'serial', not_null => 1 },
        debtor      => { type => 'integer' },
        last_name   => { type => 'character', length => 21 },
        middle_name => { type => 'character', length => 24 },
        first_name  => { type => 'character', length => 24 },
    ],
    primary_key_columns => [ 'id' ],
    foreign_keys => [
        debtor_obj => {
            class       => 'RDBO::Bk::Debtor',
            key_columns => { debtor => 'id' },
        },
    ],
    relationships => [
        debtors => {            
            class      => 'RDBO::Bk::Debtor',
            column_map => { id => 'current_name' },
            type       => 'one to many',
        },
    ],
);

In the same directory as those files that define my Debtor and Name
Rose::DB::Object-descended objects files are directories called Debtor
and Name, each with a file called Form.pm.  These seem to be my
Rose::HTML::Form objects.  Each of those Forms has a method called
init_with_debtor or init_with_name and one called debtor_from_form or
name_from_form, respectively.  I am under the impression that the
debtor_from_form method is called by form_to_object in
CatalystX::CRUD::Controller::RHTMLO.

So, now firing up the app that was magically created for me, I have a
pulldown menu with the name of app on it.  On this menu I have items
for Debtor and Name.  I see an option to create a Debtor, and I get a
blank form.  In the HTML source of the form, searching for "name='id'"
finds nothing.  So far so good.

Now I see that the current_name field has a text field for me to enter
the value of the id column of this debtor's row in the names table.
What I want to do is to replace this with three text fields for first,
middle, and last names.  I think the way to do this is with nested
forms.

http://search.cpan.org/~jsiracusa/Rose-HTML-Objects-0.6061/lib/Rose/HTML/Form.pm#NESTED_FORMS

So I edit the Debtor Form file in lib/RDBO/Bk/Debtor/Form.pm, which is
a descendant of Rose::HTML::Form.  Into the build_form method of my
Debtor Form object I add a line:

$self->add_form(name_form => RDBO::Bk::Name::Form->new);

This is a nested form, right?  Now I restart the app and go to the
webpage for creating a new debtor.  Aha!  I have the fields for first,
middle, & last names.

I look at the HTML source and see the last_name text field is an
<input> element containing the name attribute "name_form.last_name".
Okay, that looks like it has enough information for my app to figure
out that the value of that field goes into the names table rather than
the debtors table.

Now I try creating a new debtor record with a name.  No dice.  I have
to get the app to save the name record related to the debtor record at
the same time.

Still editing my Debtor/Form.pm file, the one with build_form() that I
just amended with add_form(), I make some changes to the
debtor_from_form method.  I changed it from this:

sub debtor_from_form {
    my $self = shift;
    $self->object_from_form(@_);
}

into this:

sub debtor_from_form {
    my $self = shift;
    my (@args) = @_;
    my $debtor = $self->object_from_form(@args);
    my $name_form = $self->form('name_form');
    my $name_obj = $name_form->name_from_form($name_form->object_class);
    $debtor->name($name_obj);
    return $debtor;
}

Restart the app, create a debtor with names, and yes, I have added my
data to both the debtors and names tables, and the current_name column
of the newly-created debtor's row in the debtors table correctly
contains the value, newly created, of the id column of the row with
the new debtor's first, middle, & last names.  It worked.

But, alas, on the webpage displayed to me after hitting submit, the
first, middle, last name fields have no values.  Also, there's another
problem: I didn't metion it above, but it seems that the line adding
the nested name_form into the Debtor::Form:

self->add_form(name_form => RDBO::Bk::Name::Form->new);

within the build_form() method of my Form, that line breaks something.
To wit: when I use the pull-down menu that was magically created for
me, and try to see a list all the debtors in my database, I get no
results, which was working before.  That is at the uri:
mycontroller/bk/debtor/search.  That Catalyst controller named
Debtor.pm is based on (descended from)
Rose::DBx::Garden::Catalyst::Controller, which is descended from the
CatalystX::CRUD::Controller.

Anyway, the whole time I'm doing this, I'm thinking, I can't be doing
this the right way.  It seems like there should be a better way to do
this.  I would be eternally grateful to anyone who can understand what
I wrote an shed any light on this for me.

I'm sure there's a way to use CatalystX::CRUD::Controller::RHTMLO to
affect related database tables at the same time.  As obvious as it may
seem to be after I know it, I haven't figured it out on my own yet.

Cheers,
Adam Mackler



More information about the Catalyst mailing list