Presentation de Class::DBI

Laurent GAUTROT

Red Hat

Historique

  • Programming the Perl DBI, by Alligator Descartes and Tim Bunce. Due to be published by O'Reilly September/October 1999
  • Decembre 1999 : premiere version publiee de Class::DBI
  • 5 Novembre 2006 : Class::DBI 3.0.16

Fonctionnalites

  • Utilise DBI
  • 1 classe <=> 1 table

    ORM Abstraction sur un schema simple de correspondance entre un modele objet et un modele relationnel existant.

    Ces correspondances sont les plus simples et les plus evidentes a mettre en oeuvre.

    La methode table() definit la table de la base de donnees sur laquelle va etre effectuee la correspondance.

    La methode table_alias() permet d'utiliser un nom alternatif en particulier si la table porte un nom qui est un mot reserve du SQL.

    La methode sequence() ou auto_increment() permet de definir une sequence, comme son nom l'indique.

  • Description des colonnes (columns())

    Il y a quatre familles de colonnes avec un mot-clef associe.

    All est utilise pour enumerer toutes les colonnes.

    Primary concerne la ou les colonnes qui constituent la clef primaire. Si cette famille n'est pas decrite, on considere qu'il s'agit d'une clef primaire sur un seul champ, et en l'occurence, le premier de All.

    Essential regroupe les colonnes indispensables qui sont retournees lors d'une interrogation.

    Enfin, TEMP est utilise pour des champs calcules ou d'autres valeurs qui ne seront pas stockees en base.

  • Relations 1-n, 0-1, n-m

    Les relations 1-n, 0-1 sont traditionnellement faciles a modeliser.

    Pour les relations n-m, la modelisation est plus complexe, mais en gros, on a une table d'association et eventuellement des attributs sur cette association.

  • Description des relations (has_a(), has_many(), might_have())
  • Accesseurs crees automagiquement

    Chaque colonne decrite grace a la methode columns() est utilisable en lecture et en ecriture. Toutefois, les colonnes TEMP ne sont pas vraiment utilisables en ecriture. Il est possible de modifier leur valeur, mais elle ne sera pas enregistree.

Modules utiles

  • Class::DBI::Loader

    Class::DBI::Loader est un module qui permet d'analyser une base de donnees a la recherche de la structure.

    Les metadonnees sont exploitees pour construire automatiquement les classes, pour les SGBD supportes. Sybase PostgreSQL, Informix, Oracle, DB2, MySQL, SQLite. CDBI::L::GraphViz est utilise pour exporter des graphes GraphViz.

  • Class::DBI::Loader::Relationship

    Class::DBI::Loader::Relationship permet d'exprimer les relations entre classes et donc entre tables a l'aide de descriptions en langage naturel.

    Les singuliers et les pluriels sont pris en charge, pour faciliter l'ecriture.

  • Class::DBI::AbstractSearch

    Class::DBI::AbstractSearch permet d'enrichir l'expression de certaines requetes en ajoutant des criteres de recherche interpretes comme dans SQL.

    Il est posible de specifier des noms de colonne avec des restrictions ou des ordre de tri, par exemple.

Declaration du modele relationnel

   package Asso::DBI;
   use base 'Class::DBI';
   Asso::DBI->connection(
      'dbi:SQLite:dbname=rando.db', '', ''
   );
   package Asso::rando;
   use base 'Asso::DBI';
   Asso::rando->table('rando');
   Asso::rando->columns(All => qw/
       id         jour         titre         distance
       rythme     publication  descriptif
   /);
   __PACKAGE__->has_many(
    randoanimateurs => [
     'Asso::RandoAnimateur' => 'animateur'
    ]
   );

