Catalyst et DBIx

user_icon admin | icon2 Catalyst | icon4 19/11/2007 1h21| Type doc: article| Type File: txt| icon3 No Comment

Catalyst et DBIx


1. DBIx::Class et les relationships

Il s'agit d'un module objet Perl permettant l'accès au base de données sans en connaitre la syntaxe SQL.

Dans ce chapitre nous nous intéresserons qu'aux relations inter tables (relationships) et nous verrons comment nous aider de Catalyst pour nous faciliter la vie.

HAS_A :

Prenons un exemple simple de base de données comportant deux tables (user et doc)


-- Users
CREATE TABLE user (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(30) NOT NULL,
        password VARCHAR(40) NOT NULL,
        firstname VARCHAR(40),
        lastname VARCHAR(40)
);

-- Docs

CREATE TABLE doc (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        nom VARCHAR(30) NOT NULL,
        user_id INTEGER

);

On constate qu'il existe une relation entre la table ' doc' et la table ' user' via le champ ' user_id'. La traduction en langage clair : A chaque document correspond un utilisateur. Il s'agit là d'une relation ' has_a'.

HAS_MANY:

Imaginons maintenant que nous souhaitions qu'a chaque document soit affecté plusieurs utilisateurs. Il nous faudrait alors une relation 'has_many'. Il nous faudrait alors ajouter une table intermédiaire 'user_doc':


-- Mapping User Docs
CREATE TABLE user_doc (
        user_id INTEGER,
        doc_id INTEGER,
        PRIMARY KEY (user_id, doc_id)
);

Avec cette dernière nous pouvons alors dire qu'a chaque document correspond plusieurs utilisateurs.

MANY_TO_MANY:

Avec cette même table intermédiaire la relation peut se faire dans les deux sens. Un document peut avoir plusieurs utilisateurs mais l'inverse est aussi vrai. Un utilisateur peut avoir plusieurs documents.

Mais voyons voir comment intégrer ce principe à Catalyst.

2. HAS_A / HAS_MANY

Commençons par créer une simple application:

catalyst.pl monapp
cd monapp

Notre base de donnée sera au format SQLite:

mkdir db
cat > db/monapp.sql << __EOF__
-- Users
CREATE TABLE user (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        username VARCHAR(30) NOT NULL,
        password VARCHAR(40) NOT NULL,
        firstname VARCHAR(40),
        lastname VARCHAR(40)
);

-- Docs
CREATE TABLE doc (
        id INTEGER AUTO_INCREMENT PRIMARY KEY,
        nom VARCHAR(30) NOT NULL,
        user_id INTEGER
);
__EOF__


sqlite3 db/monapp.db < db/monapp.sql