Un exemple de description complete du modele :

 #!/usr/bin/perl
 
 use strict;
 use warnings;
 
 package Asso::DBI;
 use base 'Class::DBI';
 __PACKAGE__->connection( 'dbi:SQLite:dbname=rando.db', '', '' );
 
 package Asso::Rando;
 use base 'Asso::DBI';
 __PACKAGE__->table('rando');
 __PACKAGE__->columns(
     All => qw/ id jour titre distance rythme publication descriptif / );
 __PACKAGE__->has_many(
     animateurs => [ 'Asso::RandoAnimateur' => 'animateur' ] );
 __PACKAGE__->constrain_column( rythme => [qw/L M S R/] );
 __PACKAGE__->has_many( inscriptions => 'Asso::Inscription' );
 
 package Asso::Adherent;
 use base 'Asso::DBI';
 __PACKAGE__->table('adherent');
 __PACKAGE__->columns(
     All => qw/id nom prenom naissance email adhesion echeance/ );
 __PACKAGE__->has_many( animateurs   => 'Asso::Animateur' );
 __PACKAGE__->has_many( inscriptions => 'Asso::Inscription' );
 
 package Asso::Inscription;
 use base 'Asso::DBI';
 __PACKAGE__->table('inscription');
 __PACKAGE__->columns( Primary => qw/rando_id adherent_id/ );
 __PACKAGE__->columns( All     => qw/rando_id adherent_id/ );
 __PACKAGE__->has_a( rando_id    => 'Asso::Rando' );
 __PACKAGE__->has_a( adherent_id => 'Asso::Adherent' );
 
 package Asso::Animateur;
 use base 'Asso::DBI';
 __PACKAGE__->table('animateur');
 __PACKAGE__->columns( All => qw/adherent_id date_diplome parrain_id/ );
 __PACKAGE__->has_a( adherent_id => 'Asso::Adherent' );
 __PACKAGE__->has_a( parrain_id  => 'Asso::Adherent' );
 __PACKAGE__->has_many( randos => [ 'Asso::RandoAnimateur' => 'rando' ] );
 
 package Asso::RandoAnimateur;
 use base 'Asso::DBI';
 __PACKAGE__->table('rando_animateur');
 __PACKAGE__->columns( Primary => qw/rando_id animateur_id/ );
 __PACKAGE__->columns( All     => qw/rando_id animateur_id/ );
 __PACKAGE__->has_a( rando_id     => 'Asso::Rando' );
 __PACKAGE__->has_a( animateur_id => 'Asso::Animateur' );
 
 1;

Idem, avec CDBI::L

   use Class::DBI::Loader;
   
   my $loader = Class::DBI::Loader->new(
       dsn       => 'dbi:SQLite:dbname=rando.db',
       namespace => ''
   );

Dans cette optique, il suffit de fournir a Class::DBI::Loader() une chaine de connexion et une chaine de caractere qui permet de definir un espace de nommage

On y gagne en description, mais il faut que la base de donnees soit supportee pour le reverse engineering

Relations 1-n

 Asso::Inscription->has_a(
     rando_id => 'Asso::Rando' );
 Asso::Rando->has_many(
      inscriptions => 'Asso::Inscription' );

La relation est exploitable dans les deux sens. Il y a donc une relation has_a dans un sens et la reciproque has_many dans l'autre

Faciliter la description

 use Class::DBI::Loader::Relationship;
 
 my $loader = Class::DBI::Loader->new(
       dsn       => 'dbi:SQLite:dbname=rando.db',
       namespace => ''
 );
 
 $loader->relationship(
    "a rando has rando_animateurs"
 );

Relations n-m

  • Description CDBI classique
     Asso::Animateur->has_many( 
        randos => [
           'Asso::RandoAnimateur' => 'rando' ] );
     Asso::RandoAnimateur->has_a(
        animateur_id => 'Asso::Animateur' );
     Asso::RandoAnimateur->has_a(
        rando_id     => 'Asso::Rando' );
     Asso::Rando->has_many(
        animateurs => [
            'Asso::RandoAnimateur' => 'animateur' ] );
  • Description CDBIL::relationship
     $loader->relationship(
        "a rando has animateurs on rando_animateurs"
     );

Integrite referentielle

  • Controlable a l'aide d'un hash anonyme
     Asso::Animateur->has_many( 
        randos => [
           'Asso::RandoAnimateur' => 'rando' ],
       { cascade => 'Delete' });
  • Valeur de cascade
    • 'Delete'
    • 'Fail'
    • 'None'
    • Plugin

      Si l'on ne se satisfait pas des comportements Suppression en cascade, levee d'exception ou laisser des enregistrements orphelins, alors on peut ecrire son propre plugin qui sera execute lors de la suppression de l'enregistrement

Contraintes

  • Definition avec add_constraint
     Asso::Rando->add_constraint(
         'liste_rythme', rythme => \&verifie_rythme
     );
     sub verifie_rythme {
         my ($rythme) = @_;
         return $rythme =~ /^[LMSR]$/;
     }

    Cette methode permet d'appeler une fonction pour valider des contraintes qui peuvent etre complexes. Dans l'exemple, la contrainte est enumerative et peut etre avantageusement remplacee par le raccourci ci-apres

  • Raccourci avec constraint_column
     Asso::Rando->constraint_column(
      rythme => [qw/L M S R/]);

    La methode constraint_column specifie une simple instructions pour valider une correspondance ou une egalite. Dans l'exemple, la contrainte est verifiee a l'aide d'une liste

Declencheurs

  • Meme si le SGBD ne le supporte pas
     Asso::Rando->add_trigger(
        after_create => sub {
            my ( $self, %args ) = @_; 
            $self->rythme='L';
        }   
     );

    Cet exemple force une valeur lors de l'ajout d'un enregistrement, et n'est pas vraiment utile. Neanmoins, il illustre la facilite d'ecriture des declencheurs

  • Plusieurs evenements
    • before_create
    • after_create
    • before_set_$column
    • after_set_$column
    • before_update
    • after_update
    • before_delete
    • after_delete
    • select

      Pour ce dernier point, attention aux degradations de performances

Recherches

  • search

    Permet de rechercher

  • search_like

    Idem a ce qui precede mais permet de retourner des listes d'objets sur des chaines partielles (%)

  • retrieve

    Cette methode retourne un objet

  • retrieve_all
     foreach my $rando (Asso::Rando->retrieve_all) {
        printf( "%s : %s (%s)\n",
            $rando->titre, $rando->descriptif, $rando->rythme );
     }

    Cette methode retourne tous les objets

Manipulation

  • Ajout

Ajout

 Rando::rando->insert(
    {
        jour        => '2006-11-25',
        titre       => 'La Villette',
        distance    => 0.5,
        rythme      => L,
        publication => 1,
        descriptif =>
          'Le marathon des journees Perl francophones debutera samedi 25',
    }
 );

Suppression

 $rando->delete;

Si l'on a un objet que l'on souhaite supprimer

 
 Asso::Rando->retrieve(1)->delete;

On peut aussi supprimer un objet que l'on aurait juste recupere par son identifiant

 
 Asso::Rando->search(jour => '2006-11-25')->delete_all;

On peut aussi supprimer toute une liste d'objets retournes par la methode search

 
 Asso::Rando->search_like(titre => 'La Ville%')->delete_all;

Idem avec search_like

  

Mise a jour

 $rando = Asso::Rando->retrieve(1);
 
 $rando->titre('FPW2006');
 
 $rando->update;

L'accesseur titre permet de lire ou de modifier la valeur

Il n'y a que la clef primaire qui ne doit pas etre modifie ou ca va mal se passer

Iterateurs

 my $iterator = Asso::Rando->retrieve_all;
 
 while (my $rando = $iterator->next) {
    printf( "%s : %s (%s)\n",
        $rando->titre, $rando->descriptif, $rando->rythme );
 }

Au lieu de retourner une liste d'objets, on affecte le resultat a un scalaire, et on itere a l'aide de la methode next. L'objet est alors utilise exactement comme dans le cas decrit precedement

Le gain est appreciable dans les recherches qui retournent enormement de lignes (+ de quelques centaines de milliers)

Autre module interessant

  • Class::DBI::AbstractSearch
    • implante SQL::Abstract::Limit dans CDBI
    • ajoute une methode search_where

Conclusion

  • Facile a utiliser

    Raccourcis d'ecriture possibles

  • Pas de SQL pour les cas courants ...
  • ... mais set_sql() si besoin

    Pour des jointures, ou autres operateurs comme UNION

  • Tres pratique pour des migrations (partielles)

    Simplement en ayant des classes de base qui utilisent des chaines de connexion DBI differentes

  • De nombreux modules additionnels

References