Et pour finir on se créer un modèle de donnée (notre base en l'occurence).

perl script/monapp_create.pl model DB DBIC::Schema DB::Schema create=static dbi:SQLite:db/monapp.db

Que s'est-il passé:

Le répertoire lib/DB est né et le fichier ./lib/monapp/Model/DB.pm a été créé. Jetons un oeil à ce dernier.

package monapp::Model::DB;

use strict;

use base 'Catalyst::Model::DBIC::Schema';

__PACKAGE__->config(
    schema_class => 'DB::Schema',
    connect_info => [
        'dbi:SQLite:db/monapp.db',

    ],
);

Catalyst ( et DBIx ) à créé la classe Docs::Model::DB nécessaire à la configuration de notre base de donnée. Si nous utilisions une autre base de donnée ( et de nombreuses sont reconnues ) c'est ici que la modification devrait s'effectuer ( connect_info ).

La clé ' schema_class' indique quel est le schema à utiliser. Celui-ci doit être en concordance avec la déclaration de la base.

Dans notre cas la classe du schema est lib/DB/Schema.pm ( car je l'ai décidé lors de l'exécution du doc_create.pl )


package DB::Schema;

# Created by DBIx::Class::Schema::Loader v0.03009 @ 2007-11-18 15:15:31

use strict;
use warnings;

use base 'DBIx::Class::Schema';

__PACKAGE__->load_classes;

1;

load_classes' permet de spécifier les tables à charger. Par defaut toutes les tables du schema sont prisent en compte mais nous aurions pu utiliser que la table 'user'.

__PACKAGE__->load_classes(qw/ User /);

Mais ou sont elles déclarées ? Dans le répertoire lib/DB/Schema/ :

Si on jette un oeil à lib/DB/ on constate que Catalyst via DBIx à découvert les tables de la base db/monapp.db


ls lib/DB/Schema/
Doc.pm  User.pm

Regardons la déclaration de la table lib/DB/Schema/Doc.pm :

package DB::Schema::Doc;

# Created by DBIx::Class::Schema::Loader v0.03009 @ 2007-11-18 15:17:02

use strict;

use warnings;

use base 'DBIx::Class';

__PACKAGE__->load_components("PK::Auto", "Core");
__PACKAGE__->table("doc");
__PACKAGE__->add_columns(
  "id",
  { data_type => "INTEGER AUTO_INCREMENT", is_nullable => 0, size => undef },
  "nom",
  { data_type => "VARCHAR", is_nullable => 0, size => 30 },
  "user_id",
  { data_type => "INTEGER", is_nullable => 0, size => undef },
);
__PACKAGE__->set_primary_key("id");

1;

Que nous dit se fichier ?

Et bien que la table 'doc' :

  • posséde les champs ' id', 'nom' et 'user_id'

  • Que la clé primaire est 'id'

Mais pas de relation avec la table 'user' :( D'ailleurs comment Catalyst aurait pu découvrir que 'user_id' est en fait une référence à la table 'user' ?

Deux possibilités: soit on le décrit manuellement ou c'est lors de la déclaration de la structure de la base qu'on le précise.

DE MANIERE MANUELLE:

Pour indiquer que le champ 'user_id' appartient à la table user il nous suffit d'ajouter la ligne suivante au fichier de déclaration de la table Doc:

__PACKAGE__->belongs_to("user_id", "DB::Schema::User", { id => "user_id" });

Et d'indiquer dans la déclaration de la table user qu'il existe une relation avec la table 'doc'

__PACKAGE__->has_many("docs", "DB::Schema::Doc", { "foreign.user_id" => "self.id" });

AUTOMATIQUEMENT:

Faire référence à une autre table est très simple, il nous suffit de modifier la ligne :

user_id INTEGER

par

user_id INTEGER REFERENCES user

Et donc si l'on recréer la base db/monapp.db, que l'on supprime les classes précédement découvertes et que l'on rééexécute la commande de découverte de la base nous pouvons constater que Catalyst à découvert la relation entre table.

En fait il a exactement fait ce que nous avons fait de manière manuelle d'ou l'importance de bien déclarer les relation entre tables.

3. Utilisation dans Catalyst

Pour accéder à nos données il nous faut un controleur 'doc'

 perl script/monapp_create.pl controller doc

Dans ce controleur définissons la méthode list qui nous permettra de ... lister les docs.

Pour cela j'ajoute ceci a mon controlleur doc ( lib/monapp/Controller/doc.pm ):

sub list : Local {
    my ( $self, $c ) = @_;

    $c->stash->{docs} = [$c->model('DB::Doc')->all];
    $c->stash->{template} = 'doc/list.tt2';
}


Et donc dans la variable 'docs' de stash (pour passage au template doc/list.tt2) nous aurons tous les documents.

Encore une dernière chose pour afficher les résultats, la création du template root/doc/list.tt2

mkdir root/doc

cat > root/doc/list.tt2 << __EOF__
<table>
    <tr><th>id</th><th>nom</th><th>user</th></tr>

    [% FOREACH doc IN docs -%]
      <tr>
        <td>[% doc.id %]</td>
        <td>[% doc.nom %]</td>
        <td>[% doc.user_id.username %]</td>

      </tr>
    [% END -%]
</table>
__EOF__

Le point intéressant est l'affichage du nom de l'utilisateur. On fait donc bien références à une table extérieure.

Notre vue utilisera par default les templates toolkit pour afficher le résultat des requêtes. Il nous faut le préciser dans lib/Docs.pm :


__PACKAGE__->config( name => 'monapp',
                     default_view => 'TT');

et ensuite créer la vue par défaut

$ perl script/docs_create.pl view TT TT

Si l'on se connecte à http://localhost:3000/doc/list

Pour chaque document nous avons bien ( beyong_to) un et un seul utilisateur. Par contre chaque utilisateur à plusieurs document ( has_many). Constatons le de suite.

Creation du controleur 'user' et de son template associé.

./script/monapp_create.pl controller user


En y ajoutant la méthode 'list'

sub list : Local {
    my ( $self, $c ) = @_;

    $c->stash->{template} = 'user/list.tt2';
    $c->stash->{users} = [$c->model('DB::User')->all];
}

Et son template root/user/list.tt2 :

<table>

    <tr><th>id</th><th>username</th><th>docs</th></tr>
    [% FOREACH user IN users -%]
      <tr>

        <td>[% user.id %]</td>
        <td>[% user.username %]</td>
        <td>[% FOREACH doc IN user.docs %]
               [% doc.nom %]
            [% END %]
        </td>
      </tr>

    [% END -%]
</table>

Cette fois-ci la relation se nomme 'docs' ( voir lib/DB/Schema/User.pm )

Ce qui nous donne :

héhé correct :)

4. MANY_TO_MANY

Contrairement à l'exemple précédent nous souhaitons qu'a chaque document puisse être associé plusieurs utilisateurs et vice versa. Il s'agit d'une relation many-to-many et pour cela il nous faut une table intermédiaire (user_doc)


-- Mapping User Doc
CREATE TABLE user_doc (
        user_id INTEGER REFERENCES user,
        doc_id INTEGER REFERENCES doc,
        PRIMARY KEY (user_id, doc_id)
);

Et supprimer la références à user_id dans la table Doc.

On lance ensuite la découverte des tables:

perl script/monapp_create.pl model DB DBIC::Schema DB::Schema create=static dbi:SQLite:db/monapp.db

Les classes des tables User.pm et Doc.pm comporte chacune une relation 'has_many' vers la table UserDoc, et la table UserDoc comporte deux relation beyon_to vers les tables User et Doc.

Mais il nous manque la relation many_to_many :( que j'ajoute à la fin de la classe Doc.pm


__PACKAGE__->many_to_many(users => 'user_docs', 'user_id');

Il ne me reste plus qu'a créer le template root/doc/list.tt2.

<table>
    <tr><th>id</th><th>nom</th><th>user</th></tr>

    [% FOREACH doc IN docs -%]
      <tr>
        <td>[% doc.id %]</td>
        <td>[% doc.nom %]</td>
        <td>[% FOREACH user IN doc.users -%]
               [% user.username %]
            [% END %]
        </td>

      </tr>
    [% END -%]
</table>

Notez que j'utilise la relation many_to_many 'users' précédement définie.

J'utilise la même méthode 'list' dans le controleur Doc, ce qui nous donne:

5. Insertion en base

Jsuqu'a maintenant nous avons fait que lister les éléments en base. Voyons de plus près leur insertion.

LA METHODE CLASSIQUE:

Pour ajouter un document en base nous devrons disposer de :

  • d'un formulaire d'ajout ( root/doc/form_add.tt2 )

  • d'une méthode 'form_add' pour afficher le formulaire

  • d'une méthode 'do_add' pour l'insertion en base

Notre méthode 'form_add' peut ressembler à ceci:


sub form_add : Local {
    my ( $self, $c) = @_;

    $c->stash->{users} =  [$c->model('DB::User')->all];
    $c->stash->{template} = 'doc/form_add.tt2';
}

Dans la variable stash 'users' nous insérons tous les utilisateurs et affichons le template root/doc/form_add.tt2 dont voici le code:

<form method="get" action="[% c.uri_for('do_add') %]">
<table>
  <tr><td>Nom du document:</td><td><input type="text" name="nom"></td></tr>

  <tr><td>Utilisateur</td>
      <td>
         <select name="user_id">
        [% FOREACH user IN users %]
                <option value="[% user.id %]">[% user.username %]</option>

        [% END %]
        </select>
      </td>
</table>
<input type="submit" name="Submit" value="Submit">
</form>

Nous aurons donc en entrée le nom du document et la liste des utilisateurs présent en base. Après le submit le flux sera redirigé vers la méthode 'do_add' du con,troleur 'doc'. 'do_add' va récupérer les variables CGI fournie,les insérer en base et regirigé le tout vers le listing des documents. et donc ajoutons 'do_add' à notre controleur.


sub do_add : Local {
    my ( $self, $c ) = @_;

    my $nom     = $c->request->params->{nom};
    my $user_id = $c->request->params->{user_id};
    my $doc = $c->model('DB::Doc')->create({
                             nom   => $nom,
                         });

    $doc->add_to_user_docs({user_id => $user_id});
    $c->forward("/doc/list");
}

Notons la venue de la méthode 'add_to_user_docs' pour mettre à jour notre table intermédiaire qui est automatiquement comprise par Catalyst :)

Bon certes ça reste bien lourd à mettre en oeuvre, de plus nous n'avons pas encore vu la validation des entrées :(

6. DBIx et FormFu

Heureusement pour nous Carl Franck développe pour nous le module Perl FormFu bien pratique pour la création des formulaires. Et mieux encore il développe le module DBIx::Class::HTML::FormFu qui va grandement nous aider lors de la création de nos formulaires :)

Le prérequi est bien sur d'avoir installé les modules Perl " HTML::FormFu" , Catalyst::Controller::HTML::FormFu et " DBIx::Class::HTML::FormFu".

Attention

ATTENTION: Les modules Perl doivent être installés à partir du dépot svn


svn checkout http://html-formfu.googlecode.com/svn/trunk/ trunk

Pour l'utilisation de ces modules voir le document suivant : Catalyst et formFu

La première chose à faire est d'insérer les fichiers template de FormFu par la commande suivante:

./script/myapp_create.pl HTML::FormFu

Il faut aussi que notre application prend en charge FormFu. Pour cela ajoutons le composant HTML::FormFu au fichier lib/DB/Schema/Doc.pm comme ceci:


__PACKAGE__->load_components("HTML::FormFu", "PK::Auto", "Core");

Ensuite dans notre controleur 'doc' modifions l'héritage de celui-ci

use base 'Catalyst::Controller::HTML::FormFu';

Ensuite ajoutons la methode ' add_ff' (Formulaire 'add' via formFu )

sub add_ff : Local : FormConfig {
        my ( $self, $c ) = @_;

        my $user_id = $c->request->params->{user_id};

        my $form = $c->stash->{form};
        if ( $c->req->params->{nom} ){
          my $doc =  $c->model('DB::Doc')->new({});
          $doc->populate_from_formfu( $form );
          $doc->add_to_user_docs({user_id => $user_id});
        }
        if ( $form->submitted &amp;&amp; !$form->has_errors ) {
                $c->forward("/doc/list") ;
        }

}

Pour ce qui concerne les relationships j'ai utilisé la même méthode que précédement ( add_to_user_docs ). Je n'ai jusqu'a maintenant pas trouvé de moyen de moyen plus élégant de le mettre en oeuvre. :(

Ensuite on créer le template utilisée par FormFu root/doc/add_ff.tt .

[% form %]

Et pour finir le fichier YAML de définition de notre formulaire root/forms/doc/add_ff.yml .

mkdir -p root/forms/doc

cat > root/forms/doc/add_ff.yml << __EOF__
elements:
      - type: Text
        name: nom
        label: Nom document
        constraints:
          - Required
      - type: DBIC::Select
        model: 'DB::User'
        label: Nom utilisateur
        name: user_id
        name_field: username
        value_field: id
      - type: Submit
__EOF__


Le point intéressant à ce niveau est le SELECT alimenté par une requête de la table 'user'. Pour plus d'infos voir HTML::FormFu::Element::DBIC::Select


Add a comment

Validator_logo
Catapulse v0.06
( 0.11585 s)